diff --git a/HISTORY.md b/HISTORY.md
index 7fb944bd..94b2007f 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,8 +1,31 @@
-vis.js history
+# vis.js history
http://visjs.org
-## 2014-04-16, version 0.7.4
+## not yet released, version 0.8.0
+
+### Timeline
+
+- Large refactoring of the Timeline, simplifying the code.
+- Performance improvements.
+- Improved layout of box-items inside groups.
+- Function `setWindow` now accepts an object with properties `start` and `end`.
+- Fixed option `autoResize` forcing a repaint of the Timeline with every check
+ rather than when the Timeline is actually resized.
+- Fixed `select` event fired repeatedly when clicking an empty place on the
+ Timeline, deselecting selected items).
+- Fixed initial visible window in case items exceed `zoomMax`. Thanks @Remper.
+- Option `order` is now deprecated. This was needed for performance improvements.
+- Minor bug fixes.
+- More examples added.
+
+### DataSet
+
+- A DataSet can now be constructed with initial data, like
+ `new DataSet(data, options)`.
+
+
+## 2014-04-18, version 0.7.4
### Graph
@@ -11,7 +34,6 @@ http://visjs.org
- minor bug fixes.
-
## 2014-04-16, version 0.7.3
### Graph
@@ -142,7 +164,7 @@ http://visjs.org
- Moved the generated library to folder `./dist`
- Css stylesheet must be loaded explicitly now.
-- Implemented options `showCurrentTime` and `showCustomTime`. Thanks fi0dor.
+- Implemented options `showCurrentTime` and `showCustomTime`. Thanks @fi0dor.
- Implemented touch support for Timeline.
- Fixed broken Timeline options `min` and `max`.
- Fixed not being able to load vis.js in node.js.
diff --git a/Jakefile.js b/Jakefile.js
index 671fb6e1..c85d1b40 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -67,7 +67,6 @@ task('build', {async: true}, function () {
'./src/timeline/TimeStep.js',
'./src/timeline/Stack.js',
'./src/timeline/Range.js',
- './src/timeline/Controller.js',
'./src/timeline/component/Component.js',
'./src/timeline/component/Panel.js',
'./src/timeline/component/RootPanel.js',
diff --git a/bower.json b/bower.json
index e8b547b2..45abe51a 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "vis",
- "version": "0.7.4-SNAPSHOT",
+ "version": "0.7.5-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {
diff --git a/dist/vis.css b/dist/vis.css
index a841904e..46d6b19c 100644
--- a/dist/vis.css
+++ b/dist/vis.css
@@ -9,79 +9,86 @@
border: 1px solid #bfbfbf;
-moz-box-sizing: border-box;
box-sizing: border-box;
+
+ /* FIXME: there is an issue with the height of the items when panel height is animated
+ -webkit-transition: height 4s ease-in-out;
+ transition: height 4s ease-in-out;
+ /**/
}
.vis.timeline .vpanel {
position: absolute;
overflow: hidden;
-}
-
-.vis.timeline .groupset {
- position: absolute;
- padding: 0;
- margin: 0;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
}
-.vis.timeline .labels {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
+.vis.timeline .vpanel.side {
+ border-right: 1px solid #bfbfbf;
+}
- padding: 0;
- margin: 0;
+.vis.timeline .vpanel.side.hidden {
+ display: none;
+}
- border-right: 1px solid #bfbfbf;
- -moz-box-sizing: border-box;
- box-sizing: border-box;
+.vis.timeline .groupset {
+ position: relative;
}
-.vis.timeline .labels .label-set {
- position: absolute;
- top: 0;
- left: 0;
+.vis.timeline .labelset {
+ position: relative;
width: 100%;
- height: 100%;
overflow: hidden;
- border-top: none;
- border-bottom: 1px solid #bfbfbf;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
}
-.vis.timeline .labels .label-set .vlabel {
- position: absolute;
+.vis.timeline .labelset .vlabel {
+ position: relative;
left: 0;
top: 0;
width: 100%;
color: #4d4d4d;
+
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
}
-.vis.timeline.top .labels .label-set .vlabel,
-.vis.timeline.top .groupset .itemset-axis {
+.vis.timeline.bottom .labelset .vlabel,
+.vis.timeline.top .vpanel.side-content,
+.vis.timeline.top .groupset .itemset {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
-.vis.timeline.bottom .labels .label-set .vlabel,
-.vis.timeline.bottom .groupset .itemset-axis {
+.vis.timeline.top .labelset .vlabel,
+.vis.timeline.bottom .vpanel.side-content,
+.vis.timeline.bottom .groupset .itemset {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
-.vis.timeline .labels .label-set .vlabel .inner {
+.vis.timeline .labelset .vlabel .inner {
display: inline-block;
padding: 5px;
}
.vis.timeline .itemset {
- position: absolute;
+ position: relative;
padding: 0;
margin: 0;
- overflow: hidden;
+
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+
+ /* FIXME: get transition working for rootpanel and itemset
+ -webkit-transition: height 4s ease-in-out;
+ transition: height 4s ease-in-out;
+ /**/
}
.vis.timeline .background {
@@ -90,8 +97,8 @@
.vis.timeline .foreground {
}
-.vis.timeline .itemset-axis {
- position: absolute;
+.vis.timeline .axis {
+ overflow: visible;
}
@@ -102,6 +109,11 @@
background-color: #D5DDF6;
display: inline-block;
padding: 5px;
+
+ /* TODO: enable css transitions
+ -webkit-transition: top .4s ease-in-out, bottom .4s ease-in-out;
+ transition: top .4s ease-in-out, bottom .4s ease-in-out;
+ /**/
}
.vis.timeline .item.selected {
@@ -118,7 +130,8 @@
background-color: #FFF785;
z-index: 999;
}
-.vis.timeline .item.point.selected .dot {
+.vis.timeline .item.point.selected .dot,
+.vis.timeline .item.dot.selected {
border-color: #FFC200;
}
@@ -179,6 +192,11 @@
width: 0;
border-left-width: 1px;
border-left-style: solid;
+
+ /* TODO: enable css transitions
+ -webkit-transition: height .4s ease-in-out, top .4s ease-in-out;
+ transition: height .4s ease-in-out, top .4s ease-in-out;
+ /**/
}
.vis.timeline .item .content {
@@ -220,18 +238,18 @@
z-index: 10001; /* a little higher z-index than .drag-left */
}
-.vis.timeline .axis {
- position: relative;
+.vis.timeline .timeaxis {
+ position: absolute;
}
-.vis.timeline .axis .text {
+.vis.timeline .timeaxis .text {
position: absolute;
color: #4d4d4d;
padding: 3px;
white-space: nowrap;
}
-.vis.timeline .axis .text.measure {
+.vis.timeline .timeaxis .text.measure {
position: absolute;
padding-left: 0;
padding-right: 0;
@@ -240,13 +258,13 @@
visibility: hidden;
}
-.vis.timeline .axis .grid.vertical {
+.vis.timeline .timeaxis .grid.vertical {
position: absolute;
width: 0;
border-right: 1px solid;
}
-.vis.timeline .axis .grid.horizontal {
+.vis.timeline .timeaxis .grid.horizontal {
position: absolute;
left: 0;
width: 100%;
@@ -254,11 +272,11 @@
border-bottom: 1px solid;
}
-.vis.timeline .axis .grid.minor {
+.vis.timeline .timeaxis .grid.minor {
border-color: #e5e5e5;
}
-.vis.timeline .axis .grid.major {
+.vis.timeline .timeaxis .grid.major {
border-color: #bfbfbf;
}
diff --git a/dist/vis.js b/dist/vis.js
index 253cd560..dc4cf3bf 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -4,8 +4,13 @@
*
* A dynamic, browser-based visualization library.
*
+<<<<<<< HEAD
* @version 0.7.4-SNAPSHOT
* @date 2014-04-23
+=======
+ * @version 0.7.5-SNAPSHOT
+ * @date 2014-04-22
+>>>>>>> fc75aed4cec56adbced08aa1804e23797703776f
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -407,6 +412,23 @@ util.extend = function (a, b) {
return a;
};
+/**
+ * Test whether all elements in two arrays are equal.
+ * @param {Array} a
+ * @param {Array} b
+ * @return {boolean} Returns true if both arrays have the same length and same
+ * elements.
+ */
+util.equalArray = function (a, b) {
+ if (a.length != b.length) return false;
+
+ for (var i = 1, len = a.length; i < len; i++) {
+ if (a[i] != b[i]) return false;
+ }
+
+ return true;
+};
+
/**
* Convert an object to another type
* @param {Boolean | Number | String | Date | Moment | Null | undefined} object
@@ -750,6 +772,22 @@ util.forEach = function forEach (object, callback) {
}
};
+/**
+ * Convert an object into an array: all objects properties are put into the
+ * array. The resulting array is unordered.
+ * @param {Object} object
+ * @param {Array} array
+ */
+util.toArray = function toArray(object) {
+ var array = [];
+
+ for (var prop in object) {
+ if (object.hasOwnProperty(prop)) array.push(object[prop]);
+ }
+
+ return array;
+}
+
/**
* Update a property in an object
* @param {Object} object
@@ -757,7 +795,7 @@ util.forEach = function forEach (object, callback) {
* @param {*} value
* @return {Boolean} changed
*/
-util.updateProperty = function updateProp (object, key, value) {
+util.updateProperty = function updateProperty (object, key, value) {
if (object[key] !== value) {
object[key] = value;
return true;
@@ -965,6 +1003,8 @@ util.option.asElement = function (value, defaultValue) {
util.GiveDec = function GiveDec(Hex) {
+ var Value;
+
if (Hex == "A")
Value = 10;
else if (Hex == "B")
@@ -978,12 +1018,15 @@ util.GiveDec = function GiveDec(Hex) {
else if (Hex == "F")
Value = 15;
else
- Value = eval(Hex)
+ Value = eval(Hex);
+
return Value;
};
util.GiveHex = function GiveHex(Dec) {
- if (Dec == 10)
+ var Value;
+
+ if(Dec == 10)
Value = "A";
else if (Dec == 11)
Value = "B";
@@ -997,6 +1040,7 @@ util.GiveHex = function GiveHex(Dec) {
Value = "F";
else
Value = "" + Dec;
+
return Value;
};
@@ -2864,21 +2908,19 @@ TimeStep.prototype.getLabelMajor = function(date) {
}
};
+// TODO: turn Stack into a Mixin?
+
/**
* @constructor Stack
* Stacks items on top of each other.
- * @param {ItemSet} itemset
* @param {Object} [options]
*/
-function Stack (itemset, options) {
- this.itemset = itemset;
-
+function Stack (options) {
this.options = options || {};
this.defaultOptions = {
order: function (a, b) {
- //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
- // Order: ranges over non-ranges, ranged ordered by width, and
- // lastly ordered by start.
+ // Order: ranges over non-ranges, ranged ordered by width,
+ // and non-ranges ordered by start.
if (a instanceof ItemRange) {
if (b instanceof ItemRange) {
var aInt = (a.data.end - a.data.start);
@@ -2899,141 +2941,122 @@ function Stack (itemset, options) {
}
},
margin: {
- item: 10
+ item: 10,
+ axis: 20
}
};
-
- this.ordered = []; // ordered items
}
/**
* Set options for the stack
* @param {Object} options Available options:
- * {ItemSet} itemset
- * {Number} margin
- * {function} order Stacking order
+ * {Number} [margin.item=10]
+ * {Number} [margin.axis=20]
+ * {function} [order] Stacking order
*/
Stack.prototype.setOptions = function setOptions (options) {
util.extend(this.options, options);
-
- // TODO: register on data changes at the connected itemset, and update the changed part only and immediately
};
/**
- * Stack the items such that they don't overlap. The items will have a minimal
- * distance equal to options.margin.item.
+ * Order an array with items using a predefined order function for items
+ * @param {Item[]} items
*/
-Stack.prototype.update = function update() {
- this._order();
- this._stack();
+Stack.prototype.order = function order(items) {
+ //order the items
+ var order = this.options.order || this.defaultOptions.order;
+ if (!(typeof order === 'function')) {
+ throw new Error('Option order must be a function');
+ }
+ items.sort(order);
};
/**
- * Order the items. If a custom order function has been provided via the options,
- * then this will be used.
- * @private
+ * Order items by their start data
+ * @param {Item[]} items
*/
-Stack.prototype._order = function _order () {
- var items = this.itemset.items;
- if (!items) {
- throw new Error('Cannot stack items: ItemSet does not contain items');
- }
-
- // TODO: store the sorted items, to have less work later on
- var ordered = [];
- var index = 0;
- // items is a map (no array)
- util.forEach(items, function (item) {
- if (item.visible) {
- ordered[index] = item;
- index++;
- }
+Stack.prototype.orderByStart = function orderByStart(items) {
+ items.sort(function (a, b) {
+ return a.data.start - b.data.start;
});
+};
- //if a customer stack order function exists, use it.
- var order = this.options.order || this.defaultOptions.order;
- if (!(typeof order === 'function')) {
- throw new Error('Option order must be a function');
- }
-
- ordered.sort(order);
+/**
+ * Order items by their end date. If they have no end date, their start date
+ * is used.
+ * @param {Item[]} items
+ */
+Stack.prototype.orderByEnd = function orderByEnd(items) {
+ items.sort(function (a, b) {
+ var aTime = ('end' in a.data) ? a.data.end : a.data.start,
+ bTime = ('end' in b.data) ? b.data.end : b.data.start;
- this.ordered = ordered;
+ return aTime - bTime;
+ });
};
/**
* Adjust vertical positions of the events such that they don't overlap each
* other.
+ * @param {Item[]} items All visible items
+ * @param {boolean} [force=false] If true, all items will be re-stacked.
+ * If false (default), only items having a
+ * top===null will be re-stacked
* @private
*/
-Stack.prototype._stack = function _stack () {
+Stack.prototype.stack = function stack (items, force) {
var i,
iMax,
- ordered = this.ordered,
options = this.options,
- orientation = options.orientation || this.defaultOptions.orientation,
- axisOnTop = (orientation == 'top'),
- margin;
+ marginItem,
+ marginAxis;
if (options.margin && options.margin.item !== undefined) {
- margin = options.margin.item;
+ marginItem = options.margin.item;
+ }
+ else {
+ marginItem = this.defaultOptions.margin.item
+ }
+ if (options.margin && options.margin.axis !== undefined) {
+ marginAxis = options.margin.axis;
}
else {
- margin = this.defaultOptions.margin.item
+ marginAxis = this.defaultOptions.margin.axis
}
- // calculate new, non-overlapping positions
- for (i = 0, iMax = ordered.length; i < iMax; i++) {
- var item = ordered[i];
- var collidingItem = null;
- do {
- // TODO: optimize checking for overlap. when there is a gap without items,
- // you only need to check for items from the next item on, not from zero
- collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
- if (collidingItem != null) {
- // There is a collision. Reposition the event above the colliding element
- if (axisOnTop) {
- item.top = collidingItem.top + collidingItem.height + margin;
- }
- else {
- item.top = collidingItem.top - item.height - margin;
- }
- }
- } while (collidingItem);
+ if (force) {
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ items[i].top = null;
+ }
}
-};
-/**
- * Check if the destiny position of given item overlaps with any
- * of the other items from index itemStart to itemEnd.
- * @param {Array} items Array with items
- * @param {int} itemIndex Number of the item to be checked for overlap
- * @param {int} itemStart First item to be checked.
- * @param {int} itemEnd Last item to be checked.
- * @return {Object | null} colliding item, or undefined when no collisions
- * @param {Number} margin A minimum required margin.
- * If margin is provided, the two items will be
- * marked colliding when they overlap or
- * when the margin between the two is smaller than
- * the requested margin.
- */
-Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
- itemStart, itemEnd, margin) {
- var collision = this.collision;
+ // calculate new, non-overlapping positions
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ var item = items[i];
+ if (item.top === null) {
+ // initialize top position
+ item.top = marginAxis;
+
+ do {
+ // TODO: optimize checking for overlap. when there is a gap without items,
+ // you only need to check for items from the next item on, not from zero
+ var collidingItem = null;
+ for (var j = 0, jj = items.length; j < jj; j++) {
+ var other = items[j];
+ if (other.top !== null && other !== item && this.collision(item, other, marginItem)) {
+ collidingItem = other;
+ break;
+ }
+ }
- // we loop from end to start, as we suppose that the chance of a
- // collision is larger for items at the end, so check these first.
- var a = items[itemIndex];
- for (var i = itemEnd; i >= itemStart; i--) {
- var b = items[i];
- if (collision(a, b, margin)) {
- if (i != itemIndex) {
- return b;
- }
+ if (collidingItem != null) {
+ // There is a collision. Reposition the event above the colliding element
+ item.top = collidingItem.top + collidingItem.height + marginItem;
+ }
+ } while (collidingItem);
}
}
-
- return null;
};
/**
@@ -3060,20 +3083,39 @@ Stack.prototype.collision = function collision (a, b, margin) {
* A Range controls a numeric range with a start and end value.
* The Range adjusts the range based on mouse events or programmatic changes,
* and triggers events when the range is changing or has been changed.
- * @param {Object} [options] See description at Range.setOptions
- * @extends Controller
+ * @param {RootPanel} root Root panel, used to subscribe to events
+ * @param {Panel} parent Parent panel, used to attach to the DOM
+ * @param {Object} [options] See description at Range.setOptions
*/
-function Range(options) {
+function Range(root, parent, options) {
this.id = util.randomUUID();
this.start = null; // Number
this.end = null; // Number
+ this.root = root;
+ this.parent = parent;
this.options = options || {};
+ // drag listeners for dragging
+ this.root.on('dragstart', this._onDragStart.bind(this));
+ this.root.on('drag', this._onDrag.bind(this));
+ this.root.on('dragend', this._onDragEnd.bind(this));
+
+ // ignore dragging when holding
+ this.root.on('hold', this._onHold.bind(this));
+
+ // mouse wheel for zooming
+ this.root.on('mousewheel', this._onMouseWheel.bind(this));
+ this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
+
+ // pinch to zoom
+ this.root.on('touch', this._onTouch.bind(this));
+ this.root.on('pinch', this._onPinch.bind(this));
+
this.setOptions(options);
}
-// extend the Range prototype with an event emitter mixin
+// turn Range into an event emitter
Emitter(Range.prototype);
/**
@@ -3106,59 +3148,6 @@ function validateDirection (direction) {
}
}
-/**
- * Add listeners for mouse and touch events to the component
- * @param {Controller} controller
- * @param {Component} component Should be a rootpanel
- * @param {String} event Available events: 'move', 'zoom'
- * @param {String} direction Available directions: 'horizontal', 'vertical'
- */
-Range.prototype.subscribe = function (controller, component, event, direction) {
- var me = this;
-
- if (event == 'move') {
- // drag start listener
- controller.on('dragstart', function (event) {
- me._onDragStart(event, component);
- });
-
- // drag listener
- controller.on('drag', function (event) {
- me._onDrag(event, component, direction);
- });
-
- // drag end listener
- controller.on('dragend', function (event) {
- me._onDragEnd(event, component);
- });
-
- // ignore dragging when holding
- controller.on('hold', function (event) {
- me._onHold();
- });
- }
- else if (event == 'zoom') {
- // mouse wheel
- function mousewheel (event) {
- me._onMouseWheel(event, component, direction);
- }
- controller.on('mousewheel', mousewheel);
- controller.on('DOMMouseScroll', mousewheel); // For FF
-
- // pinch
- controller.on('touch', function (event) {
- me._onTouch(event);
- });
- controller.on('pinch', function (event) {
- me._onPinch(event, component, direction);
- });
- }
- else {
- throw new TypeError('Unknown event "' + event + '". ' +
- 'Choose "move" or "zoom".');
- }
-};
-
/**
* Set a new start and end range
* @param {Number} [start]
@@ -3168,8 +3157,8 @@ Range.prototype.setRange = function(start, end) {
var changed = this._applyRange(start, end);
if (changed) {
var params = {
- start: this.start,
- end: this.end
+ start: new Date(this.start),
+ end: new Date(this.end)
};
this.emit('rangechange', params);
this.emit('rangechanged', params);
@@ -3337,10 +3326,9 @@ var touchParams = {};
/**
* Start dragging horizontally or vertically
* @param {Event} event
- * @param {Object} component
* @private
*/
-Range.prototype._onDragStart = function(event, component) {
+Range.prototype._onDragStart = function(event) {
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.ignore) return;
@@ -3350,7 +3338,7 @@ Range.prototype._onDragStart = function(event, component) {
touchParams.start = this.start;
touchParams.end = this.end;
- var frame = component.frame;
+ var frame = this.parent.frame;
if (frame) {
frame.style.cursor = 'move';
}
@@ -3359,11 +3347,10 @@ Range.prototype._onDragStart = function(event, component) {
/**
* Perform dragging operating.
* @param {Event} event
- * @param {Component} component
- * @param {String} direction 'horizontal' or 'vertical'
* @private
*/
-Range.prototype._onDrag = function (event, component, direction) {
+Range.prototype._onDrag = function (event) {
+ var direction = this.options.direction;
validateDirection(direction);
// TODO: reckon with option movable
@@ -3375,38 +3362,37 @@ Range.prototype._onDrag = function (event, component, direction) {
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
interval = (touchParams.end - touchParams.start),
- width = (direction == 'horizontal') ? component.width : component.height,
+ width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
diffRange = -delta / width * interval;
this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
this.emit('rangechange', {
- start: this.start,
- end: this.end
+ start: new Date(this.start),
+ end: new Date(this.end)
});
};
/**
* Stop dragging operating.
* @param {event} event
- * @param {Component} component
* @private
*/
-Range.prototype._onDragEnd = function (event, component) {
+Range.prototype._onDragEnd = function (event) {
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.ignore) return;
// TODO: reckon with option movable
- if (component.frame) {
- component.frame.style.cursor = 'auto';
+ if (this.parent.frame) {
+ this.parent.frame.style.cursor = 'auto';
}
// fire a rangechanged event
this.emit('rangechanged', {
- start: this.start,
- end: this.end
+ start: new Date(this.start),
+ end: new Date(this.end)
});
};
@@ -3414,13 +3400,9 @@ Range.prototype._onDragEnd = function (event, component) {
* Event handler for mouse wheel event, used to zoom
* Code from http://adomas.org/javascript-mouse-wheel/
* @param {Event} event
- * @param {Component} component
- * @param {String} direction 'horizontal' or 'vertical'
* @private
*/
-Range.prototype._onMouseWheel = function(event, component, direction) {
- validateDirection(direction);
-
+Range.prototype._onMouseWheel = function(event) {
// TODO: reckon with option zoomable
// retrieve delta
@@ -3451,8 +3433,8 @@ Range.prototype._onMouseWheel = function(event, component, direction) {
// calculate center, the date to zoom around
var gesture = util.fakeGesture(this, event),
- pointer = getPointer(gesture.center, component.frame),
- pointerDate = this._pointerToDate(component, direction, pointer);
+ pointer = getPointer(gesture.center, this.parent.frame),
+ pointerDate = this._pointerToDate(pointer);
this.zoom(scale, pointerDate);
}
@@ -3491,24 +3473,23 @@ Range.prototype._onHold = function () {
/**
* Handle pinch event
* @param {Event} event
- * @param {Component} component
- * @param {String} direction 'horizontal' or 'vertical'
* @private
*/
-Range.prototype._onPinch = function (event, component, direction) {
+Range.prototype._onPinch = function (event) {
+ var direction = this.options.direction;
touchParams.ignore = true;
// TODO: reckon with option zoomable
if (event.gesture.touches.length > 1) {
if (!touchParams.center) {
- touchParams.center = getPointer(event.gesture.center, component.frame);
+ touchParams.center = getPointer(event.gesture.center, this.parent.frame);
}
var scale = 1 / event.gesture.scale,
- initDate = this._pointerToDate(component, direction, touchParams.center),
- center = getPointer(event.gesture.center, component.frame),
- date = this._pointerToDate(component, direction, center),
+ initDate = this._pointerToDate(touchParams.center),
+ center = getPointer(event.gesture.center, this.parent.frame),
+ date = this._pointerToDate(this.parent, center),
delta = date - initDate; // TODO: utilize delta
// calculate new start and end
@@ -3522,21 +3503,23 @@ Range.prototype._onPinch = function (event, component, direction) {
/**
* Helper function to calculate the center date for zooming
- * @param {Component} component
* @param {{x: Number, y: Number}} pointer
- * @param {String} direction 'horizontal' or 'vertical'
* @return {number} date
* @private
*/
-Range.prototype._pointerToDate = function (component, direction, pointer) {
+Range.prototype._pointerToDate = function (pointer) {
var conversion;
+ var direction = this.options.direction;
+
+ validateDirection(direction);
+
if (direction == 'horizontal') {
- var width = component.width;
+ var width = this.parent.width;
conversion = this.conversion(width);
return pointer.x / conversion.scale + conversion.offset;
}
else {
- var height = component.height;
+ var height = this.parent.height;
conversion = this.conversion(height);
return pointer.y / conversion.scale + conversion.offset;
}
@@ -3615,207 +3598,24 @@ Range.prototype.moveTo = function(moveTo) {
this.setRange(newStart, newEnd);
};
-/**
- * @constructor Controller
- *
- * A Controller controls the reflows and repaints of all components,
- * and is used as an event bus for all components.
- */
-function Controller () {
- var me = this;
-
- this.id = util.randomUUID();
- this.components = {};
-
- /**
- * Listen for a 'request-reflow' event. The controller will schedule a reflow
- * @param {Boolean} [force] If true, an immediate reflow is forced. Default
- * is false.
- */
- var reflowTimer = null;
- this.on('request-reflow', function requestReflow(force) {
- if (force) {
- me.reflow();
- }
- else {
- if (!reflowTimer) {
- reflowTimer = setTimeout(function () {
- reflowTimer = null;
- me.reflow();
- }, 0);
- }
- }
- });
-
- /**
- * Request a repaint. The controller will schedule a repaint
- * @param {Boolean} [force] If true, an immediate repaint is forced. Default
- * is false.
- */
- var repaintTimer = null;
- this.on('request-repaint', function requestRepaint(force) {
- if (force) {
- me.repaint();
- }
- else {
- if (!repaintTimer) {
- repaintTimer = setTimeout(function () {
- repaintTimer = null;
- me.repaint();
- }, 0);
- }
- }
- });
-}
-
-// Extend controller with Emitter mixin
-Emitter(Controller.prototype);
-
-/**
- * Add a component to the controller
- * @param {Component} component
- */
-Controller.prototype.add = function add(component) {
- // validate the component
- if (component.id == undefined) {
- throw new Error('Component has no field id');
- }
- if (!(component instanceof Component) && !(component instanceof Controller)) {
- throw new TypeError('Component must be an instance of ' +
- 'prototype Component or Controller');
- }
-
- // add the component
- component.setController(this);
- this.components[component.id] = component;
-};
-
-/**
- * Remove a component from the controller
- * @param {Component | String} component
- */
-Controller.prototype.remove = function remove(component) {
- var id;
- for (id in this.components) {
- if (this.components.hasOwnProperty(id)) {
- if (id == component || this.components[id] === component) {
- break;
- }
- }
- }
-
- if (id) {
- // unregister the controller (gives the component the ability to unregister
- // event listeners and clean up other stuff)
- this.components[id].setController(null);
-
- delete this.components[id];
- }
-};
-
-/**
- * Repaint all components
- */
-Controller.prototype.repaint = function repaint() {
- var changed = false;
-
- // cancel any running repaint request
- if (this.repaintTimer) {
- clearTimeout(this.repaintTimer);
- this.repaintTimer = undefined;
- }
-
- var done = {};
-
- function repaint(component, id) {
- if (!(id in done)) {
- // first repaint the components on which this component is dependent
- if (component.depends) {
- component.depends.forEach(function (dep) {
- repaint(dep, dep.id);
- });
- }
- if (component.parent) {
- repaint(component.parent, component.parent.id);
- }
-
- // repaint the component itself and mark as done
- changed = component.repaint() || changed;
- done[id] = true;
- }
- }
-
- util.forEach(this.components, repaint);
-
- this.emit('repaint');
-
- // immediately reflow when needed
- if (changed) {
- this.reflow();
- }
- // TODO: limit the number of nested reflows/repaints, prevent loop
-};
-
-/**
- * Reflow all components
- */
-Controller.prototype.reflow = function reflow() {
- var resized = false;
-
- // cancel any running repaint request
- if (this.reflowTimer) {
- clearTimeout(this.reflowTimer);
- this.reflowTimer = undefined;
- }
-
- var done = {};
-
- function reflow(component, id) {
- if (!(id in done)) {
- // first reflow the components on which this component is dependent
- if (component.depends) {
- component.depends.forEach(function (dep) {
- reflow(dep, dep.id);
- });
- }
- if (component.parent) {
- reflow(component.parent, component.parent.id);
- }
-
- // reflow the component itself and mark as done
- resized = component.reflow() || resized;
- done[id] = true;
- }
- }
-
- util.forEach(this.components, reflow);
-
- this.emit('reflow');
-
- // immediately repaint when needed
- if (resized) {
- this.repaint();
- }
- // TODO: limit the number of nested reflows/repaints, prevent loop
-};
-
/**
* Prototype for visual components
*/
function Component () {
this.id = null;
this.parent = null;
- this.depends = null;
- this.controller = null;
+ this.childs = null;
this.options = null;
- this.frame = null; // main DOM element
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
}
+// Turn the Component into an event emitter
+Emitter(Component.prototype);
+
/**
* Set parameters for the frame. Parameters will be merged in current parameter
* set.
@@ -3830,10 +3630,7 @@ Component.prototype.setOptions = function setOptions(options) {
if (options) {
util.extend(this.options, options);
- if (this.controller) {
- this.requestRepaint();
- this.requestReflow();
- }
+ this.repaint();
}
};
@@ -3855,46 +3652,18 @@ Component.prototype.getOption = function getOption(name) {
return value;
};
-/**
- * Set controller for this component, or remove current controller by passing
- * null as parameter value.
- * @param {Controller | null} controller
- */
-Component.prototype.setController = function setController (controller) {
- this.controller = controller || null;
-};
-
-/**
- * Get controller of this component
- * @return {Controller} controller
- */
-Component.prototype.getController = function getController () {
- return this.controller;
-};
-
-/**
- * Get the container element of the component, which can be used by a child to
- * add its own widgets. Not all components do have a container for childs, in
- * that case null is returned.
- * @returns {HTMLElement | null} container
- */
-// TODO: get rid of the getContainer and getFrame methods, provide these via the options
-Component.prototype.getContainer = function getContainer() {
- // should be implemented by the component
- return null;
-};
-
/**
* Get the frame element of the component, the outer HTML DOM element.
* @returns {HTMLElement | null} frame
*/
Component.prototype.getFrame = function getFrame() {
- return this.frame;
+ // should be implemented by the component
+ return null;
};
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component is resized
*/
Component.prototype.repaint = function repaint() {
// should be implemented by the component
@@ -3902,73 +3671,22 @@ Component.prototype.repaint = function repaint() {
};
/**
- * Reflow the component
- * @return {Boolean} resized
- */
-Component.prototype.reflow = function reflow() {
- // should be implemented by the component
- return false;
-};
-
-/**
- * Hide the component from the DOM
- * @return {Boolean} changed
- */
-Component.prototype.hide = function hide() {
- if (this.frame && this.frame.parentNode) {
- this.frame.parentNode.removeChild(this.frame);
- return true;
- }
- else {
- return false;
- }
-};
-
-/**
- * Show the component in the DOM (when not already visible).
- * A repaint will be executed when the component is not visible
- * @return {Boolean} changed
+ * Test whether the component is resized since the last time _isResized() was
+ * called.
+ * @return {Boolean} Returns true if the component is resized
+ * @private
*/
-Component.prototype.show = function show() {
- if (!this.frame || !this.frame.parentNode) {
- return this.repaint();
- }
- else {
- return false;
- }
-};
+Component.prototype._isResized = function _isResized() {
+ var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
-/**
- * Request a repaint. The controller will schedule a repaint
- */
-Component.prototype.requestRepaint = function requestRepaint() {
- if (this.controller) {
- this.controller.emit('request-repaint');
- }
- else {
- throw new Error('Cannot request a repaint: no controller configured');
- // TODO: just do a repaint when no parent is configured?
- }
-};
+ this._previousWidth = this.width;
+ this._previousHeight = this.height;
-/**
- * Request a reflow. The controller will schedule a reflow
- */
-Component.prototype.requestReflow = function requestReflow() {
- if (this.controller) {
- this.controller.emit('request-reflow');
- }
- else {
- throw new Error('Cannot request a reflow: no controller configured');
- // TODO: just do a reflow when no parent is configured?
- }
+ return resized;
};
/**
* A panel can contain components
- * @param {Component} [parent]
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
* @param {Object} [options] Available parameters:
* {String | Number | function} [left]
* {String | Number | function} [top]
@@ -3978,12 +3696,15 @@ Component.prototype.requestReflow = function requestReflow() {
* @constructor Panel
* @extends Component
*/
-function Panel(parent, depends, options) {
+function Panel(options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
+ this.parent = null;
+ this.childs = [];
this.options = options || {};
+
+ // create frame
+ this.frame = document.createElement('div');
}
Panel.prototype = new Component();
@@ -4000,81 +3721,139 @@ Panel.prototype = new Component();
Panel.prototype.setOptions = Component.prototype.setOptions;
/**
- * Get the container element of the panel, which can be used by a child to
- * add its own widgets.
- * @returns {HTMLElement} container
+ * Get the outer frame of the panel
+ * @returns {HTMLElement} frame
*/
-Panel.prototype.getContainer = function () {
+Panel.prototype.getFrame = function () {
return this.frame;
};
/**
- * Repaint the component
- * @return {Boolean} changed
+ * Append a child to the panel
+ * @param {Component} child
*/
-Panel.prototype.repaint = function () {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
- options = this.options,
- frame = this.frame;
- if (!frame) {
- frame = document.createElement('div');
- frame.className = 'vpanel';
+Panel.prototype.appendChild = function (child) {
+ this.childs.push(child);
+ child.parent = this;
+
+ // attach to the DOM
+ var frame = child.getFrame();
+ if (frame) {
+ if (frame.parentNode) {
+ frame.parentNode.removeChild(frame);
+ }
+ this.frame.appendChild(frame);
+ }
+};
+
+/**
+ * Insert a child to the panel
+ * @param {Component} child
+ * @param {Component} beforeChild
+ */
+Panel.prototype.insertBefore = function (child, beforeChild) {
+ var index = this.childs.indexOf(beforeChild);
+ if (index != -1) {
+ this.childs.splice(index, 0, child);
+ child.parent = this;
+
+ // attach to the DOM
+ var frame = child.getFrame();
+ if (frame) {
+ if (frame.parentNode) {
+ frame.parentNode.removeChild(frame);
+ }
- var className = options.className;
- if (className) {
- if (typeof className == 'function') {
- util.addClassName(frame, String(className()));
+ var beforeFrame = beforeChild.getFrame();
+ if (beforeFrame) {
+ this.frame.insertBefore(frame, beforeFrame);
}
else {
- util.addClassName(frame, String(className));
+ this.frame.appendChild(frame);
}
}
-
- this.frame = frame;
- changed += 1;
}
- if (!frame.parentNode) {
- if (!this.parent) {
- throw new Error('Cannot repaint panel: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint panel: parent has no container element');
+};
+
+/**
+ * Remove a child from the panel
+ * @param {Component} child
+ */
+Panel.prototype.removeChild = function (child) {
+ var index = this.childs.indexOf(child);
+ if (index != -1) {
+ this.childs.splice(index, 1);
+ child.parent = null;
+
+ // remove from the DOM
+ var frame = child.getFrame();
+ if (frame && frame.parentNode) {
+ this.frame.removeChild(frame);
}
- parentContainer.appendChild(frame);
- changed += 1;
}
+};
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
- changed += update(frame.style, 'height', asSize(options.height, '100%'));
-
- return (changed > 0);
+/**
+ * Test whether the panel contains given child
+ * @param {Component} child
+ */
+Panel.prototype.hasChild = function (child) {
+ var index = this.childs.indexOf(child);
+ return (index != -1);
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Repaint the component
+ * @return {boolean} Returns true if the component was resized since previous repaint
*/
-Panel.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame;
+Panel.prototype.repaint = function () {
+ var asString = util.option.asString,
+ options = this.options,
+ frame = this.getFrame();
- if (frame) {
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
- changed += update(this, 'height', frame.offsetHeight);
- }
- else {
- changed += 1;
+ // update className
+ frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
+
+ // repaint the child components
+ var childsResized = this._repaintChilds();
+
+ // update frame size
+ this._updateSize();
+
+ return this._isResized() || childsResized;
+};
+
+/**
+ * Repaint all childs of the panel
+ * @return {boolean} Returns true if the component is resized
+ * @private
+ */
+Panel.prototype._repaintChilds = function () {
+ var resized = false;
+ for (var i = 0, ii = this.childs.length; i < ii; i++) {
+ resized = this.childs[i].repaint() || resized;
}
+ return resized;
+};
+
+/**
+ * Apply the size from options to the panel, and recalculate it's actual size.
+ * @private
+ */
+Panel.prototype._updateSize = function () {
+ // apply size
+ this.frame.style.top = util.option.asSize(this.options.top);
+ this.frame.style.bottom = util.option.asSize(this.options.bottom);
+ this.frame.style.left = util.option.asSize(this.options.left);
+ this.frame.style.right = util.option.asSize(this.options.right);
+ this.frame.style.width = util.option.asSize(this.options.width, '100%');
+ this.frame.style.height = util.option.asSize(this.options.height, '');
- return (changed > 0);
+ // get actual size
+ this.top = this.frame.offsetTop;
+ this.left = this.frame.offsetLeft;
+ this.width = this.frame.offsetWidth;
+ this.height = this.frame.offsetHeight;
};
/**
@@ -4089,32 +3868,53 @@ function RootPanel(container, options) {
this.id = util.randomUUID();
this.container = container;
- // create functions to be used as DOM event listeners
- var me = this;
- this.hammer = null;
+ this.options = options || {};
+ this.defaultOptions = {
+ autoResize: true
+ };
+
+ // create the HTML DOM
+ this._create();
+
+ // attach the root panel to the provided container
+ if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
+ this.container.appendChild(this.getFrame());
+
+
+ this._initWatch();
+}
+
+RootPanel.prototype = new Panel();
+
+/**
+ * Create the HTML DOM for the root panel
+ */
+RootPanel.prototype._create = function _create() {
+ // create frame
+ this.frame = document.createElement('div');
+
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.frame, {
+ prevent_default: true
+ });
+ this.listeners = {};
- // create listeners for all interesting events, these events will be emitted
- // via the controller
+ var me = this;
var events = [
'touch', 'pinch', 'tap', 'doubletap', 'hold',
'dragstart', 'drag', 'dragend',
'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
];
- this.listeners = {};
events.forEach(function (event) {
- me.listeners[event] = function () {
+ var listener = function () {
var args = [event].concat(Array.prototype.slice.call(arguments, 0));
- me.controller.emit.apply(me.controller, args);
+ me.emit.apply(me, args);
};
+ me.hammer.on(event, listener);
+ me.listeners[event] = listener;
});
-
- this.options = options || {};
- this.defaultOptions = {
- autoResize: true
- };
-}
-
-RootPanel.prototype = new Panel();
+};
/**
* Set options. Will extend the current options.
@@ -4126,80 +3926,53 @@ RootPanel.prototype = new Panel();
* {String | Number | function} [height]
* {Boolean | function} [autoResize]
*/
-RootPanel.prototype.setOptions = Component.prototype.setOptions;
-
-/**
- * Repaint the component
- * @return {Boolean} changed
- */
-RootPanel.prototype.repaint = function () {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
- options = this.options,
- frame = this.frame;
-
- if (!frame) {
- frame = document.createElement('div');
-
- this.frame = frame;
-
- this._registerListeners();
+RootPanel.prototype.setOptions = function setOptions(options) {
+ if (options) {
+ util.extend(this.options, options);
- changed += 1;
- }
- if (!frame.parentNode) {
- if (!this.container) {
- throw new Error('Cannot repaint root panel: no container attached');
- }
- this.container.appendChild(frame);
- changed += 1;
- }
+ this.repaint();
- frame.className = 'vis timeline rootpanel ' + options.orientation +
- (options.editable ? ' editable' : '');
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
+ this._initWatch();
}
+};
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
- changed += update(frame.style, 'height', asSize(options.height, '100%'));
-
- this._updateWatch();
-
- return (changed > 0);
+/**
+ * Get the frame of the root panel
+ */
+RootPanel.prototype.getFrame = function getFrame() {
+ return this.frame;
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Repaint the root panel
*/
-RootPanel.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame;
+RootPanel.prototype.repaint = function repaint() {
+ // update class name
+ var options = this.options;
+ var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
+ if (options.className) className += ' ' + util.option.asString(className);
+ this.frame.className = className;
- if (frame) {
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
- changed += update(this, 'height', frame.offsetHeight);
- }
- else {
- changed += 1;
- }
+ // repaint the child components
+ var childsResized = this._repaintChilds();
+
+ // update frame size
+ this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
+ this._updateSize();
- return (changed > 0);
+ // if the root panel or any of its childs is resized, repaint again,
+ // as other components may need to be resized accordingly
+ var resized = this._isResized() || childsResized;
+ if (resized) {
+ setTimeout(this.repaint.bind(this), 0);
+ }
};
/**
- * Update watching for resize, depending on the current option
+ * Initialize watching when option autoResize is true
* @private
*/
-RootPanel.prototype._updateWatch = function () {
+RootPanel.prototype._initWatch = function _initWatch() {
var autoResize = this.getOption('autoResize');
if (autoResize) {
this._watch();
@@ -4214,12 +3987,12 @@ RootPanel.prototype._updateWatch = function () {
* automatically redraw itself.
* @private
*/
-RootPanel.prototype._watch = function () {
+RootPanel.prototype._watch = function _watch() {
var me = this;
this._unwatch();
- var checkSize = function () {
+ var checkSize = function checkSize() {
var autoResize = me.getOption('autoResize');
if (!autoResize) {
// stop watching when the option autoResize is changed to false
@@ -4229,9 +4002,12 @@ RootPanel.prototype._watch = function () {
if (me.frame) {
// check whether the frame is resized
- if ((me.frame.clientWidth != me.width) ||
- (me.frame.clientHeight != me.height)) {
- me.requestReflow();
+ if ((me.frame.clientWidth != me.lastWidth) ||
+ (me.frame.clientHeight != me.lastHeight)) {
+ me.lastWidth = me.frame.clientWidth;
+ me.lastHeight = me.frame.clientHeight;
+ me.repaint();
+ // TODO: emit a resize event instead?
}
}
};
@@ -4246,7 +4022,7 @@ RootPanel.prototype._watch = function () {
* Stop watching for a resize of the frame.
* @private
*/
-RootPanel.prototype._unwatch = function () {
+RootPanel.prototype._unwatch = function _unwatch() {
if (this.watchTimer) {
clearInterval(this.watchTimer);
this.watchTimer = undefined;
@@ -4255,70 +4031,15 @@ RootPanel.prototype._unwatch = function () {
// TODO: remove event listener on window.resize
};
-/**
- * Set controller for this component, or remove current controller by passing
- * null as parameter value.
- * @param {Controller | null} controller
- */
-RootPanel.prototype.setController = function setController (controller) {
- this.controller = controller || null;
-
- if (this.controller) {
- this._registerListeners();
- }
- else {
- this._unregisterListeners();
- }
-};
-
-/**
- * Register event emitters emitted by the rootpanel
- * @private
- */
-RootPanel.prototype._registerListeners = function () {
- if (this.frame && this.controller && !this.hammer) {
- this.hammer = Hammer(this.frame, {
- prevent_default: true
- });
-
- for (var event in this.listeners) {
- if (this.listeners.hasOwnProperty(event)) {
- this.hammer.on(event, this.listeners[event]);
- }
- }
- }
-};
-
-/**
- * Unregister event emitters from the rootpanel
- * @private
- */
-RootPanel.prototype._unregisterListeners = function () {
- if (this.hammer) {
- for (var event in this.listeners) {
- if (this.listeners.hasOwnProperty(event)) {
- this.hammer.off(event, this.listeners[event]);
- }
- }
-
- this.hammer = null;
- }
-};
-
/**
* 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) {
+function TimeAxis (options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
this.dom = {
majorLines: [],
@@ -4349,8 +4070,10 @@ function TimeAxis (parent, depends, options) {
showMajorLabels: true
};
- this.conversion = null;
this.range = null;
+
+ // create the HTML DOM
+ this._create();
}
TimeAxis.prototype = new Component();
@@ -4358,6 +4081,13 @@ TimeAxis.prototype = new Component();
// TODO: comment options
TimeAxis.prototype.setOptions = Component.prototype.setOptions;
+/**
+ * Create the HTML DOM for the TimeAxis
+ */
+TimeAxis.prototype._create = function _create() {
+ this.frame = document.createElement('div');
+};
+
/**
* Set a range (start and end)
* @param {Range | Object} range A Range or an object containing start and end.
@@ -4371,126 +4101,70 @@ TimeAxis.prototype.setRange = function (range) {
};
/**
- * Convert a position on screen (pixels) to a datetime
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
- */
-TimeAxis.prototype.toTime = function(x) {
- var conversion = this.conversion;
- return new Date(x / conversion.scale + conversion.offset);
-};
-
-/**
- * Convert a datetime (Date object) into a position on the screen
- * @param {Date} time A date
- * @return {int} x The position on the screen in pixels which corresponds
- * with the given date.
- * @private
+ * Get the outer frame of the time axis
+ * @return {HTMLElement} frame
*/
-TimeAxis.prototype.toScreen = function(time) {
- var conversion = this.conversion;
- return (time.valueOf() - conversion.offset) * conversion.scale;
+TimeAxis.prototype.getFrame = function getFrame() {
+ return this.frame;
};
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component is resized
*/
TimeAxis.prototype.repaint = function () {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
+ var asSize = util.option.asSize,
options = this.options,
- orientation = this.getOption('orientation'),
props = this.props,
- step = this.step;
-
- var frame = this.frame;
- if (!frame) {
- frame = document.createElement('div');
- this.frame = frame;
- changed += 1;
- }
- frame.className = 'axis';
- // TODO: custom className?
-
- if (!frame.parentNode) {
- if (!this.parent) {
- throw new Error('Cannot repaint time axis: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint time axis: parent has no container element');
- }
- parentContainer.appendChild(frame);
+ frame = this.frame;
- changed += 1;
- }
+ // update classname
+ frame.className = 'timeaxis'; // TODO: add className from options if defined
var parent = frame.parentNode;
if (parent) {
- var beforeChild = frame.nextSibling;
- parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
-
- var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
- (this.props.parentHeight - this.height) + 'px' :
- '0px';
- changed += update(frame.style, 'top', asSize(options.top, defaultTop));
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+ // calculate character width and height
+ this._calculateCharSize();
- // 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);
- }
+ // TODO: recalculate sizes only needed when parent is resized or options is changed
+ var orientation = this.getOption('orientation'),
+ showMinorLabels = this.getOption('showMinorLabels'),
+ showMajorLabels = this.getOption('showMajorLabels');
- step.next();
- }
+ // determine the width and height of the elemens for the axis
+ var parentHeight = this.parent.height;
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+ this.height = props.minorLabelHeight + props.majorLabelHeight;
+ this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
- // create a major label on the left when needed
- if (this.getOption('showMajorLabels')) {
- var leftTime = this.toTime(0),
- leftText = step.getLabelMajor(leftTime),
- widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
+ props.minorLineHeight = parentHeight + props.minorLabelHeight;
+ props.minorLineWidth = 1; // TODO: really calculate width
+ props.majorLineHeight = parentHeight + this.height;
+ props.majorLineWidth = 1; // TODO: really calculate width
- if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
- this._repaintMajorText(0, leftText);
- }
- }
+ // take frame offline while updating (is almost twice as fast)
+ var beforeChild = frame.nextSibling;
+ parent.removeChild(frame);
- this._repaintEnd();
+ // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
+ if (orientation == 'top') {
+ frame.style.top = '0';
+ frame.style.left = '0';
+ frame.style.bottom = '';
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = this.height + 'px';
+ }
+ else { // bottom
+ frame.style.top = '';
+ frame.style.bottom = '0';
+ frame.style.left = '0';
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = this.height + 'px';
}
+ this._repaintLabels();
+
this._repaintLine();
// put frame online again
@@ -4502,34 +4176,80 @@ TimeAxis.prototype.repaint = function () {
}
}
- return (changed > 0);
+ return this._isResized();
};
/**
- * Start a repaint. Move all DOM elements to a redundant list, where they
- * can be picked for re-use, or can be cleaned up in the end
+ * Repaint major and minor text labels and vertical grid lines
* @private
*/
-TimeAxis.prototype._repaintStart = function () {
- var dom = this.dom,
- redundant = dom.redundant;
+TimeAxis.prototype._repaintLabels = function () {
+ var orientation = this.getOption('orientation');
- redundant.majorLines = dom.majorLines;
- redundant.majorTexts = dom.majorTexts;
- redundant.minorLines = dom.minorLines;
- redundant.minorTexts = dom.minorTexts;
+ // calculate range and step
+ var start = util.convert(this.range.start, 'Number'),
+ end = util.convert(this.range.end, 'Number'),
+ minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 5).valueOf()
+ -this.options.toTime(0).valueOf();
+ var step = new TimeStep(new Date(start), new Date(end), minimumStep);
+ this.step = step;
+ // Move all DOM elements to a "redundant" list, where they
+ // can be picked for re-use, and clear the lists with lines and texts.
+ // At the end of the function _repaintLabels, left over elements will be cleaned up
+ var dom = this.dom;
+ dom.redundant.majorLines = dom.majorLines;
+ dom.redundant.majorTexts = dom.majorTexts;
+ dom.redundant.minorLines = dom.minorLines;
+ dom.redundant.minorTexts = dom.minorTexts;
dom.majorLines = [];
dom.majorTexts = [];
dom.minorLines = [];
dom.minorTexts = [];
-};
-/**
- * End a repaint. Cleanup leftover DOM elements in the redundant list
- * @private
- */
-TimeAxis.prototype._repaintEnd = function () {
+ step.first();
+ var xFirstMajorLabel = undefined;
+ var max = 0;
+ while (step.hasNext() && max < 1000) {
+ max++;
+ var cur = step.getCurrent(),
+ x = this.options.toScreen(cur),
+ isMajor = step.isMajor();
+
+ // TODO: lines must have a width, such that we can create css backgrounds
+
+ if (this.getOption('showMinorLabels')) {
+ this._repaintMinorText(x, step.getLabelMinor(), orientation);
+ }
+
+ if (isMajor && this.getOption('showMajorLabels')) {
+ if (x > 0) {
+ if (xFirstMajorLabel == undefined) {
+ xFirstMajorLabel = x;
+ }
+ this._repaintMajorText(x, step.getLabelMajor(), orientation);
+ }
+ this._repaintMajorLine(x, orientation);
+ }
+ else {
+ this._repaintMinorLine(x, orientation);
+ }
+
+ step.next();
+ }
+
+ // create a major label on the left when needed
+ if (this.getOption('showMajorLabels')) {
+ var leftTime = this.options.toTime(0),
+ leftText = step.getLabelMajor(leftTime),
+ widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
+
+ if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
+ this._repaintMajorText(0, leftText, orientation);
+ }
+ }
+
+ // Cleanup leftover DOM elements from the redundant list
util.forEach(this.dom.redundant, function (arr) {
while (arr.length) {
var elem = arr.pop();
@@ -4540,14 +4260,14 @@ TimeAxis.prototype._repaintEnd = function () {
});
};
-
/**
* Create a minor label for the axis at position x
* @param {Number} x
* @param {String} text
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMinorText = function (x, text) {
+TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
// reuse redundant label
var label = this.dom.redundant.minorTexts.shift();
@@ -4562,8 +4282,16 @@ TimeAxis.prototype._repaintMinorText = function (x, text) {
this.dom.minorTexts.push(label);
label.childNodes[0].nodeValue = text;
+
+ if (orientation == 'top') {
+ label.style.top = this.props.majorLabelHeight + 'px';
+ label.style.bottom = '';
+ }
+ else {
+ label.style.top = '';
+ label.style.bottom = this.props.majorLabelHeight + 'px';
+ }
label.style.left = x + 'px';
- label.style.top = this.props.minorLabelTop + 'px';
//label.title = title; // TODO: this is a heavy operation
};
@@ -4571,9 +4299,10 @@ TimeAxis.prototype._repaintMinorText = function (x, text) {
* Create a Major label for the axis at position x
* @param {Number} x
* @param {String} text
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMajorText = function (x, text) {
+TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
// reuse redundant label
var label = this.dom.redundant.majorTexts.shift();
@@ -4588,17 +4317,26 @@ TimeAxis.prototype._repaintMajorText = function (x, text) {
this.dom.majorTexts.push(label);
label.childNodes[0].nodeValue = text;
- label.style.top = this.props.majorLabelTop + 'px';
- label.style.left = x + 'px';
//label.title = title; // TODO: this is a heavy operation
+
+ if (orientation == 'top') {
+ label.style.top = '0px';
+ label.style.bottom = '';
+ }
+ else {
+ label.style.top = '';
+ label.style.bottom = '0px';
+ }
+ label.style.left = x + 'px';
};
/**
* Create a minor line for the axis at position x
* @param {Number} x
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMinorLine = function (x) {
+TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
// reuse redundant line
var line = this.dom.redundant.minorLines.shift();
@@ -4611,7 +4349,14 @@ TimeAxis.prototype._repaintMinorLine = function (x) {
this.dom.minorLines.push(line);
var props = this.props;
- line.style.top = props.minorLineTop + 'px';
+ if (orientation == 'top') {
+ line.style.top = this.props.majorLabelHeight + 'px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = this.props.majorLabelHeight + 'px';
+ }
line.style.height = props.minorLineHeight + 'px';
line.style.left = (x - props.minorLineWidth / 2) + 'px';
};
@@ -4619,9 +4364,10 @@ TimeAxis.prototype._repaintMinorLine = function (x) {
/**
* Create a Major line for the axis at position x
* @param {Number} x
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMajorLine = function (x) {
+TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
// reuse redundant line
var line = this.dom.redundant.majorLines.shift();
@@ -4634,7 +4380,14 @@ TimeAxis.prototype._repaintMajorLine = function (x) {
this.dom.majorLines.push(line);
var props = this.props;
- line.style.top = props.majorLineTop + 'px';
+ if (orientation == 'top') {
+ line.style.top = '0px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = '0px';
+ }
line.style.left = (x - props.majorLineWidth / 2) + 'px';
line.style.height = props.majorLineHeight + 'px';
};
@@ -4647,7 +4400,7 @@ TimeAxis.prototype._repaintMajorLine = function (x) {
TimeAxis.prototype._repaintLine = function() {
var line = this.dom.line,
frame = this.frame,
- options = this.options;
+ orientation = this.getOption('orientation');
// line before all axis elements
if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
@@ -4664,297 +4417,174 @@ TimeAxis.prototype._repaintLine = function() {
this.dom.line = line;
}
- line.style.top = this.props.lineTop + 'px';
+ if (orientation == 'top') {
+ line.style.top = this.height + 'px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = this.height + 'px';
+ }
}
else {
- if (line && line.parentElement) {
- frame.removeChild(line.line);
+ if (line && line.parentNode) {
+ line.parentNode.removeChild(line);
delete this.dom.line;
}
}
};
/**
- * Create characters used to determine the size of text on the axis
+ * Determine the size of text on the axis (both major and minor axis).
+ * The size is calculated only once and then cached in this.props.
* @private
*/
-TimeAxis.prototype._repaintMeasureChars = function () {
- // calculate the width and height of a single character
- // this is used to calculate the step size, and also the positioning of the
- // axis
- var dom = this.dom,
- text;
-
- if (!dom.measureCharMinor) {
- text = document.createTextNode('0');
+TimeAxis.prototype._calculateCharSize = function () {
+ // determine the char width and height on the minor axis
+ if (!('minorCharHeight' in this.props)) {
+ var textMinor = document.createTextNode('0');
var measureCharMinor = document.createElement('DIV');
measureCharMinor.className = 'text minor measure';
- measureCharMinor.appendChild(text);
+ measureCharMinor.appendChild(textMinor);
this.frame.appendChild(measureCharMinor);
- dom.measureCharMinor = measureCharMinor;
+ this.props.minorCharHeight = measureCharMinor.clientHeight;
+ this.props.minorCharWidth = measureCharMinor.clientWidth;
+
+ this.frame.removeChild(measureCharMinor);
}
- if (!dom.measureCharMajor) {
- text = document.createTextNode('0');
+ if (!('majorCharHeight' in this.props)) {
+ var textMajor = document.createTextNode('0');
var measureCharMajor = document.createElement('DIV');
measureCharMajor.className = 'text major measure';
- measureCharMajor.appendChild(text);
+ measureCharMajor.appendChild(textMajor);
this.frame.appendChild(measureCharMajor);
- dom.measureCharMajor = measureCharMajor;
+ this.props.majorCharHeight = measureCharMajor.clientHeight;
+ this.props.majorCharWidth = measureCharMajor.clientWidth;
+
+ this.frame.removeChild(measureCharMajor);
}
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
*/
-TimeAxis.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame,
- range = this.range;
+TimeAxis.prototype.snap = function snap (date) {
+ return this.step.snap(date);
+};
- if (!range) {
- throw new Error('Cannot repaint time axis: no range configured');
- }
+/**
+ * A current time bar
+ * @param {Range} range
+ * @param {Object} [options] Available parameters:
+ * {Boolean} [showCurrentTime]
+ * @constructor CurrentTime
+ * @extends Component
+ */
- if (frame) {
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
-
- // calculate size of a character
- var props = this.props,
- showMinorLabels = this.getOption('showMinorLabels'),
- showMajorLabels = this.getOption('showMajorLabels'),
- measureCharMinor = this.dom.measureCharMinor,
- measureCharMajor = this.dom.measureCharMajor;
- if (measureCharMinor) {
- props.minorCharHeight = measureCharMinor.clientHeight;
- props.minorCharWidth = measureCharMinor.clientWidth;
- }
- if (measureCharMajor) {
- props.majorCharHeight = measureCharMajor.clientHeight;
- props.majorCharWidth = measureCharMajor.clientWidth;
- }
-
- var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
- if (parentHeight != props.parentHeight) {
- props.parentHeight = parentHeight;
- changed += 1;
- }
- switch (this.getOption('orientation')) {
- case 'bottom':
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
-
- props.minorLabelTop = 0;
- props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
-
- props.minorLineTop = -this.top;
- props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
- props.minorLineWidth = 1; // TODO: really calculate width
-
- props.majorLineTop = -this.top;
- props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
- props.majorLineWidth = 1; // TODO: really calculate width
-
- props.lineTop = 0;
-
- break;
-
- case 'top':
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
-
- props.majorLabelTop = 0;
- props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
-
- props.minorLineTop = props.minorLabelTop;
- props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
- props.minorLineWidth = 1; // TODO: really calculate width
-
- props.majorLineTop = 0;
- props.majorLineHeight = Math.max(parentHeight - this.top);
- props.majorLineWidth = 1; // TODO: really calculate width
-
- props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
-
- break;
-
- default:
- throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
- }
+function CurrentTime (range, options) {
+ this.id = util.randomUUID();
- var height = props.minorLabelHeight + props.majorLabelHeight;
- changed += update(this, 'width', frame.offsetWidth);
- changed += update(this, 'height', height);
+ this.range = range;
+ this.options = options || {};
+ this.defaultOptions = {
+ showCurrentTime: false
+ };
- // calculate range and step
- this._updateConversion();
+ this._create();
+}
- var start = util.convert(range.start, 'Number'),
- end = util.convert(range.end, 'Number'),
- minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
- -this.toTime(0).valueOf();
- this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
- changed += update(props.range, 'start', start);
- changed += update(props.range, 'end', end);
- changed += update(props.range, 'minimumStep', minimumStep.valueOf());
- }
+CurrentTime.prototype = new Component();
- return (changed > 0);
-};
+CurrentTime.prototype.setOptions = Component.prototype.setOptions;
/**
- * Calculate the scale and offset to convert a position on screen to the
- * corresponding date and vice versa.
- * After the method _updateConversion is executed once, the methods toTime
- * and toScreen can be used.
+ * Create the HTML DOM for the current time bar
* @private
*/
-TimeAxis.prototype._updateConversion = function() {
- var range = this.range;
- if (!range) {
- throw new Error('No range configured');
- }
+CurrentTime.prototype._create = function _create () {
+ var bar = document.createElement('div');
+ bar.className = 'currenttime';
+ bar.style.position = 'absolute';
+ bar.style.top = '0px';
+ bar.style.height = '100%';
- if (range.conversion) {
- this.conversion = range.conversion(this.width);
- }
- else {
- this.conversion = Range.conversion(range.start, range.end, this.width);
- }
+ this.bar = bar;
};
/**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
+ * Get the frame element of the current time bar
+ * @returns {HTMLElement} frame
*/
-TimeAxis.prototype.snap = function snap (date) {
- return this.step.snap(date);
+CurrentTime.prototype.getFrame = function getFrame() {
+ return this.bar;
};
/**
- * A current time bar
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
- * @param {Object} [options] Available parameters:
- * {Boolean} [showCurrentTime]
- * @constructor CurrentTime
- * @extends Component
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
*/
+CurrentTime.prototype.repaint = function repaint() {
+ var parent = this.parent;
-function CurrentTime (parent, depends, options) {
- this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
-
- this.options = options || {};
- this.defaultOptions = {
- showCurrentTime: false
- };
-}
-
-CurrentTime.prototype = new Component();
+ var now = new Date();
+ var x = this.options.toScreen(now);
-CurrentTime.prototype.setOptions = Component.prototype.setOptions;
+ this.bar.style.left = x + 'px';
+ this.bar.title = 'Current time: ' + now;
-/**
- * Get the container element of the bar, which can be used by a child to
- * add its own widgets.
- * @returns {HTMLElement} container
- */
-CurrentTime.prototype.getContainer = function () {
- return this.frame;
+ return false;
};
/**
- * Repaint the component
- * @return {Boolean} changed
+ * Start auto refreshing the current time bar
*/
-CurrentTime.prototype.repaint = function () {
- var bar = this.frame,
- parent = this.parent,
- parentContainer = parent.parent.getContainer();
-
- if (!parent) {
- throw new Error('Cannot repaint bar: no parent attached');
- }
-
- if (!parentContainer) {
- throw new Error('Cannot repaint bar: parent has no container element');
- }
-
- if (!this.getOption('showCurrentTime')) {
- if (bar) {
- parentContainer.removeChild(bar);
- delete this.frame;
- }
+CurrentTime.prototype.start = function start() {
+ var me = this;
- return false;
- }
+ function update () {
+ me.stop();
- if (!bar) {
- bar = document.createElement('div');
- bar.className = 'currenttime';
- bar.style.position = 'absolute';
- bar.style.top = '0px';
- bar.style.height = '100%';
+ // determine interval to refresh
+ var scale = me.range.conversion(me.parent.width).scale;
+ var interval = 1 / scale / 10;
+ if (interval < 30) interval = 30;
+ if (interval > 1000) interval = 1000;
- parentContainer.appendChild(bar);
- this.frame = bar;
- }
+ me.repaint();
- if (!parent.conversion) {
- parent._updateConversion();
+ // start a timer to adjust for the new time
+ me.currentTimeTimer = setTimeout(update, interval);
}
- var now = new Date();
- var x = parent.toScreen(now);
-
- bar.style.left = x + 'px';
- bar.title = 'Current time: ' + now;
+ update();
+};
- // start a timer to adjust for the new time
+/**
+ * Stop auto refreshing the current time bar
+ */
+CurrentTime.prototype.stop = function stop() {
if (this.currentTimeTimer !== undefined) {
clearTimeout(this.currentTimeTimer);
delete this.currentTimeTimer;
}
-
- var timeline = this;
- var interval = 1 / parent.conversion.scale / 2;
-
- if (interval < 30) {
- interval = 30;
- }
-
- this.currentTimeTimer = setTimeout(function() {
- timeline.repaint();
- }, interval);
-
- return false;
};
/**
* A custom time bar
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
* @param {Object} [options] Available parameters:
* {Boolean} [showCustomTime]
* @constructor CustomTime
* @extends Component
*/
-function CustomTime (parent, depends, options) {
+function CustomTime (options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
this.options = options || {};
this.defaultOptions = {
@@ -4963,85 +4593,61 @@ function CustomTime (parent, depends, options) {
this.customTime = new Date();
this.eventParams = {}; // stores state parameters while dragging the bar
+
+ // create the DOM
+ this._create();
}
CustomTime.prototype = new Component();
-Emitter(CustomTime.prototype);
-
CustomTime.prototype.setOptions = Component.prototype.setOptions;
/**
- * Get the container element of the bar, which can be used by a child to
- * add its own widgets.
- * @returns {HTMLElement} container
+ * Create the DOM for the custom time
+ * @private
*/
-CustomTime.prototype.getContainer = function () {
- return this.frame;
+CustomTime.prototype._create = function _create () {
+ var bar = document.createElement('div');
+ bar.className = 'customtime';
+ bar.style.position = 'absolute';
+ bar.style.top = '0px';
+ bar.style.height = '100%';
+ this.bar = bar;
+
+ var drag = document.createElement('div');
+ drag.style.position = 'relative';
+ drag.style.top = '0px';
+ drag.style.left = '-10px';
+ drag.style.height = '100%';
+ drag.style.width = '20px';
+ bar.appendChild(drag);
+
+ // attach event listeners
+ this.hammer = Hammer(bar, {
+ prevent_default: true
+ });
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
+ this.hammer.on('drag', this._onDrag.bind(this));
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
+};
+
+/**
+ * Get the frame element of the custom time bar
+ * @returns {HTMLElement} frame
+ */
+CustomTime.prototype.getFrame = function getFrame() {
+ return this.bar;
};
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component is resized
*/
CustomTime.prototype.repaint = function () {
- var bar = this.frame,
- parent = this.parent;
-
- if (!parent) {
- throw new Error('Cannot repaint bar: no parent attached');
- }
-
- var parentContainer = parent.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint bar: parent has no container element');
- }
-
- if (!this.getOption('showCustomTime')) {
- if (bar) {
- parentContainer.removeChild(bar);
- delete this.frame;
- }
-
- return false;
- }
+ var x = this.options.toScreen(this.customTime);
- if (!bar) {
- bar = document.createElement('div');
- bar.className = 'customtime';
- bar.style.position = 'absolute';
- bar.style.top = '0px';
- bar.style.height = '100%';
-
- parentContainer.appendChild(bar);
-
- var drag = document.createElement('div');
- drag.style.position = 'relative';
- drag.style.top = '0px';
- drag.style.left = '-10px';
- drag.style.height = '100%';
- drag.style.width = '20px';
- bar.appendChild(drag);
-
- this.frame = bar;
-
- // attach event listeners
- this.hammer = Hammer(bar, {
- prevent_default: true
- });
- this.hammer.on('dragstart', this._onDragStart.bind(this));
- this.hammer.on('drag', this._onDrag.bind(this));
- this.hammer.on('dragend', this._onDragEnd.bind(this));
- }
-
- if (!parent.conversion) {
- parent._updateConversion();
- }
-
- var x = parent.toScreen(this.customTime);
-
- bar.style.left = x + 'px';
- bar.title = 'Time: ' + this.customTime;
+ this.bar.style.left = x + 'px';
+ this.bar.title = 'Time: ' + this.customTime;
return false;
};
@@ -5069,6 +4675,7 @@ CustomTime.prototype.getCustomTime = function() {
* @private
*/
CustomTime.prototype._onDragStart = function(event) {
+ this.eventParams.dragging = true;
this.eventParams.customTime = this.customTime;
event.stopPropagation();
@@ -5081,18 +4688,18 @@ CustomTime.prototype._onDragStart = function(event) {
* @private
*/
CustomTime.prototype._onDrag = function (event) {
+ if (!this.eventParams.dragging) return;
+
var deltaX = event.gesture.deltaX,
- x = this.parent.toScreen(this.eventParams.customTime) + deltaX,
- time = this.parent.toTime(x);
+ x = this.options.toScreen(this.eventParams.customTime) + deltaX,
+ time = this.options.toTime(x);
this.setCustomTime(time);
// fire a timechange event
- if (this.controller) {
- this.controller.emit('timechange', {
- time: this.customTime
- })
- }
+ this.emit('timechange', {
+ time: new Date(this.customTime.valueOf())
+ });
event.stopPropagation();
event.preventDefault();
@@ -5104,12 +4711,12 @@ CustomTime.prototype._onDrag = function (event) {
* @private
*/
CustomTime.prototype._onDragEnd = function (event) {
+ if (!this.eventParams.dragging) return;
+
// fire a timechanged event
- if (this.controller) {
- this.controller.emit('timechanged', {
- time: this.customTime
- })
- }
+ this.emit('timechanged', {
+ time: new Date(this.customTime.valueOf())
+ });
event.stopPropagation();
event.preventDefault();
@@ -5119,41 +4726,24 @@ CustomTime.prototype._onDragEnd = function (event) {
* An ItemSet holds a set of items and ranges which can be displayed in a
* range. The width is determined by the parent of the ItemSet, and the height
* is determined by the size of the items.
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
- * @param {Object} [options] See ItemSet.setOptions for the available
- * options.
+ * @param {Panel} backgroundPanel Panel which can be used to display the
+ * vertical lines of box items.
+ * @param {Panel} axisPanel Panel on the axis where the dots of box-items
+ * can be displayed.
+ * @param {Object} [options] See ItemSet.setOptions for the available options.
* @constructor ItemSet
* @extends Panel
*/
-// TODO: improve performance by replacing all Array.forEach with a for loop
-function ItemSet(parent, depends, options) {
+function ItemSet(backgroundPanel, axisPanel, options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
-
- // event listeners
- this.eventListeners = {
- dragstart: this._onDragStart.bind(this),
- drag: this._onDrag.bind(this),
- dragend: this._onDragEnd.bind(this)
- };
// one options object is shared by this itemset and all its items
this.options = options || {};
- this.defaultOptions = {
- type: 'box',
- align: 'center',
- orientation: 'bottom',
- margin: {
- axis: 20,
- item: 10
- },
- padding: 5
- };
-
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
+ this.itemOptions = Object.create(this.options);
this.dom = {};
+ this.hammer = null;
var me = this;
this.itemsData = null; // DataSet
@@ -5162,31 +4752,33 @@ function ItemSet(parent, depends, options) {
// data change listeners
this.listeners = {
'add': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onAdd(params.items);
- }
+ if (senderId != me.id) me._onAdd(params.items);
},
'update': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onUpdate(params.items);
- }
+ if (senderId != me.id) me._onUpdate(params.items);
},
'remove': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onRemove(params.items);
- }
+ if (senderId != me.id) me._onRemove(params.items);
}
};
- this.items = {}; // object with an Item for every data item
+ this.items = {}; // object with an Item for every data item
+ this.orderedItems = {
+ byStart: [],
+ byEnd: []
+ };
+ this.visibleItems = []; // visible, ordered items
+ this.visibleItemsStart = 0; // start index of visible items in this.orderedItems // TODO: cleanup
+ this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems // TODO: cleanup
this.selection = []; // list with the ids of all selected nodes
this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
- this.stack = new Stack(this, Object.create(this.options));
- this.conversion = null;
+ this.stack = new Stack(Object.create(this.options));
+ this.stackDirty = true; // if true, all items will be restacked on next repaint
this.touchParams = {}; // stores properties while dragging
- // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
+ // create the HTML DOM
+ this._create();
}
ItemSet.prototype = new Panel();
@@ -5199,6 +4791,42 @@ ItemSet.types = {
point: ItemPoint
};
+/**
+ * Create the HTML DOM for the ItemSet
+ */
+ItemSet.prototype._create = function _create(){
+ var frame = document.createElement('div');
+ frame['timeline-itemset'] = this;
+ this.frame = frame;
+
+ // create background panel
+ var background = document.createElement('div');
+ background.className = 'background';
+ this.backgroundPanel.frame.appendChild(background);
+ this.dom.background = background;
+
+ // create foreground panel
+ var foreground = document.createElement('div');
+ foreground.className = 'foreground';
+ frame.appendChild(foreground);
+ this.dom.foreground = foreground;
+
+ // create axis panel
+ var axis = document.createElement('div');
+ axis.className = 'axis';
+ this.dom.axis = axis;
+ this.axisPanel.frame.appendChild(axis);
+
+ // attach event listeners
+ // TODO: use event listeners from the rootpanel to improve performance?
+ this.hammer = Hammer(frame, {
+ prevent_default: true
+ });
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
+ this.hammer.on('drag', this._onDrag.bind(this));
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
+};
+
/**
* Set options for the ItemSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
@@ -5229,54 +4857,36 @@ ItemSet.types = {
*/
ItemSet.prototype.setOptions = Component.prototype.setOptions;
-
-
/**
- * Set controller for this component
- * @param {Controller | null} controller
+ * Hide the component from the DOM
*/
-ItemSet.prototype.setController = function setController (controller) {
- var event;
-
- // unregister old event listeners
- if (this.controller) {
- for (event in this.eventListeners) {
- if (this.eventListeners.hasOwnProperty(event)) {
- this.controller.off(event, this.eventListeners[event]);
- }
- }
+ItemSet.prototype.hide = function hide() {
+ // remove the axis with dots
+ if (this.dom.axis.parentNode) {
+ this.dom.axis.parentNode.removeChild(this.dom.axis);
}
- this.controller = controller || null;
-
- // register new event listeners
- if (this.controller) {
- for (event in this.eventListeners) {
- if (this.eventListeners.hasOwnProperty(event)) {
- this.controller.on(event, this.eventListeners[event]);
- }
- }
+ // remove the background with vertical lines
+ if (this.dom.background.parentNode) {
+ this.dom.background.parentNode.removeChild(this.dom.background);
}
};
-// attach event listeners for dragging items to the controller
-(function (me) {
- var _controller = null;
- var _onDragStart = null;
- var _onDrag = null;
- var _onDragEnd = null;
-
- Object.defineProperty(me, 'controller', {
- get: function () {
- return _controller;
- },
-
- set: function (controller) {
-
- }
- });
-}) (this);
+/**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+ItemSet.prototype.show = function show() {
+ // show axis with dots
+ if (!this.dom.axis.parentNode) {
+ this.axisPanel.frame.appendChild(this.dom.axis);
+ }
+ // show background with vertical lines
+ if (!this.dom.background.parentNode) {
+ this.backgroundPanel.frame.appendChild(this.dom.background);
+ }
+};
/**
* Set range (start and end).
@@ -5298,7 +4908,7 @@ ItemSet.prototype.setRange = function setRange(range) {
* unselected.
*/
ItemSet.prototype.setSelection = function setSelection(ids) {
- var i, ii, id, item, selection;
+ var i, ii, id, item;
if (ids) {
if (!Array.isArray(ids)) {
@@ -5322,10 +4932,6 @@ ItemSet.prototype.setSelection = function setSelection(ids) {
item.select();
}
}
-
- if (this.controller) {
- this.requestRepaint();
- }
}
};
@@ -5352,184 +4958,151 @@ ItemSet.prototype._deselect = function _deselect(id) {
}
};
+/**
+ * Return the item sets frame
+ * @returns {HTMLElement} frame
+ */
+ItemSet.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component is resized
*/
ItemSet.prototype.repaint = function repaint() {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
+ var asSize = util.option.asSize,
+ asString = util.option.asString,
options = this.options,
orientation = this.getOption('orientation'),
- defaultOptions = this.defaultOptions,
frame = this.frame;
- if (!frame) {
- frame = document.createElement('div');
- frame.className = 'itemset';
- frame['timeline-itemset'] = this;
+ // update className
+ frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
- 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;
+ // check whether zoomed (in that case we need to re-stack everything)
+ var visibleInterval = this.range.end - this.range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ this.lastVisibleInterval = visibleInterval;
+ this.lastWidth = this.width;
- this.frame = frame;
- changed += 1;
+ /* TODO: implement+fix smarter way to update visible items
+ // find the first visible item
+ // TODO: use faster search, not linear
+ var byEnd = this.orderedItems.byEnd;
+ var start = 0;
+ var item = null;
+ while ((item = byEnd[start]) &&
+ (('end' in item.data) ? item.data.end : item.data.start) < this.range.start) {
+ start++;
}
- if (!this.parent) {
- throw new Error('Cannot repaint itemset: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint itemset: parent has no container element');
- }
- if (!frame.parentNode) {
- parentContainer.appendChild(frame);
- changed += 1;
- }
- if (!this.dom.axis.parentNode) {
- parentContainer.appendChild(this.dom.axis);
- changed += 1;
+ // find the last visible item
+ // TODO: use faster search, not linear
+ var byStart = this.orderedItems.byStart;
+ var end = 0;
+ while ((item = byStart[end]) && item.data.start < this.range.end) {
+ end++;
}
- // reposition frame
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+ console.log('visible items', start, end); // TODO: cleanup
+ console.log('visible item ids', byStart[start] && byStart[start].id, byEnd[end-1] && byEnd[end-1].id); // TODO: cleanup
- // reposition axis
- changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
- changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
- if (orientation == 'bottom') {
- changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
- }
- else { // orientation == 'top'
- changed += update(this.dom.axis.style, 'top', this.top + 'px');
+ this.visibleItems = [];
+ var i = start;
+ item = byStart[i];
+ var lastItem = byEnd[end];
+ while (item && item !== lastItem) {
+ this.visibleItems.push(item);
+ item = byStart[++i];
}
+ this.stack.order(this.visibleItems);
- this._updateConversion();
-
- var me = this,
- queue = this.queue,
- itemsData = this.itemsData,
- items = this.items,
- dataOptions = {
- // TODO: cleanup
- // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
- };
-
- // show/hide added/changed/removed items
- for (var id in queue) {
- if (queue.hasOwnProperty(id)) {
- var entry = queue[id],
- item = items[id],
- action = entry.action;
-
- //noinspection FallthroughInSwitchStatementJS
- switch (action) {
- case 'add':
- case 'update':
- var itemData = itemsData && itemsData.get(id, dataOptions);
-
- if (itemData) {
- var type = itemData.type ||
- (itemData.start && itemData.end && 'range') ||
- options.type ||
- 'box';
- var constructor = ItemSet.types[type];
-
- // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
- if (item) {
- // update item
- if (!constructor || !(item instanceof constructor)) {
- // item type has changed, hide and delete the item
- changed += item.hide();
- item = null;
- }
- else {
- item.data = itemData; // TODO: create a method item.setData ?
- changed++;
- }
- }
+ // show visible items
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ item = this.visibleItems[i];
- if (!item) {
- // create item
- if (constructor) {
- item = new constructor(me, itemData, options, defaultOptions);
- item.id = entry.id; // we take entry.id, as id itself is stringified
- changed++;
- }
- else {
- throw new TypeError('Unknown item type "' + type + '"');
- }
- }
+ if (!item.displayed) item.show();
+ item.top = null; // reset stacking position
- // force a repaint (not only a reposition)
- item.repaint();
+ // reposition item horizontally
+ item.repositionX();
+ }
+ */
- items[id] = item;
- }
+ // simple, brute force calculation of visible items
+ // TODO: replace with a faster, more sophisticated solution
+ this.visibleItems = [];
+ for (var id in this.items) {
+ if (this.items.hasOwnProperty(id)) {
+ var item = this.items[id];
+ if (item.isVisible(this.range)) {
+ if (!item.displayed) item.show();
- // update queue
- delete queue[id];
- break;
+ // reposition item horizontally
+ item.repositionX();
- case 'remove':
- if (item) {
- // remove the item from the set selected items
- if (item.selected) {
- me._deselect(id);
- }
+ this.visibleItems.push(item);
+ }
+ else {
+ if (item.displayed) item.hide();
+ }
+ }
+ }
- // remove DOM of the item
- changed += item.hide();
- }
+ // reposition visible items vertically
+ //this.stack.order(this.visibleItems); // TODO: improve ordering
+ var force = this.stackDirty || zoomed; // force re-stacking of all items if true
+ this.stack.stack(this.visibleItems, force);
+ this.stackDirty = false;
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ this.visibleItems[i].repositionY();
+ }
- // update lists
- delete items[id];
- delete queue[id];
- break;
+ // recalculate the height of the itemset
+ var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis,
+ marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item,
+ height;
- default:
- console.log('Error: unknown action "' + action + '"');
- }
- }
+ // determine the height from the stacked items
+ var visibleItems = this.visibleItems;
+ if (visibleItems.length) {
+ var min = visibleItems[0].top;
+ var max = visibleItems[0].top + visibleItems[0].height;
+ util.forEach(visibleItems, function (item) {
+ min = Math.min(min, item.top);
+ max = Math.max(max, (item.top + item.height));
+ });
+ height = (max - min) + marginAxis + marginItem;
+ }
+ else {
+ height = marginAxis + marginItem;
}
- // reposition all items. Show items only when in the visible area
- util.forEach(this.items, function (item) {
- if (item.visible) {
- changed += item.show();
- item.reposition();
- }
- else {
- changed += item.hide();
- }
- });
+ // reposition frame
+ frame.style.left = asSize(options.left, '');
+ frame.style.right = asSize(options.right, '');
+ frame.style.top = asSize((orientation == 'top') ? '0' : '');
+ frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = asSize(height);
+ //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
+
+ // calculate actual size and position
+ this.top = frame.offsetTop;
+ this.left = frame.offsetLeft;
+ this.width = frame.offsetWidth;
+ this.height = height;
+
+ // reposition axis
+ this.dom.axis.style.left = asSize(options.left, '0');
+ this.dom.axis.style.right = asSize(options.right, '');
+ this.dom.axis.style.width = asSize(options.width, '100%');
+ this.dom.axis.style.height = asSize(0);
+ this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
+ this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
- return (changed > 0);
+ return this._isResized();
};
/**
@@ -5557,97 +5130,13 @@ ItemSet.prototype.getAxis = function getAxis() {
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Set items
+ * @param {vis.DataSet | null} items
*/
-ItemSet.prototype.reflow = function reflow () {
- var changed = 0,
- options = this.options,
- marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.defaultOptions.margin.axis,
- marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.defaultOptions.margin.item,
- update = util.updateProperty,
- asNumber = util.option.asNumber,
- asSize = util.option.asSize,
- frame = this.frame;
-
- if (frame) {
- this._updateConversion();
-
- util.forEach(this.items, function (item) {
- changed += item.reflow();
- });
-
- // TODO: stack.update should be triggered via an event, in stack itself
- // TODO: only update the stack when there are changed items
- this.stack.update();
-
- var maxHeight = asNumber(options.maxHeight);
- var fixedHeight = (asSize(options.height) != null);
- var height;
- if (fixedHeight) {
- height = frame.offsetHeight;
- }
- else {
- // height is not specified, determine the height from the height and positioned items
- var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
- if (visibleItems.length) {
- var min = visibleItems[0].top;
- var max = visibleItems[0].top + visibleItems[0].height;
- util.forEach(visibleItems, function (item) {
- min = Math.min(min, item.top);
- max = Math.max(max, (item.top + item.height));
- });
- height = (max - min) + marginAxis + marginItem;
- }
- else {
- height = marginAxis + marginItem;
- }
- }
- if (maxHeight != null) {
- height = Math.min(height, maxHeight);
- }
- changed += update(this, 'height', height);
-
- // calculate height from items
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
- }
- else {
- changed += 1;
- }
-
- return (changed > 0);
-};
-
-/**
- * Hide this component from the DOM
- * @return {Boolean} changed
- */
-ItemSet.prototype.hide = function hide() {
- var changed = false;
-
- // remove the DOM
- if (this.frame && this.frame.parentNode) {
- this.frame.parentNode.removeChild(this.frame);
- changed = true;
- }
- if (this.dom.axis && this.dom.axis.parentNode) {
- this.dom.axis.parentNode.removeChild(this.dom.axis);
- changed = true;
- }
-
- return changed;
-};
-
-/**
- * Set items
- * @param {vis.DataSet | null} items
- */
-ItemSet.prototype.setItems = function setItems(items) {
- var me = this,
- ids,
- oldItemsData = this.itemsData;
+ItemSet.prototype.setItems = function setItems(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
// replace the dataset
if (!items) {
@@ -5704,7 +5193,9 @@ ItemSet.prototype.removeItem = function removeItem (id) {
// confirm deletion
this.options.onRemove(item, function (item) {
if (item) {
- dataset.remove(item);
+ // remove by id here, it is possible that an item has no id defined
+ // itself, so better not delete by the item itself
+ dataset.remove(id);
}
});
}
@@ -5716,17 +5207,58 @@ ItemSet.prototype.removeItem = function removeItem (id) {
* @private
*/
ItemSet.prototype._onUpdate = function _onUpdate(ids) {
- this._toQueue('update', ids);
+ var me = this,
+ items = this.items,
+ itemOptions = this.itemOptions;
+
+ ids.forEach(function (id) {
+ var itemData = me.itemsData.get(id),
+ item = items[id],
+ type = itemData.type ||
+ (itemData.start && itemData.end && 'range') ||
+ me.options.type ||
+ 'box';
+
+ var constructor = ItemSet.types[type];
+
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, hide and delete the item
+ item.hide();
+ item = null;
+ }
+ else {
+ item.data = itemData; // TODO: create a method item.setData ?
+ }
+ }
+
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(me, itemData, me.options, itemOptions);
+ item.id = id;
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
+
+ me.items[id] = item;
+ });
+
+ this._order();
+
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
};
/**
- * Handle changed items
+ * Handle added items
* @param {Number[]} ids
* @private
*/
-ItemSet.prototype._onAdd = function _onAdd(ids) {
- this._toQueue('add', ids);
-};
+ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
/**
* Handle removed items
@@ -5734,73 +5266,42 @@ ItemSet.prototype._onAdd = function _onAdd(ids) {
* @private
*/
ItemSet.prototype._onRemove = function _onRemove(ids) {
- this._toQueue('remove', ids);
-};
-
-/**
- * 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;
+ var count = 0;
+ var me = this;
ids.forEach(function (id) {
- queue[id] = {
- id: id,
- action: action
- };
+ var item = me.items[id];
+ if (item) {
+ count++;
+ item.hide();
+ delete me.items[id];
+ delete me.visibleItems[id];
+
+ // remove from selection
+ var index = me.selection.indexOf(id);
+ if (index != -1) me.selection.splice(index, 1);
+ }
});
- if (this.controller) {
- //this.requestReflow();
- this.requestRepaint();
+ if (count) {
+ // update order
+ this._order();
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
}
};
/**
- * Calculate the scale and offset to convert a position on screen to the
- * corresponding date and vice versa.
- * After the method _updateConversion is executed once, the methods toTime
- * and toScreen can be used.
+ * Order the items
* @private
*/
-ItemSet.prototype._updateConversion = function _updateConversion() {
- var range = this.range;
- if (!range) {
- throw new Error('No range configured');
- }
-
- if (range.conversion) {
- this.conversion = range.conversion(this.width);
- }
- else {
- this.conversion = Range.conversion(range.start, range.end, this.width);
- }
-};
-
-/**
- * Convert a position on screen (pixels) to a datetime
- * Before this method can be used, the method _updateConversion must be
- * executed once.
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
- */
-ItemSet.prototype.toTime = function toTime(x) {
- var conversion = this.conversion;
- return new Date(x / conversion.scale + conversion.offset);
-};
+ItemSet.prototype._order = function _order() {
+ var array = util.toArray(this.items);
+ this.orderedItems.byStart = array;
+ this.orderedItems.byEnd = [].concat(array);
-/**
- * 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.scale;
+ // reorder the items
+ this.stack.orderByStart(this.orderedItems.byStart);
+ this.stack.orderByEnd(this.orderedItems.byEnd);
};
/**
@@ -5863,7 +5364,8 @@ ItemSet.prototype._onDrag = function (event) {
if (this.touchParams.itemProps) {
var snap = this.options.snap || null,
deltaX = event.gesture.deltaX,
- offset = deltaX / this.conversion.scale;
+ scale = (this.width / (this.range.end - this.range.start)),
+ offset = deltaX / scale;
// move
this.touchParams.itemProps.forEach(function (props) {
@@ -5881,7 +5383,8 @@ ItemSet.prototype._onDrag = function (event) {
// TODO: implement dragging from one group to another
- this.requestReflow();
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
event.stopPropagation();
}
@@ -5897,8 +5400,7 @@ ItemSet.prototype._onDragEnd = function (event) {
// prepare a change set for the changed items
var changes = [],
me = this,
- dataset = this._myDataSet(),
- type;
+ dataset = this._myDataSet();
this.touchParams.itemProps.forEach(function (props) {
var id = props.item.id,
@@ -5919,13 +5421,16 @@ ItemSet.prototype._onDragEnd = function (event) {
me.options.onMove(item, function (item) {
if (item) {
// apply changes
+ item[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(item);
}
else {
// restore original values
if ('start' in props) props.item.data.start = props.start;
if ('end' in props) props.item.data.end = props.end;
- me.requestReflow();
+
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
}
});
}
@@ -6007,12 +5512,13 @@ function Item (parent, data, options, defaultOptions) {
this.defaultOptions = defaultOptions || {};
this.selected = false;
- this.visible = false;
- this.top = 0;
- this.left = 0;
- this.width = 0;
- this.height = 0;
- this.offset = 0;
+ this.displayed = false;
+ this.dirty = true;
+
+ this.top = null;
+ this.left = null;
+ this.width = null;
+ this.height = null;
}
/**
@@ -6020,7 +5526,7 @@ function Item (parent, data, options, defaultOptions) {
*/
Item.prototype.select = function select() {
this.selected = true;
- if (this.visible) this.repaint();
+ if (this.displayed) this.repaint();
};
/**
@@ -6028,7 +5534,7 @@ Item.prototype.select = function select() {
*/
Item.prototype.unselect = function unselect() {
this.selected = false;
- if (this.visible) this.repaint();
+ if (this.displayed) this.repaint();
};
/**
@@ -6049,28 +5555,23 @@ Item.prototype.hide = function hide() {
/**
* Repaint the item
- * @return {Boolean} changed
*/
Item.prototype.repaint = function repaint() {
// should be implemented by the item
- return false;
};
/**
- * Reflow the item
- * @return {Boolean} resized
+ * Reposition the Item horizontally
*/
-Item.prototype.reflow = function reflow() {
+Item.prototype.repositionX = function repositionX() {
// should be implemented by the item
- return false;
};
/**
- * Give the item a display offset in pixels
- * @param {Number} offset Offset on screen in pixels
+ * Reposition the Item vertically
*/
-Item.prototype.setOffset = function setOffset(offset) {
- this.offset = offset;
+Item.prototype.repositionY = function repositionY() {
+ // should be implemented by the item
};
/**
@@ -6120,296 +5621,223 @@ Item.prototype._repaintDeleteButton = function (anchor) {
function ItemBox (parent, data, options, defaultOptions) {
this.props = {
dot: {
- left: 0,
- top: 0,
width: 0,
height: 0
},
line: {
- top: 0,
- left: 0,
width: 0,
height: 0
}
};
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
Item.call(this, parent, data, options, defaultOptions);
}
ItemBox.prototype = new Item (null, null);
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemBox.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ // TODO: account for the real width of the item. Right now we just add 1/4 to the window
+ var interval = (range.end - range.start) / 4;
+ return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
+};
+
/**
* Repaint the item
- * @return {Boolean} changed
*/
ItemBox.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
var dom = this.dom;
-
if (!dom) {
- this._create();
+ // create DOM
+ this.dom = {};
dom = this.dom;
- changed = true;
+
+ // create main box
+ dom.box = document.createElement('DIV');
+
+ // contents box (inside the background box). used for making margins
+ dom.content = document.createElement('DIV');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
+
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
}
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.getForeground();
+ if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
+ foreground.appendChild(dom.box);
+ }
+ if (!dom.line.parentNode) {
+ var background = this.parent.getBackground();
+ if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
+ background.appendChild(dom.line);
+ }
+ if (!dom.dot.parentNode) {
+ var axis = this.parent.getAxis();
+ if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
+ axis.appendChild(dom.dot);
+ }
+ this.displayed = true;
- if (!dom.box.parentNode) {
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
- }
- foreground.appendChild(dom.box);
- changed = true;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
-
- if (!dom.line.parentNode) {
- var background = this.parent.getBackground();
- if (!background) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no background container element');
- }
- background.appendChild(dom.line);
- changed = true;
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
-
- if (!dom.dot.parentNode) {
- var axis = this.parent.getAxis();
- if (!background) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no axis container element');
- }
- axis.appendChild(dom.dot);
- changed = true;
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
}
- this._repaintDeleteButton(dom.box);
+ this.dirty = true;
+ }
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item box' + className;
+ dom.line.className = 'item line' + className;
+ dom.dot.className = 'item dot' + className;
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
- changed = true;
- }
+ this.dirty = true;
+ }
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.box.className = 'item box' + className;
- dom.line.className = 'item line' + className;
- dom.dot.className = 'item dot' + className;
- changed = true;
- }
+ // recalculate size
+ if (this.dirty) {
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.line.width = dom.line.offsetWidth;
+ this.width = dom.box.offsetWidth;
+ this.height = dom.box.offsetHeight;
+
+ this.dirty = false;
}
- return changed;
+ this._repaintDeleteButton(dom.box);
};
/**
- * Show the item in the DOM (when not already visible). The items DOM will
+ * Show the item in the DOM (when not already displayed). The items DOM will
* be created when needed.
- * @return {Boolean} changed
*/
ItemBox.prototype.show = function show() {
- if (!this.dom || !this.dom.box.parentNode) {
- return this.repaint();
- }
- else {
- return false;
+ if (!this.displayed) {
+ this.repaint();
}
};
/**
* Hide the item from the DOM (when visible)
- * @return {Boolean} changed
*/
ItemBox.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.box.parentNode) {
- dom.box.parentNode.removeChild(dom.box);
- changed = true;
- }
- if (dom.line.parentNode) {
- dom.line.parentNode.removeChild(dom.line);
- }
- if (dom.dot.parentNode) {
- dom.dot.parentNode.removeChild(dom.dot);
- }
+ if (this.displayed) {
+ var dom = this.dom;
+
+ if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
+ if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
+ if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
}
- return changed;
};
/**
- * Reflow the item: calculate its actual size and position from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
+ * Reposition the item horizontally
+ * @Override
*/
-ItemBox.prototype.reflow = function reflow() {
- var changed = 0,
- update,
- dom,
- props,
- options,
- margin,
- start,
- align,
- orientation,
- top,
+ItemBox.prototype.repositionX = function repositionX() {
+ var start = this.defaultOptions.toScreen(this.data.start),
+ align = this.options.align || this.defaultOptions.align,
left,
- data,
- range;
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
+ // calculate left position of the box
+ if (align == 'right') {
+ this.left = start - this.width;
}
-
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item
- var interval = (range.end - range.start);
- this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
+ else if (align == 'left') {
+ this.left = start;
}
else {
- this.visible = false;
+ // default or 'center'
+ this.left = start - this.width / 2;
}
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- update = util.updateProperty;
- props = this.props;
- options = this.options;
- start = this.parent.toScreen(this.data.start) + this.offset;
- align = options.align || this.defaultOptions.align;
- margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
- orientation = options.orientation || this.defaultOptions.orientation;
-
- changed += update(props.dot, 'height', dom.dot.offsetHeight);
- changed += update(props.dot, 'width', dom.dot.offsetWidth);
- changed += update(props.line, 'width', dom.line.offsetWidth);
- changed += update(props.line, 'height', dom.line.offsetHeight);
- changed += update(props.line, 'top', dom.line.offsetTop);
- changed += update(this, 'width', dom.box.offsetWidth);
- changed += update(this, 'height', dom.box.offsetHeight);
- if (align == 'right') {
- left = start - this.width;
- }
- else if (align == 'left') {
- left = start;
- }
- else {
- // default or 'center'
- left = start - this.width / 2;
- }
- changed += update(this, 'left', left);
-
- changed += update(props.line, 'left', start - props.line.width / 2);
- changed += update(props.dot, 'left', start - props.dot.width / 2);
- changed += update(props.dot, 'top', -props.dot.height / 2);
- if (orientation == 'top') {
- top = margin;
-
- changed += update(this, 'top', top);
- }
- else {
- // default or 'bottom'
- var parentHeight = this.parent.height;
- top = parentHeight - this.height - margin;
+ // reposition box
+ box.style.left = this.left + 'px';
- changed += update(this, 'top', top);
- }
- }
- else {
- changed += 1;
- }
- }
+ // reposition line
+ line.style.left = (start - this.props.line.width / 2) + 'px';
- return (changed > 0);
+ // reposition dot
+ dot.style.left = (start - this.props.dot.width / 2) + 'px';
};
/**
- * Create an items DOM
- * @private
+ * Reposition the item vertically
+ * @Override
*/
-ItemBox.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
-
- // create the box
- dom.box = document.createElement('DIV');
- // className is updated in repaint()
+ItemBox.prototype.repositionY = function repositionY () {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
- // contents box (inside the background box). used for making margins
- dom.content = document.createElement('DIV');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
-
- // line to axis
- dom.line = document.createElement('DIV');
- dom.line.className = 'line';
-
- // dot on axis
- dom.dot = document.createElement('DIV');
- dom.dot.className = 'dot';
+ if (orientation == 'top') {
+ box.style.top = (this.top || 0) + 'px';
+ box.style.bottom = '';
- // attach this item as attribute
- dom.box['timeline-item'] = this;
+ line.style.top = '0';
+ line.style.bottom = '';
+ line.style.height = (this.parent.top + this.top + 1) + 'px';
}
-};
-
-/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
- */
-ItemBox.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props,
- orientation = this.options.orientation || this.defaultOptions.orientation;
-
- if (dom) {
- var box = dom.box,
- line = dom.line,
- dot = dom.dot;
-
- box.style.left = this.left + 'px';
- box.style.top = this.top + 'px';
+ else { // orientation 'bottom'
+ box.style.top = '';
+ box.style.bottom = (this.top || 0) + 'px';
- line.style.left = props.line.left + 'px';
- if (orientation == 'top') {
- line.style.top = 0 + 'px';
- line.style.height = this.top + 'px';
- }
- else {
- // orientation 'bottom'
- line.style.top = (this.top + this.height) + 'px';
- line.style.height = Math.max(this.parent.height - this.top - this.height +
- this.props.dot.height / 2, 0) + 'px';
- }
-
- dot.style.left = props.dot.left + 'px';
- dot.style.top = props.dot.top + 'px';
+ line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
+ line.style.bottom = '0';
+ line.style.height = '';
}
+
+ dot.style.top = (-this.props.dot.height / 2) + 'px';
};
/**
@@ -6435,222 +5863,173 @@ function ItemPoint (parent, data, options, defaultOptions) {
}
};
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
Item.call(this, parent, data, options, defaultOptions);
}
ItemPoint.prototype = new Item (null, null);
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemPoint.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ var interval = (range.end - range.start);
+ return (this.data.start > range.start - interval) && (this.data.start < range.end);
+}
+
/**
* Repaint the item
- * @return {Boolean} changed
*/
ItemPoint.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
var dom = this.dom;
-
if (!dom) {
- this._create();
+ // create DOM
+ this.dom = {};
dom = this.dom;
- changed = true;
+
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box, right from the dot
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.point.appendChild(dom.content);
+
+ // dot at start
+ dom.dot = document.createElement('div');
+ dom.dot.className = 'dot';
+ dom.point.appendChild(dom.dot);
+
+ // attach this item as attribute
+ dom.point['timeline-item'] = this;
}
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.point.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
+ throw new Error('Cannot repaint time axis: parent has no foreground container element');
}
+ foreground.appendChild(dom.point);
+ }
+ this.displayed = true;
- if (!dom.point.parentNode) {
- foreground.appendChild(dom.point);
- foreground.appendChild(dom.point);
- changed = true;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
-
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
- changed = true;
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+
+ this.dirty = true;
+ }
- this._repaintDeleteButton(dom.point);
+ // 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;
- // 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;
- }
+ this.dirty = true;
}
- return changed;
+ // recalculate size
+ if (this.dirty) {
+ this.width = dom.point.offsetWidth;
+ this.height = dom.point.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.content.height = dom.content.offsetHeight;
+
+ // resize contents
+ dom.content.style.marginLeft = 1.5 * this.props.dot.width + 'px';
+ //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
+
+ dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.point);
};
/**
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
- * @return {Boolean} changed
*/
ItemPoint.prototype.show = function show() {
- if (!this.dom || !this.dom.point.parentNode) {
- return this.repaint();
- }
- else {
- return false;
+ if (!this.displayed) {
+ this.repaint();
}
};
/**
* Hide the item from the DOM (when visible)
- * @return {Boolean} changed
*/
ItemPoint.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.point.parentNode) {
- dom.point.parentNode.removeChild(dom.point);
- changed = true;
+ if (this.displayed) {
+ if (this.dom.point.parentNode) {
+ this.dom.point.parentNode.removeChild(this.dom.point);
}
- }
- return changed;
-};
-
-/**
- * Reflow the item: calculate its actual size from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
- */
-ItemPoint.prototype.reflow = function 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);
- }
- else {
- this.visible = false;
- }
+ this.top = null;
+ this.left = null;
- 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) + this.offset;
-
- 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;
- }
+ this.displayed = false;
}
-
- return (changed > 0);
};
/**
- * Create an items DOM
- * @private
+ * Reposition the item horizontally
+ * @Override
*/
-ItemPoint.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
-
- // background box
- dom.point = document.createElement('div');
- // className is updated in repaint()
-
- // contents box, right from the dot
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.point.appendChild(dom.content);
+ItemPoint.prototype.repositionX = function repositionX() {
+ var start = this.defaultOptions.toScreen(this.data.start);
- // dot at start
- dom.dot = document.createElement('div');
- dom.dot.className = 'dot';
- dom.point.appendChild(dom.dot);
+ this.left = start - this.props.dot.width / 2;
- // attach this item as attribute
- dom.point['timeline-item'] = this;
- }
+ // reposition point
+ this.dom.point.style.left = this.left + 'px';
};
/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
+ * Reposition the item vertically
+ * @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
+ItemPoint.prototype.repositionY = function repositionY () {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ point = this.dom.point;
- dom.dot.style.top = props.dot.top + 'px';
+ if (orientation == 'top') {
+ point.style.top = this.top + 'px';
+ point.style.bottom = '';
}
-};
+ else {
+ point.style.top = '';
+ point.style.bottom = this.top + 'px';
+ }
+}
/**
* @constructor ItemRange
@@ -6665,90 +6044,120 @@ ItemPoint.prototype.reposition = function reposition() {
function ItemRange (parent, data, options, defaultOptions) {
this.props = {
content: {
- left: 0,
width: 0
}
};
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data.id);
+ }
+ if (data.end == undefined) {
+ throw new Error('Property "end" missing in item ' + data.id);
+ }
+ }
+
Item.call(this, parent, data, options, defaultOptions);
}
ItemRange.prototype = new Item (null, null);
+ItemRange.prototype.baseClassName = 'item range';
+
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemRange.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ return (this.data.start < range.end) && (this.data.end > range.start);
+};
+
/**
* Repaint the item
- * @return {Boolean} changed
*/
ItemRange.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
var dom = this.dom;
-
if (!dom) {
- this._create();
+ // create DOM
+ this.dom = {};
dom = this.dom;
- changed = true;
+
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
}
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
+ throw new Error('Cannot repaint time axis: parent has no foreground container element');
}
+ foreground.appendChild(dom.box);
+ }
+ this.displayed = true;
- if (!dom.box.parentNode) {
- foreground.appendChild(dom.box);
- changed = true;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
-
- // update content
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
- changed = true;
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
}
- this._repaintDeleteButton(dom.box);
- this._repaintDragLeft();
- this._repaintDragRight();
+ this.dirty = true;
+ }
- // update class
- var className = (this.data.className ? (' ' + this.data.className) : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.box.className = 'item range' + className;
- changed = true;
- }
+ // update class
+ var className = (this.data.className ? (' ' + this.data.className) : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = this.baseClassName + className;
+
+ this.dirty = true;
+ }
+
+ // recalculate size
+ if (this.dirty) {
+ this.props.content.width = this.dom.content.offsetWidth;
+ this.height = this.dom.box.offsetHeight;
+
+ this.dirty = false;
}
- return changed;
+ this._repaintDeleteButton(dom.box);
+ this._repaintDragLeft();
+ this._repaintDragRight();
};
/**
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
- * @return {Boolean} changed
*/
ItemRange.prototype.show = function show() {
- if (!this.dom || !this.dom.box.parentNode) {
- return this.repaint();
- }
- else {
- return false;
+ if (!this.displayed) {
+ this.repaint();
}
};
@@ -6757,154 +6166,73 @@ ItemRange.prototype.show = function show() {
* @return {Boolean} changed
*/
ItemRange.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.box.parentNode) {
- dom.box.parentNode.removeChild(dom.box);
- changed = true;
+ if (this.displayed) {
+ var box = this.dom.box;
+
+ if (box.parentNode) {
+ box.parentNode.removeChild(box);
}
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
}
- return changed;
};
/**
- * Reflow the item: calculate its actual size from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
+ * Reposition the item horizontally
+ * @Override
*/
-ItemRange.prototype.reflow = function reflow() {
- var changed = 0,
- dom,
- props,
- options,
- margin,
- padding,
- parent,
- start,
- end,
- data,
- range,
- update,
- box,
- parentWidth,
- contentLeft,
- orientation,
- top;
+ItemRange.prototype.repositionX = function repositionX() {
+ var props = this.props,
+ parentWidth = this.parent.width,
+ start = this.defaultOptions.toScreen(this.data.start),
+ end = this.defaultOptions.toScreen(this.data.end),
+ padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
+ contentLeft;
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
}
- if (this.data.end == undefined) {
- throw new Error('Property "end" missing in item ' + this.data.id);
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
}
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item. Take some margin
- this.visible = (data.start < range.end) && (data.end > range.start);
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ if (start < 0) {
+ contentLeft = Math.min(-start,
+ (end - start - props.content.width - 2 * padding));
+ // TODO: remove the need for options.padding. it's terrible.
}
else {
- this.visible = false;
+ contentLeft = 0;
}
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- props = this.props;
- options = this.options;
- parent = this.parent;
- start = parent.toScreen(this.data.start) + this.offset;
- end = parent.toScreen(this.data.end) + this.offset;
- 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);
-
- // limit the width of the this, as browsers cannot draw very wide divs
- if (start < -parentWidth) {
- start = -parentWidth;
- }
- if (end > 2 * parentWidth) {
- end = 2 * parentWidth;
- }
-
- // when range exceeds left of the window, position the contents at the left of the visible area
- if (start < 0) {
- contentLeft = Math.min(-start,
- (end - start - props.content.width - 2 * padding));
- // TODO: remove the need for options.padding. it's terrible.
- }
- else {
- contentLeft = 0;
- }
- changed += update(props.content, 'left', contentLeft);
-
- 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);
- }
-
- changed += update(this, 'left', start);
- changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
- }
- else {
- changed += 1;
- }
- }
+ this.left = start;
+ this.width = Math.max(end - start, 1);
- return (changed > 0);
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = this.width + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
};
/**
- * Create an items DOM
- * @private
+ * Reposition the item vertically
+ * @Override
*/
-ItemRange.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
- // background box
- dom.box = document.createElement('div');
- // className is updated in repaint()
-
- // contents box
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+ItemRange.prototype.repositionY = function repositionY() {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ box = this.dom.box;
- // attach this item as attribute
- dom.box['timeline-item'] = this;
+ if (orientation == 'top') {
+ box.style.top = this.top + 'px';
+ box.style.bottom = '';
}
-};
-
-/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
- */
-ItemRange.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props;
-
- if (dom) {
- dom.box.style.top = this.top + 'px';
- dom.box.style.left = this.left + 'px';
- dom.box.style.width = this.width + 'px';
-
- dom.content.style.left = props.content.left + 'px';
+ else {
+ box.style.top = '';
+ box.style.bottom = this.top + 'px';
}
};
@@ -6986,122 +6314,66 @@ function ItemRangeOverflow (parent, data, options, defaultOptions) {
}
};
- // define a private property _width, which is the with of the range box
- // adhering to the ranges start and end date. The property width has a
- // getter which returns the max of border width and content width
- this._width = 0;
- Object.defineProperty(this, 'width', {
- get: function () {
- return (this.props.content && this._width < this.props.content.width) ?
- this.props.content.width :
- this._width;
- },
-
- set: function (width) {
- this._width = width;
- }
- });
-
ItemRange.call(this, parent, data, options, defaultOptions);
}
ItemRangeOverflow.prototype = new ItemRange (null, null);
+ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
+
/**
- * Repaint the item
- * @return {Boolean} changed
+ * Reposition the item horizontally
+ * @Override
*/
-ItemRangeOverflow.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
- var dom = this.dom;
+ItemRangeOverflow.prototype.repositionX = function repositionX() {
+ var parentWidth = this.parent.width,
+ start = this.defaultOptions.toScreen(this.data.start),
+ end = this.defaultOptions.toScreen(this.data.end),
+ padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
+ contentLeft;
- if (!dom) {
- this._create();
- dom = this.dom;
- changed = true;
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
}
-
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
- }
-
- if (!dom.box.parentNode) {
- foreground.appendChild(dom.box);
- changed = true;
- }
-
- // update content
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.id);
- }
- changed = true;
- }
-
- this._repaintDeleteButton(dom.box);
- this._repaintDragLeft();
- this._repaintDragRight();
-
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.box.className = 'item rangeoverflow' + className;
- changed = true;
- }
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
}
- return changed;
-};
-
-/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
- */
-ItemRangeOverflow.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props;
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ contentLeft = Math.max(-start, 0);
- if (dom) {
- dom.box.style.top = this.top + 'px';
- dom.box.style.left = this.left + 'px';
- dom.box.style.width = this._width + 'px';
+ this.left = start;
+ var boxWidth = Math.max(end - start, 1);
+ this.width = (this.props.content.width < boxWidth) ?
+ boxWidth :
+ start + contentLeft + this.props.content.width;
- dom.content.style.left = props.content.left + 'px';
- }
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = boxWidth + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
};
/**
* @constructor Group
- * @param {GroupSet} parent
+ * @param {Panel} groupPanel
+ * @param {Panel} labelPanel
+ * @param {Panel} backgroundPanel
+ * @param {Panel} axisPanel
* @param {Number | String} groupId
* @param {Object} [options] Options to set initial property values
* // TODO: describe available options
* @extends Component
*/
-function Group (parent, groupId, options) {
+function Group (groupPanel, labelPanel, backgroundPanel, axisPanel, groupId, options) {
this.id = util.randomUUID();
- this.parent = parent;
+ this.groupPanel = groupPanel;
+ this.labelPanel = labelPanel;
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
this.groupId = groupId;
- this.itemset = null; // ItemSet
+ this.itemSet = null; // ItemSet
this.options = options || {};
this.options.top = 0;
@@ -7112,10 +6384,14 @@ function Group (parent, groupId, options) {
}
};
+ this.dom = {};
+
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
+
+ this._create();
}
Group.prototype = new Component();
@@ -7124,47 +6400,131 @@ Group.prototype = new Component();
Group.prototype.setOptions = Component.prototype.setOptions;
/**
- * Get the container element of the panel, which can be used by a child to
- * add its own widgets.
- * @returns {HTMLElement} container
+ * Create DOM elements for the group
+ * @private
+ */
+Group.prototype._create = function() {
+ var label = document.createElement('div');
+ label.className = 'vlabel';
+ this.dom.label = label;
+
+ var inner = document.createElement('div');
+ inner.className = 'inner';
+ label.appendChild(inner);
+ this.dom.inner = inner;
+};
+
+/**
+ * Set the group data for this group
+ * @param {Object} data Group data, can contain properties content and className
*/
-Group.prototype.getContainer = function () {
- return this.parent.getContainer();
+Group.prototype.setData = function setData(data) {
+ // update contents
+ var content = data && data.content;
+ if (content instanceof Element) {
+ this.dom.inner.appendChild(content);
+ }
+ else if (content != undefined) {
+ this.dom.inner.innerHTML = content;
+ }
+ else {
+ this.dom.inner.innerHTML = this.groupId;
+ }
+
+ // update className
+ var className = data && data.className;
+ if (className) {
+ util.addClassName(this.dom.label, className);
+ }
};
/**
- * Set item set for the group. The group will create a view on the itemset,
+ * Set item set for the group. The group will create a view on the itemSet,
* filtered by the groups id.
- * @param {DataSet | DataView} items
+ * @param {DataSet | DataView} itemsData
*/
-Group.prototype.setItems = function setItems(items) {
- if (this.itemset) {
+Group.prototype.setItems = function setItems(itemsData) {
+ if (this.itemSet) {
// remove current item set
- this.itemset.hide();
- this.itemset.setItems();
-
- this.parent.controller.remove(this.itemset);
- this.itemset = null;
+ this.itemSet.setItems();
+ this.itemSet.hide();
+ this.groupPanel.frame.removeChild(this.itemSet.getFrame());
+ this.itemSet = null;
}
- if (items) {
+ if (itemsData) {
var groupId = this.groupId;
- var itemsetOptions = Object.create(this.options);
- this.itemset = new ItemSet(this, null, itemsetOptions);
- this.itemset.setRange(this.parent.range);
+ var me = this;
+ var itemSetOptions = util.extend(this.options, {
+ height: function () {
+ // FIXME: setting height doesn't yet work
+ return Math.max(me.props.label.height, me.itemSet.height);
+ }
+ });
+ this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, itemSetOptions);
+ this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event
+ this.itemSet.parent = this;
+ this.groupPanel.frame.appendChild(this.itemSet.getFrame());
+
+ if (this.range) this.itemSet.setRange(this.range);
- this.view = new DataView(items, {
+ this.view = new DataView(itemsData, {
filter: function (item) {
return item.group == groupId;
}
});
- this.itemset.setItems(this.view);
+ this.itemSet.setItems(this.view);
+ }
+};
+
+/**
+ * hide the group, detach from DOM if needed
+ */
+Group.prototype.show = function show() {
+ if (!this.dom.label.parentNode) {
+ this.labelPanel.frame.appendChild(this.dom.label);
+ }
+
+ var itemSetFrame = this.itemSet && this.itemSet.getFrame();
+ if (itemSetFrame) {
+ if (itemSetFrame.parentNode) {
+ itemSetFrame.parentNode.removeChild(itemSetFrame);
+ }
+ this.groupPanel.frame.appendChild(itemSetFrame);
+
+ this.itemSet.show();
+ }
+};
+
+/**
+ * hide the group, detach from DOM if needed
+ */
+Group.prototype.hide = function hide() {
+ if (this.dom.label.parentNode) {
+ this.dom.label.parentNode.removeChild(this.dom.label);
+ }
+
+ if (this.itemSet) {
+ this.itemSet.hide();
+ }
- this.parent.controller.add(this.itemset);
+ var itemSetFrame = this.itemset && this.itemSet.getFrame();
+ if (itemSetFrame && itemSetFrame.parentNode) {
+ itemSetFrame.parentNode.removeChild(itemSetFrame);
}
};
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+Group.prototype.setRange = function (range) {
+ this.range = range;
+
+ if (this.itemSet) this.itemSet.setRange(range);
+};
+
/**
* Set selected items by their id. Replaces the current selection.
* Unknown id's are silently ignored.
@@ -7173,7 +6533,7 @@ Group.prototype.setItems = function setItems(items) {
* unselected.
*/
Group.prototype.setSelection = function setSelection(ids) {
- if (this.itemset) this.itemset.setSelection(ids);
+ if (this.itemSet) this.itemSet.setSelection(ids);
};
/**
@@ -7181,58 +6541,53 @@ Group.prototype.setSelection = function setSelection(ids) {
* @return {Array} ids The ids of the selected items
*/
Group.prototype.getSelection = function getSelection() {
- return this.itemset ? this.itemset.getSelection() : [];
+ return this.itemSet ? this.itemSet.getSelection() : [];
};
/**
- * Repaint the item
- * @return {Boolean} changed
+ * Repaint the group
+ * @return {boolean} Returns true if the component is resized
*/
Group.prototype.repaint = function repaint() {
- return false;
-};
+ var resized = false;
-/**
- * Reflow the item
- * @return {Boolean} resized
- */
-Group.prototype.reflow = function reflow() {
- var changed = 0,
- update = util.updateProperty;
+ this.show();
- changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
- changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
+ if (this.itemSet) {
+ resized = this.itemSet.repaint() || resized;
+ }
- // TODO: reckon with the height of the group label
+ // calculate inner size of the label
+ resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
+ resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
- if (this.label) {
- var inner = this.label.firstChild;
- changed += update(this.props.label, 'width', inner.clientWidth);
- changed += update(this.props.label, 'height', inner.clientHeight);
- }
- else {
- changed += update(this.props.label, 'width', 0);
- changed += update(this.props.label, 'height', 0);
- }
+ this.height = this.itemSet ? this.itemSet.height : 0;
+
+ this.dom.label.style.height = this.height + 'px';
- return (changed > 0);
+ return resized;
};
/**
* An GroupSet holds a set of groups
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
+ * @param {Panel} contentPanel Panel where the ItemSets will be created
+ * @param {Panel} labelPanel Panel where the labels will be created
+ * @param {Panel} backgroundPanel Panel where the vertical lines of box
+ * items are created
+ * @param {Panel} axisPanel Panel on the axis where the dots of box
+ * items will be created
* @param {Object} [options] See GroupSet.setOptions for the available
* options.
* @constructor GroupSet
* @extends Panel
*/
-function GroupSet(parent, depends, options) {
+function GroupSet(contentPanel, labelPanel, backgroundPanel, axisPanel, options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
+ this.contentPanel = contentPanel;
+ this.labelPanel = labelPanel;
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
this.options = options || {};
this.range = null; // Range or Object {start: number, end: number}
@@ -7240,6 +6595,7 @@ function GroupSet(parent, depends, options) {
this.groupsData = null; // DataSet with groups
this.groups = {}; // map with groups
+ this.groupIds = []; // list with ordered group ids
this.dom = {};
this.props = {
@@ -7248,10 +6604,7 @@ function GroupSet(parent, depends, options) {
}
};
- // TODO: implement right orientation of the labels
-
- // changes in groups are queued key/value map containing id/action
- this.queue = {};
+ // TODO: implement right orientation of the labels (left/right)
var me = this;
this.listeners = {
@@ -7265,10 +6618,40 @@ function GroupSet(parent, depends, options) {
me._onRemove(params.items);
}
};
+
+ // create HTML DOM
+ this._create();
}
GroupSet.prototype = new Panel();
+/**
+ * Create the HTML DOM elements for the GroupSet
+ * @private
+ */
+GroupSet.prototype._create = function _create () {
+ // TODO: reimplement groupSet DOM elements
+ var frame = document.createElement('div');
+ frame.className = 'groupset';
+ frame['timeline-groupset'] = this;
+ this.frame = frame;
+
+ this.labelSet = new Panel({
+ className: 'labelset',
+ width: '100%',
+ height: '100%'
+ });
+ this.labelPanel.appendChild(this.labelSet);
+};
+
+/**
+ * Get the frame element of component
+ * @returns {null} Get frame is not supported by GroupSet
+ */
+GroupSet.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
/**
* Set options for the GroupSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
@@ -7277,8 +6660,18 @@ GroupSet.prototype = new Panel();
*/
GroupSet.prototype.setOptions = Component.prototype.setOptions;
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
GroupSet.prototype.setRange = function (range) {
- // TODO: implement setRange
+ this.range = range;
+
+ for (var id in this.groups) {
+ if (this.groups.hasOwnProperty(id)) {
+ this.groups[id].setRange(range);
+ }
+ }
};
/**
@@ -7291,6 +6684,7 @@ GroupSet.prototype.setItems = function setItems(items) {
for (var id in this.groups) {
if (this.groups.hasOwnProperty(id)) {
var group = this.groups[id];
+ // TODO: every group will emit a change event, causing a lot of unnecessary repaints. improve this.
group.setItems(items);
}
}
@@ -7359,6 +6753,8 @@ GroupSet.prototype.setGroups = function setGroups(groups) {
ids = this.groupsData.getIds();
this._onAdd(ids);
}
+
+ this.emit('change');
};
/**
@@ -7412,313 +6808,117 @@ GroupSet.prototype.getSelection = function getSelection() {
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component was resized since previous repaint
*/
GroupSet.prototype.repaint = function repaint() {
- var changed = 0,
- i, id, group, label,
- update = util.updateProperty,
+ var i, id, group,
asSize = util.option.asSize,
- asElement = util.option.asElement,
+ asString = util.option.asString,
options = this.options,
- frame = this.dom.frame,
- labels = this.dom.labels,
- labelSet = this.dom.labelSet;
-
- // create frame
- if (!this.parent) {
- throw new Error('Cannot repaint groupset: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint groupset: parent has no container element');
- }
- if (!frame) {
- frame = document.createElement('div');
- frame.className = 'groupset';
- frame['timeline-groupset'] = this;
- this.dom.frame = frame;
-
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
- }
-
- changed += 1;
- }
- if (!frame.parentNode) {
- parentContainer.appendChild(frame);
- changed += 1;
- }
-
- // create labels
- var labelContainer = asElement(options.labelContainer);
- if (!labelContainer) {
- throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
- }
- if (!labels) {
- labels = document.createElement('div');
- labels.className = 'labels';
- this.dom.labels = labels;
- }
- if (!labelSet) {
- labelSet = document.createElement('div');
- labelSet.className = 'label-set';
- labels.appendChild(labelSet);
- this.dom.labelSet = labelSet;
- }
- if (!labels.parentNode || labels.parentNode != labelContainer) {
- if (labels.parentNode) {
- labels.parentNode.removeChild(labels.parentNode);
- }
- labelContainer.appendChild(labels);
- }
-
- // reposition frame
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
-
- // reposition labels
- changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
- changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
-
- var me = this,
- queue = this.queue,
- groups = this.groups,
- groupsData = this.groupsData;
-
- // show/hide added/changed/removed groups
- var ids = Object.keys(queue);
- if (ids.length) {
- ids.forEach(function (id) {
- var action = queue[id];
- var group = groups[id];
-
- //noinspection FallthroughInSwitchStatementJS
- switch (action) {
- case 'add':
- case 'update':
- if (!group) {
- var groupOptions = Object.create(me.options);
- util.extend(groupOptions, {
- height: null,
- maxHeight: null
- });
-
- group = new Group(me, id, groupOptions);
- group.setItems(me.itemsData); // attach items data
- groups[id] = group;
-
- me.controller.add(group);
- }
-
- // TODO: update group data
- group.data = groupsData.get(id);
-
- delete queue[id];
- break;
-
- case 'remove':
- if (group) {
- group.setItems(); // detach items data
- delete groups[id];
-
- me.controller.remove(group);
- }
-
- // update lists
- delete queue[id];
- break;
-
- default:
- console.log('Error: unknown action "' + action + '"');
- }
- });
-
- // the groupset depends on each of the groups
- //this.depends = this.groups; // TODO: gives a circular reference through the parent
-
- // TODO: apply dependencies of the groupset
-
- // update the top positions of the groups in the correct order
- var orderedGroups = this.groupsData.getIds({
- order: this.options.groupOrder
- });
- for (i = 0; i < orderedGroups.length; i++) {
- (function (group, prevGroup) {
- var top = 0;
- if (prevGroup) {
- top = function () {
- // TODO: top must reckon with options.maxHeight
- return prevGroup.top + prevGroup.height;
- }
- }
- group.setOptions({
- top: top
- });
- })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
- }
-
- // (re)create the labels
- while (labelSet.firstChild) {
- labelSet.removeChild(labelSet.firstChild);
- }
- for (i = 0; i < orderedGroups.length; i++) {
- id = orderedGroups[i];
- label = this._createLabel(id);
- labelSet.appendChild(label);
- }
-
- changed++;
- }
-
- // 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';
- }
- }
- }
-
- return (changed > 0);
-};
+ orientation = this.getOption('orientation'),
+ frame = this.frame,
+ resized = false,
+ groups = this.groups;
-/**
- * Create a label for group with given id
- * @param {Number} id
- * @return {Element} label
- * @private
- */
-GroupSet.prototype._createLabel = function(id) {
- var group = this.groups[id];
- var label = document.createElement('div');
- label.className = 'vlabel';
- var inner = document.createElement('div');
- inner.className = 'inner';
- label.appendChild(inner);
+ // repaint all groups in order
+ this.groupIds.forEach(function (id) {
+ var groupResized = groups[id].repaint();
+ resized = resized || groupResized;
+ });
- var content = group.data && group.data.content;
- if (content instanceof Element) {
- inner.appendChild(content);
- }
- else if (content != undefined) {
- inner.innerHTML = content;
+ // reposition the labels and calculate the maximum label width
+ var maxWidth = 0;
+ for (id in groups) {
+ if (groups.hasOwnProperty(id)) {
+ group = groups[id];
+ maxWidth = Math.max(maxWidth, group.props.label.width);
+ }
}
+ resized = util.updateProperty(this.props.labels, 'width', maxWidth) || resized;
- var className = group.data && group.data.className;
- if (className) {
- util.addClassName(label, className);
+ // recalculate the height of the groupset, and recalculate top positions of the groups
+ var fixedHeight = (asSize(options.height) != null);
+ var height;
+ if (!fixedHeight) {
+ // height is not specified, calculate the sum of the height of all groups
+ height = 0;
+
+ this.groupIds.forEach(function (id) {
+ var group = groups[id];
+ group.top = height;
+ if (group.itemSet) group.itemSet.top = group.top; // TODO: this is an ugly hack
+ height += group.height;
+ });
}
- group.label = label; // TODO: not so nice, parking labels in the group this way!!!
+ // update classname
+ frame.className = 'groupset' + (options.className ? (' ' + asString(options.className)) : '');
- return label;
+ // calculate actual size and position
+ this.top = frame.offsetTop;
+ this.left = frame.offsetLeft;
+ this.width = frame.offsetWidth;
+ this.height = height;
+
+ return resized;
};
/**
- * Get container element
- * @return {HTMLElement} container
+ * Update the groupIds. Requires a repaint afterwards
+ * @private
*/
-GroupSet.prototype.getContainer = function getContainer() {
- return this.dom.frame;
+GroupSet.prototype._updateGroupIds = function () {
+ // reorder the groups
+ this.groupIds = this.groupsData.getIds({
+ order: this.options.groupOrder
+ });
+
+ // hide the groups now, they will be shown again in the next repaint
+ // in correct order
+ var groups = this.groups;
+ this.groupIds.forEach(function (id) {
+ groups[id].hide();
+ });
};
/**
* Get the width of the group labels
* @return {Number} width
*/
-GroupSet.prototype.getLabelsWidth = function getContainer() {
+GroupSet.prototype.getLabelsWidth = function getLabelsWidth() {
return this.props.labels.width;
};
-/**
- * Reflow the component
- * @return {Boolean} resized
- */
-GroupSet.prototype.reflow = function reflow() {
- var changed = 0,
- id, group,
- options = this.options,
- update = util.updateProperty,
- asNumber = util.option.asNumber,
- asSize = util.option.asSize,
- frame = this.dom.frame;
-
- if (frame) {
- var maxHeight = asNumber(options.maxHeight);
- var fixedHeight = (asSize(options.height) != null);
- var height;
- if (fixedHeight) {
- height = frame.offsetHeight;
- }
- else {
- // height is not specified, calculate the sum of the height of all groups
- height = 0;
-
- for (id in this.groups) {
- if (this.groups.hasOwnProperty(id)) {
- group = this.groups[id];
- height += group.height;
- }
- }
- }
- if (maxHeight != null) {
- height = Math.min(height, maxHeight);
- }
- changed += update(this, 'height', height);
-
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
- }
-
- // calculate the maximum width of the labels
- var width = 0;
- for (id in this.groups) {
- if (this.groups.hasOwnProperty(id)) {
- group = this.groups[id];
- var labelWidth = group.props && group.props.label && group.props.label.width || 0;
- width = Math.max(width, labelWidth);
- }
- }
- changed += update(this.props.labels, 'width', width);
-
- return (changed > 0);
-};
-
/**
* Hide the component from the DOM
- * @return {Boolean} changed
*/
GroupSet.prototype.hide = function hide() {
- if (this.dom.frame && this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
- return true;
- }
- else {
- return false;
+ // hide labelset
+ this.labelPanel.removeChild(this.labelSet);
+
+ // hide each of the groups
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].hide();
+ }
}
};
/**
* Show the component in the DOM (when not already visible).
- * A repaint will be executed when the component is not visible
* @return {Boolean} changed
*/
GroupSet.prototype.show = function show() {
- if (!this.dom.frame || !this.dom.frame.parentNode) {
- return this.repaint();
+ // show label set
+ if (!this.labelPanel.hasChild(this.labelSet)) {
+ this.labelPanel.removeChild(this.labelSet);
}
- else {
- return false;
+
+ // show each of the groups
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].show();
+ }
}
};
@@ -7728,7 +6928,7 @@ GroupSet.prototype.show = function show() {
* @private
*/
GroupSet.prototype._onUpdate = function _onUpdate(ids) {
- this._toQueue(ids, 'update');
+ this._onAdd(ids);
};
/**
@@ -7737,7 +6937,31 @@ GroupSet.prototype._onUpdate = function _onUpdate(ids) {
* @private
*/
GroupSet.prototype._onAdd = function _onAdd(ids) {
- this._toQueue(ids, 'add');
+ var me = this;
+
+ ids.forEach(function (id) {
+ var group = me.groups[id];
+ if (!group) {
+ var groupOptions = Object.create(me.options);
+ util.extend(groupOptions, {
+ height: null
+ });
+
+ group = new Group(me, me.labelSet, me.backgroundPanel, me.axisPanel, id, groupOptions);
+ group.on('change', me.emit.bind(me, 'change')); // propagate change event
+ group.setRange(me.range);
+ group.setItems(me.itemsData); // attach items data
+ me.groups[id] = group;
+ group.parent = me;
+ }
+
+ // update group data
+ group.setData(me.groupsData.get(id));
+ });
+
+ this._updateGroupIds();
+
+ this.emit('change');
};
/**
@@ -7746,50 +6970,61 @@ GroupSet.prototype._onAdd = function _onAdd(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;
+ var groups = this.groups;
ids.forEach(function (id) {
- queue[id] = action;
+ var group = groups[id];
+
+ if (group) {
+ group.setItems(); // detach items data
+ group.hide(); // FIXME: for some reason when doing setItems after hide, setItems again makes the label visible
+ delete groups[id];
+ }
});
- if (this.controller) {
- //this.requestReflow();
- this.requestRepaint();
- }
+ this._updateGroupIds();
+
+ this.emit('change');
};
/**
- * Find the Group from an event target:
+ * Find the GroupSet from an event target:
* searches for the attribute 'timeline-groupset' in the event target's element
* tree, then finds the right group in this groupset
* @param {Event} event
* @return {Group | null} group
*/
-GroupSet.groupFromTarget = function groupFromTarget (event) {
- var groupset,
- target = event.target;
-
+GroupSet.groupSetFromTarget = function groupSetFromTarget (event) {
+ var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-groupset')) {
- groupset = target['timeline-groupset'];
- break;
+ return target['timeline-groupset'];
}
target = target.parentNode;
}
- if (groupset) {
- for (var groupId in groupset.groups) {
- if (groupset.groups.hasOwnProperty(groupId)) {
- var group = groupset.groups[groupId];
- if (group.itemset && ItemSet.itemSetFromTarget(event) == group.itemset) {
+ return null;
+};
+
+/**
+ * Find the Group from an event target:
+ * searches for the two elements having attributes 'timeline-groupset' and
+ * 'timeline-itemset' in the event target's element, then finds the right group.
+ * @param {Event} event
+ * @return {Group | null} group
+ */
+GroupSet.groupFromTarget = function groupFromTarget (event) {
+ // find the groupSet
+ var groupSet = GroupSet.groupSetFromTarget(event);
+
+ // find the ItemSet
+ var itemSet = ItemSet.itemSetFromTarget(event);
+
+ // find the right group
+ if (groupSet && itemSet) {
+ for (var groupId in groupSet.groups) {
+ if (groupSet.groups.hasOwnProperty(groupId)) {
+ var group = groupSet.groups[groupId];
+ if (group.itemSet == itemSet) {
return group;
}
}
@@ -7807,10 +7042,14 @@ GroupSet.groupFromTarget = function groupFromTarget (event) {
* @constructor
*/
function Timeline (container, items, options) {
+ // validate arguments
+ if (!container) throw new Error('No container element provided');
+
var me = this;
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.options = {
orientation: 'bottom',
+ direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true,
editable: false,
selectable: true,
@@ -7828,6 +7067,14 @@ function Timeline (container, items, options) {
showCurrentTime: false,
showCustomTime: false,
+ type: 'box',
+ align: 'center',
+ margin: {
+ axis: 20,
+ item: 10
+ },
+ padding: 5,
+
onAdd: function (item, callback) {
callback(item);
},
@@ -7839,109 +7086,194 @@ function Timeline (container, items, options) {
},
onRemove: function (item, callback) {
callback(item);
- }
- };
+ },
- // controller
- this.controller = new Controller();
+ toScreen: me._toScreen.bind(me),
+ toTime: me._toTime.bind(me)
+ };
// root panel
- if (!container) {
- throw new Error('No container element provided');
- }
- var rootOptions = Object.create(this.options);
- rootOptions.height = function () {
- // TODO: change to height
- if (me.options.height) {
- // fixed height
- return me.options.height;
- }
- else {
- // auto height
- return (me.timeaxis.height + me.content.height) + 'px';
+ var rootOptions = util.extend(Object.create(this.options), {
+ height: function () {
+ if (me.options.height) {
+ // fixed height
+ return me.options.height;
+ }
+ else {
+ // auto height
+ // TODO: implement a css based solution to automatically have the right hight
+ return (me.timeAxis.height + me.contentPanel.height) + 'px';
+ }
}
- };
+ });
this.rootPanel = new RootPanel(container, rootOptions);
- this.controller.add(this.rootPanel);
// single select (or unselect) when tapping an item
- this.controller.on('tap', this._onSelectItem.bind(this));
+ this.rootPanel.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
- this.controller.on('hold', this._onMultiSelectItem.bind(this));
+ this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
// add item on doubletap
- this.controller.on('doubletap', this._onAddItem.bind(this));
+ this.rootPanel.on('doubletap', this._onAddItem.bind(this));
- // item panel
- var itemOptions = Object.create(this.options);
- itemOptions.left = function () {
- return me.labelPanel.width;
- };
- itemOptions.width = function () {
- return me.rootPanel.width - me.labelPanel.width;
- };
- itemOptions.top = null;
- itemOptions.height = null;
- this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
- this.controller.add(this.itemPanel);
-
- // label panel
- var labelOptions = Object.create(this.options);
- labelOptions.top = null;
- labelOptions.left = null;
- labelOptions.height = null;
- labelOptions.width = function () {
- if (me.content && typeof me.content.getLabelsWidth === 'function') {
- return me.content.getLabelsWidth();
- }
- else {
- return 0;
+ // side panel
+ var sideOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (sideOptions.orientation == 'top') ? '0' : '';
+ },
+ bottom: function () {
+ return (sideOptions.orientation == 'top') ? '' : '0';
+ },
+ left: '0',
+ right: null,
+ height: '100%',
+ width: function () {
+ if (me.groupSet) {
+ return me.groupSet.getLabelsWidth();
+ }
+ else {
+ return 0;
+ }
+ },
+ className: function () {
+ return 'side' + (me.groupsData ? '' : ' hidden');
}
- };
- this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
- this.controller.add(this.labelPanel);
+ });
+ this.sidePanel = new Panel(sideOptions);
+ this.rootPanel.appendChild(this.sidePanel);
+
+ // main panel (contains time axis and itemsets)
+ var mainOptions = util.extend(Object.create(this.options), {
+ left: function () {
+ // we align left to enable a smooth resizing of the window
+ return me.sidePanel.width;
+ },
+ right: null,
+ height: '100%',
+ width: function () {
+ return me.rootPanel.width - me.sidePanel.width;
+ },
+ className: 'main'
+ });
+ this.mainPanel = new Panel(mainOptions);
+ this.rootPanel.appendChild(this.mainPanel);
// range
+ // TODO: move range inside rootPanel?
var rangeOptions = Object.create(this.options);
- this.range = new Range(rangeOptions);
+ this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
this.range.setRange(
now.clone().add('days', -3).valueOf(),
now.clone().add('days', 4).valueOf()
);
-
- this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
- this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function (properties) {
- var force = true;
- me.controller.emit('rangechange', properties);
- me.controller.emit('request-reflow', force);
+ me.rootPanel.repaint();
+ me.emit('rangechange', properties);
});
this.range.on('rangechanged', function (properties) {
- var force = true;
- me.controller.emit('rangechanged', properties);
- me.controller.emit('request-reflow', force);
+ me.rootPanel.repaint();
+ me.emit('rangechanged', properties);
+ });
+
+ // panel with time axis
+ var timeAxisOptions = util.extend(Object.create(rootOptions), {
+ range: this.range,
+ left: null,
+ top: null,
+ width: null,
+ height: null
+ });
+ this.timeAxis = new TimeAxis(timeAxisOptions);
+ this.timeAxis.setRange(this.range);
+ this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
+ this.mainPanel.appendChild(this.timeAxis);
+
+ // content panel (contains itemset(s))
+ var contentOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: null,
+ width: null,
+ className: 'content'
+ });
+ this.contentPanel = new Panel(contentOptions);
+ this.mainPanel.appendChild(this.contentPanel);
+
+ // content panel (contains the vertical lines of box items)
+ var backgroundOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: function () {
+ return me.contentPanel.height;
+ },
+ width: null,
+ className: 'background'
+ });
+ this.backgroundPanel = new Panel(backgroundOptions);
+ this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
+
+ // panel with axis holding the dots of item boxes
+ var axisPanelOptions = util.extend(Object.create(rootOptions), {
+ left: 0,
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ width: '100%',
+ height: 0,
+ className: 'axis'
});
+ this.axisPanel = new Panel(axisPanelOptions);
+ this.mainPanel.appendChild(this.axisPanel);
- // time axis
- var timeaxisOptions = Object.create(rootOptions);
- timeaxisOptions.range = this.range;
- timeaxisOptions.left = null;
- timeaxisOptions.top = null;
- timeaxisOptions.width = '100%';
- timeaxisOptions.height = null;
- this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
- this.timeaxis.setRange(this.range);
- this.controller.add(this.timeaxis);
- this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
+ // content panel (contains itemset(s))
+ var sideContentOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: null,
+ width: null,
+ className: 'side-content'
+ });
+ this.sideContentPanel = new Panel(sideContentOptions);
+ this.sidePanel.appendChild(this.sideContentPanel);
// current time bar
- this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
- this.controller.add(this.currenttime);
+ // Note: time bar will be attached in this.setOptions when selected
+ this.currentTime = new CurrentTime(this.range, rootOptions);
// custom time bar
- this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
- this.controller.add(this.customtime);
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(rootOptions);
+ this.customTime.on('timechange', function (time) {
+ me.emit('timechange', time);
+ });
+ this.customTime.on('timechanged', function (time) {
+ me.emit('timechanged', time);
+ });
+
+ this.itemSet = null;
+ this.groupSet = null;
// create groupset
this.setGroups(null);
@@ -7960,24 +7292,8 @@ function Timeline (container, items, options) {
}
}
-/**
- * Add an event listener to the timeline
- * @param {String} event Available events: select, rangechange, rangechanged,
- * timechange, timechanged
- * @param {function} callback
- */
-Timeline.prototype.on = function on (event, callback) {
- this.controller.on(event, callback);
-};
-
-/**
- * Add an event listener from the timeline
- * @param {String} event
- * @param {function} callback
- */
-Timeline.prototype.off = function off (event, callback) {
- this.controller.off(event, callback);
-};
+// turn Timeline into an event emitter
+Emitter(Timeline.prototype);
/**
* Set options
@@ -8009,8 +7325,34 @@ Timeline.prototype.setOptions = function (options) {
}).bind(this);
['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
- this.controller.reflow();
- this.controller.repaint();
+ // add/remove the current time bar
+ if (this.options.showCurrentTime) {
+ if (!this.mainPanel.hasChild(this.currentTime)) {
+ this.mainPanel.appendChild(this.currentTime);
+ this.currentTime.start();
+ }
+ }
+ else {
+ if (this.mainPanel.hasChild(this.currentTime)) {
+ this.currentTime.stop();
+ this.mainPanel.removeChild(this.currentTime);
+ }
+ }
+
+ // add/remove the custom time bar
+ if (this.options.showCustomTime) {
+ if (!this.mainPanel.hasChild(this.customTime)) {
+ this.mainPanel.appendChild(this.customTime);
+ }
+ }
+ else {
+ if (this.mainPanel.hasChild(this.customTime)) {
+ this.mainPanel.removeChild(this.customTime);
+ }
+ }
+
+ // repaint everything
+ this.rootPanel.repaint();
};
/**
@@ -8018,11 +7360,11 @@ Timeline.prototype.setOptions = function (options) {
* @param {Date} time
*/
Timeline.prototype.setCustomTime = function (time) {
- if (!this.customtime) {
+ if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
- this.customtime.setCustomTime(time);
+ this.customTime.setCustomTime(time);
};
/**
@@ -8030,11 +7372,11 @@ Timeline.prototype.setCustomTime = function (time) {
* @return {Date} customTime
*/
Timeline.prototype.getCustomTime = function() {
- if (!this.customtime) {
+ if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
- return this.customtime.getCustomTime();
+ return this.customTime.getCustomTime();
};
/**
@@ -8064,7 +7406,7 @@ Timeline.prototype.setItems = function(items) {
// set items
this.itemsData = newDataSet;
- this.content.setItems(newDataSet);
+ (this.itemSet || this.groupSet).setItems(newDataSet);
if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
// apply the data range as range
@@ -8100,78 +7442,62 @@ Timeline.prototype.setItems = function(items) {
/**
* Set groups
- * @param {vis.DataSet | Array | google.visualization.DataTable} groups
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groupSet
*/
-Timeline.prototype.setGroups = function(groups) {
+Timeline.prototype.setGroups = function(groupSet) {
var me = this;
- this.groupsData = groups;
+ this.groupsData = groupSet;
+
+ // create options for the itemset or groupset
+ var options = util.extend(Object.create(this.options), {
+ top: null,
+ bottom: null,
+ right: null,
+ left: null,
+ width: null,
+ height: null
+ });
- // switch content type between ItemSet or GroupSet when needed
- var Type = this.groupsData ? GroupSet : ItemSet;
- if (!(this.content instanceof Type)) {
- // remove old content set
- if (this.content) {
- this.content.hide();
- if (this.content.setItems) {
- this.content.setItems(); // disconnect from items
- }
- if (this.content.setGroups) {
- this.content.setGroups(); // disconnect from groups
- }
- this.controller.remove(this.content);
- }
+ if (this.groupsData) {
+ // Create a GroupSet
- // create new content set
- var options = Object.create(this.options);
- util.extend(options, {
- top: function () {
- if (me.options.orientation == 'top') {
- return me.timeaxis.height;
- }
- else {
- return me.itemPanel.height - me.timeaxis.height - me.content.height;
- }
- },
- left: null,
- width: '100%',
- height: function () {
- if (me.options.height) {
- // fixed height
- return me.itemPanel.height - me.timeaxis.height;
- }
- else {
- // auto height
- return null;
- }
- },
- maxHeight: function () {
- // TODO: change maxHeight to be a css string like '100%' or '300px'
- if (me.options.maxHeight) {
- if (!util.isNumber(me.options.maxHeight)) {
- throw new TypeError('Number expected for property maxHeight');
- }
- return me.options.maxHeight - me.timeaxis.height;
- }
- else {
- return null;
- }
- },
- labelContainer: function () {
- return me.labelPanel.getContainer();
- }
- });
+ // remove itemset if existing
+ if (this.itemSet) {
+ this.itemSet.hide(); // TODO: not so nice having to hide here
+ this.contentPanel.removeChild(this.itemSet);
+ this.itemSet.setItems(); // disconnect from itemset
+ this.itemSet = null;
+ }
- this.content = new Type(this.itemPanel, [this.timeaxis], options);
- if (this.content.setRange) {
- this.content.setRange(this.range);
+ // create new GroupSet when needed
+ if (!this.groupSet) {
+ this.groupSet = new GroupSet(this.contentPanel, this.sideContentPanel, this.backgroundPanel, this.axisPanel, options);
+ this.groupSet.on('change', this.rootPanel.repaint.bind(this.rootPanel));
+ this.groupSet.setRange(this.range);
+ this.groupSet.setItems(this.itemsData);
+ this.groupSet.setGroups(this.groupsData);
+ this.contentPanel.appendChild(this.groupSet);
}
- if (this.content.setItems) {
- this.content.setItems(this.itemsData);
+ else {
+ this.groupSet.setGroups(this.groupsData);
}
- if (this.content.setGroups) {
- this.content.setGroups(this.groupsData);
+ }
+ else {
+ // ItemSet
+ if (this.groupSet) {
+ this.groupSet.hide(); // TODO: not so nice having to hide here
+ //this.groupSet.setGroups(); // disconnect from groupset
+ this.groupSet.setItems(); // disconnect from itemset
+ this.contentPanel.removeChild(this.groupSet);
+ this.groupSet = null;
}
- this.controller.add(this.content);
+
+ // create new items
+ this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, options);
+ this.itemSet.setRange(this.range);
+ this.itemSet.setItems(this.itemsData);
+ this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
+ this.contentPanel.appendChild(this.itemSet);
}
};
@@ -8222,7 +7548,9 @@ Timeline.prototype.getItemRange = function getItemRange() {
* unselected.
*/
Timeline.prototype.setSelection = function setSelection (ids) {
- if (this.content) this.content.setSelection(ids);
+ var itemOrGroupSet = (this.itemSet || this.groupSet);
+
+ if (itemOrGroupSet) itemOrGroupSet.setSelection(ids);
};
/**
@@ -8230,7 +7558,9 @@ Timeline.prototype.setSelection = function setSelection (ids) {
* @return {Array} ids The ids of the selected items
*/
Timeline.prototype.getSelection = function getSelection() {
- return this.content ? this.content.getSelection() : [];
+ var itemOrGroupSet = (this.itemSet || this.groupSet);
+
+ return itemOrGroupSet ? itemOrGroupSet.getSelection() : [];
};
/**
@@ -8239,6 +7569,8 @@ Timeline.prototype.getSelection = function getSelection() {
* @param {Date | Number | String} [start] Start date of visible window
* @param {Date | Number | String} [end] End date of visible window
*/
+// TODO: implement support for setWindow({start: ..., end: ...})
+// TODO: rename setWindow to setRange?
Timeline.prototype.setWindow = function setWindow(start, end) {
this.range.setRange(start, end);
};
@@ -8247,6 +7579,7 @@ Timeline.prototype.setWindow = function setWindow(start, end) {
* Get the visible window
* @return {{start: Date, end: Date}} Visible range
*/
+// TODO: rename getWindow to getRange?
Timeline.prototype.getWindow = function setWindow() {
var range = this.range.getRange();
return {
@@ -8271,14 +7604,20 @@ Timeline.prototype._onSelectItem = function (event) {
return;
}
- var item = ItemSet.itemFromTarget(event);
+ var oldSelection = this.getSelection();
+ var item = ItemSet.itemFromTarget(event);
var selection = item ? [item.id] : [];
this.setSelection(selection);
- this.controller.emit('select', {
- items: this.getSelection()
- });
+ var newSelection = this.getSelection();
+
+ // if selection is changed, emit a select event
+ if (!util.equalArray(oldSelection, newSelection)) {
+ this.emit('select', {
+ items: this.getSelection()
+ });
+ }
event.stopPropagation();
};
@@ -8311,7 +7650,7 @@ Timeline.prototype._onAddItem = function (event) {
var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
var x = event.gesture.center.pageX - xAbs;
var newItem = {
- start: this.timeaxis.snap(this._toTime(x)),
+ start: this.timeAxis.snap(this._toTime(x)),
content: 'new item'
};
@@ -8327,15 +7666,7 @@ Timeline.prototype._onAddItem = function (event) {
this.options.onAdd(newItem, function (item) {
if (item) {
me.itemsData.add(newItem);
-
- // select the created item after it is repainted
- me.controller.once('repaint', function () {
- me.setSelection([id]);
-
- me.controller.emit('select', {
- items: me.getSelection()
- });
- }.bind(me));
+ // TODO: need to trigger a repaint?
}
});
}
@@ -8367,7 +7698,7 @@ Timeline.prototype._onMultiSelectItem = function (event) {
}
this.setSelection(selection);
- this.controller.emit('select', {
+ this.emit('select', {
items: this.getSelection()
});
@@ -8382,7 +7713,7 @@ Timeline.prototype._onMultiSelectItem = function (event) {
* @private
*/
Timeline.prototype._toTime = function _toTime(x) {
- var conversion = this.range.conversion(this.content.width);
+ var conversion = this.range.conversion(this.mainPanel.width);
return new Date(x / conversion.scale + conversion.offset);
};
@@ -8394,7 +7725,7 @@ Timeline.prototype._toTime = function _toTime(x) {
* @private
*/
Timeline.prototype._toScreen = function _toScreen(time) {
- var conversion = this.range.conversion(this.content.width);
+ var conversion = this.range.conversion(this.mainPanel.width);
return (time.valueOf() - conversion.offset) * conversion.scale;
};
@@ -18148,7 +17479,6 @@ Graph.prototype.storePosition = function() {
var vis = {
util: util,
- Controller: Controller,
DataSet: DataSet,
DataView: DataView,
Range: Range,
@@ -19799,8 +19129,8 @@ else {
}
})(this);
},{}],4:[function(require,module,exports){
-//! moment.js
-//! version : 2.5.1
+var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
+//! version : 2.6.0
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
@@ -19812,8 +19142,10 @@ else {
************************************/
var moment,
- VERSION = "2.5.1",
- global = this,
+ VERSION = "2.6.0",
+ // the global-scope this is NOT the global object in Node.js
+ globalScope = typeof global !== 'undefined' ? global : this,
+ oldGlobalMoment,
round = Math.round,
i,
@@ -19842,7 +19174,7 @@ else {
},
// check for nodeJS
- hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
+ hasModule = (typeof module !== 'undefined' && module.exports),
// ASP.NET json date format regex
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
@@ -19853,7 +19185,7 @@ else {
isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
// format tokens
- formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
// parsing token regexes
@@ -19866,6 +19198,7 @@ else {
parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
parseTokenT = /T/i, // T (ISO separator)
parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+ parseTokenOrdinal = /\d{1,2}/,
//strict parsing regexes
parseTokenOneDigit = /\d/, // 0 - 9
@@ -19891,7 +19224,7 @@ else {
// iso time formats and regexes
isoTimes = [
- ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
+ ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
['HH:mm', /(T| )\d\d:\d\d/],
['HH', /(T| )\d\d/]
@@ -19922,6 +19255,7 @@ else {
w : 'week',
W : 'isoWeek',
M : 'month',
+ Q : 'quarter',
y : 'year',
DDD : 'dayOfYear',
e : 'weekday',
@@ -20097,6 +19431,23 @@ else {
};
}
+ function deprecate(msg, fn) {
+ var firstTime = true;
+ function printMsg() {
+ if (moment.suppressDeprecationWarnings === false &&
+ typeof console !== 'undefined' && console.warn) {
+ console.warn("Deprecation warning: " + msg);
+ }
+ }
+ return extend(function () {
+ if (firstTime) {
+ printMsg();
+ firstTime = false;
+ }
+ return fn.apply(this, arguments);
+ }, fn);
+ }
+
function padToken(func, count) {
return function (a) {
return leftZeroFill(func.call(this, a), count);
@@ -20137,6 +19488,7 @@ else {
function Duration(duration) {
var normalizedInput = normalizeObjectUnits(duration),
years = normalizedInput.year || 0,
+ quarters = normalizedInput.quarter || 0,
months = normalizedInput.month || 0,
weeks = normalizedInput.week || 0,
days = normalizedInput.day || 0,
@@ -20158,6 +19510,7 @@ else {
// which months you are are talking about, so we have to store
// it separately.
this._months = +months +
+ quarters * 3 +
years * 12;
this._data = {};
@@ -20220,34 +19573,23 @@ else {
}
// helper function for _.addTime and _.subtractTime
- function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
+ function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
var milliseconds = duration._milliseconds,
days = duration._days,
- months = duration._months,
- minutes,
- hours;
+ months = duration._months;
+ updateOffset = updateOffset == null ? true : updateOffset;
if (milliseconds) {
mom._d.setTime(+mom._d + milliseconds * isAdding);
}
- // store the minutes and hours so we can restore them
- if (days || months) {
- minutes = mom.minute();
- hours = mom.hour();
- }
if (days) {
- mom.date(mom.date() + days * isAdding);
+ rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
}
if (months) {
- mom.month(mom.month() + months * isAdding);
+ rawMonthSetter(mom, rawGetter(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);
+ if (updateOffset) {
+ moment.updateOffset(mom, days || months);
}
}
@@ -20362,6 +19704,10 @@ else {
return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
}
+ function weeksInYear(year, dow, doy) {
+ return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
+ }
+
function daysInYear(year) {
return isLeapYear(year) ? 366 : 365;
}
@@ -20752,6 +20098,8 @@ else {
function getParseRegexForToken(token, config) {
var a, strict = config._strict;
switch (token) {
+ case 'Q':
+ return parseTokenOneDigit;
case 'DDDD':
return parseTokenThreeDigits;
case 'YYYY':
@@ -20820,6 +20168,8 @@ else {
case 'e':
case 'E':
return parseTokenOneOrTwoDigits;
+ case 'Do':
+ return parseTokenOrdinal;
default :
a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
return a;
@@ -20841,6 +20191,12 @@ else {
var a, datePartArray = config._a;
switch (token) {
+ // QUARTER
+ case 'Q':
+ if (input != null) {
+ datePartArray[MONTH] = (toInt(input) - 1) * 3;
+ }
+ break;
// MONTH
case 'M' : // fall through to MM
case 'MM' :
@@ -20865,6 +20221,11 @@ else {
datePartArray[DATE] = toInt(input);
}
break;
+ case 'Do' :
+ if (input != null) {
+ datePartArray[DATE] = toInt(parseInt(input, 10));
+ }
+ break;
// DAY OF YEAR
case 'DDD' : // fall through to DDDD
case 'DDDD' :
@@ -20875,7 +20236,7 @@ else {
break;
// YEAR
case 'YY' :
- datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+ datePartArray[YEAR] = moment.parseTwoDigitYear(input);
break;
case 'YYYY' :
case 'YYYYY' :
@@ -20964,9 +20325,9 @@ else {
//compute day of the year from weeks and weekdays
if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
fixYear = function (val) {
- var int_val = parseInt(val, 10);
+ var intVal = parseInt(val, 10);
return val ?
- (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
+ (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
(config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
};
@@ -21202,7 +20563,7 @@ else {
makeDateFromStringAndFormat(config);
}
else {
- config._d = new Date(string);
+ moment.createFromInputFallback(config);
}
}
@@ -21223,8 +20584,11 @@ else {
config._d = new Date(+input);
} else if (typeof(input) === 'object') {
dateFromObject(config);
- } else {
+ } else if (typeof(input) === 'number') {
+ // from milliseconds
config._d = new Date(input);
+ } else {
+ moment.createFromInputFallback(config);
}
}
@@ -21351,7 +20715,7 @@ else {
var input = config._i,
format = config._f;
- if (input === null) {
+ if (input === null || (format === undefined && input === '')) {
return moment.invalid({nullInput: true});
}
@@ -21397,6 +20761,17 @@ else {
return makeMoment(c);
};
+ moment.suppressDeprecationWarnings = false;
+
+ moment.createFromInputFallback = deprecate(
+ "moment construction falls back to js Date. This is " +
+ "discouraged and will be removed in upcoming major " +
+ "release. Please refer to " +
+ "https://github.com/moment/moment/issues/1407 for more info.",
+ function (config) {
+ config._d = new Date(config._i);
+ });
+
// creating with utc
moment.utc = function (input, format, lang, strict) {
var c;
@@ -21493,6 +20868,10 @@ else {
// default format
moment.defaultFormat = isoFormat;
+ // Plugins that add properties should also add the key here (null value),
+ // so we can properly clone ourselves.
+ moment.momentProperties = momentProperties;
+
// This function will be called whenever a moment is mutated.
// It is intended to keep the offset in sync with the timezone.
moment.updateOffset = function () {};
@@ -21556,8 +20935,12 @@ else {
return m;
};
- moment.parseZone = function (input) {
- return moment(input).parseZone();
+ moment.parseZone = function () {
+ return moment.apply(null, arguments).parseZone();
+ };
+
+ moment.parseTwoDigitYear = function (input) {
+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
};
/************************************
@@ -21744,29 +21127,7 @@ else {
}
},
- month : function (input) {
- var utc = this._isUTC ? 'UTC' : '',
- dayOfMonth;
-
- if (input != null) {
- if (typeof input === 'string') {
- input = this.lang().monthsParse(input);
- if (typeof input !== 'number') {
- return this;
- }
- }
-
- dayOfMonth = this.date();
- this.date(1);
- this._d['set' + utc + 'Month'](input);
- this.date(Math.min(dayOfMonth, this.daysInMonth()));
-
- moment.updateOffset(this);
- return this;
- } else {
- return this._d['get' + utc + 'Month']();
- }
- },
+ month : makeAccessor('Month', true),
startOf: function (units) {
units = normalizeUnits(units);
@@ -21776,6 +21137,7 @@ else {
case 'year':
this.month(0);
/* falls through */
+ case 'quarter':
case 'month':
this.date(1);
/* falls through */
@@ -21802,6 +21164,11 @@ else {
this.isoWeekday(1);
}
+ // quarters are also special
+ if (units === 'quarter') {
+ this.month(Math.floor(this.month() / 3) * 3);
+ }
+
return this;
},
@@ -21835,7 +21202,17 @@ else {
return other > this ? this : other;
},
- zone : function (input) {
+ // keepTime = true means only change the timezone, without affecting
+ // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
+ // It is possible that 5:31:26 doesn't exist int zone +0200, so we
+ // adjust the time as needed, to be valid.
+ //
+ // Keeping the time actually adds/subtracts (one hour)
+ // from the actual represented time. That is why we call updateOffset
+ // a second time. In case it wants us to change the offset again
+ // _changeInProgress == true case, then we have to adjust, because
+ // there is no such time in the given timezone.
+ zone : function (input, keepTime) {
var offset = this._offset || 0;
if (input != null) {
if (typeof input === "string") {
@@ -21847,7 +21224,14 @@ else {
this._offset = input;
this._isUTC = true;
if (offset !== input) {
- addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
+ if (!keepTime || this._changeInProgress) {
+ addOrSubtractDurationFromMoment(this,
+ moment.duration(offset - input, 'm'), 1, false);
+ } else if (!this._changeInProgress) {
+ this._changeInProgress = true;
+ moment.updateOffset(this, true);
+ this._changeInProgress = null;
+ }
}
} else {
return this._isUTC ? offset : this._d.getTimezoneOffset();
@@ -21892,8 +21276,8 @@ else {
return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
},
- quarter : function () {
- return Math.ceil((this.month() + 1.0) / 3.0);
+ quarter : function (input) {
+ return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
},
weekYear : function (input) {
@@ -21928,6 +21312,15 @@ else {
return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
},
+ isoWeeksInYear : function () {
+ return weeksInYear(this.year(), 1, 4);
+ },
+
+ weeksInYear : function () {
+ var weekInfo = this._lang._week;
+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+ },
+
get : function (units) {
units = normalizeUnits(units);
return this[units]();
@@ -21954,33 +21347,68 @@ else {
}
});
- // helper for adding shortcuts
- function makeGetterAndSetter(name, key) {
- moment.fn[name] = moment.fn[name + 's'] = function (input) {
- var utc = this._isUTC ? 'UTC' : '';
- if (input != null) {
- this._d['set' + utc + key](input);
- moment.updateOffset(this);
+ function rawMonthSetter(mom, value) {
+ var dayOfMonth;
+
+ // TODO: Move this out of here!
+ if (typeof value === 'string') {
+ value = mom.lang().monthsParse(value);
+ // TODO: Another silent failure?
+ if (typeof value !== 'number') {
+ return mom;
+ }
+ }
+
+ dayOfMonth = Math.min(mom.date(),
+ daysInMonth(mom.year(), value));
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+ return mom;
+ }
+
+ function rawGetter(mom, unit) {
+ return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
+ }
+
+ function rawSetter(mom, unit, value) {
+ if (unit === 'Month') {
+ return rawMonthSetter(mom, value);
+ } else {
+ return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+ }
+ }
+
+ function makeAccessor(unit, keepTime) {
+ return function (value) {
+ if (value != null) {
+ rawSetter(this, unit, value);
+ moment.updateOffset(this, keepTime);
return this;
} else {
- return this._d['get' + utc + key]();
+ return rawGetter(this, unit);
}
};
}
- // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
- for (i = 0; i < proxyGettersAndSetters.length; i ++) {
- makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
- }
-
- // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
- makeGetterAndSetter('year', 'FullYear');
+ moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
+ moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
+ moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
+ // Setting the hour should keep the time, because the user explicitly
+ // specified which hour he wants. So trying to maintain the same hour (in
+ // a new timezone) makes sense. Adding/subtracting hours does not follow
+ // this rule.
+ moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
+ // moment.fn.month is defined separately
+ moment.fn.date = makeAccessor('Date', true);
+ moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
+ moment.fn.year = makeAccessor('FullYear', true);
+ moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
// add plural methods
moment.fn.days = moment.fn.day;
moment.fn.months = moment.fn.month;
moment.fn.weeks = moment.fn.week;
moment.fn.isoWeeks = moment.fn.isoWeek;
+ moment.fn.quarters = moment.fn.quarter;
// add aliased format methods
moment.fn.toJSON = moment.fn.toISOString;
@@ -22156,45 +21584,36 @@ else {
Exposing Moment
************************************/
- function makeGlobal(deprecate) {
- var warned = false, local_moment = moment;
+ function makeGlobal(shouldDeprecate) {
/*global ender:false */
if (typeof ender !== 'undefined') {
return;
}
- // here, `this` means `window` in the browser, or `global` on the server
- // add `moment` as a global object via a string identifier,
- // for Closure Compiler "advanced" mode
- if (deprecate) {
- global.moment = function () {
- if (!warned && console && console.warn) {
- warned = true;
- console.warn(
- "Accessing Moment through the global scope is " +
- "deprecated, and will be removed in an upcoming " +
- "release.");
- }
- return local_moment.apply(null, arguments);
- };
- extend(global.moment, local_moment);
+ oldGlobalMoment = globalScope.moment;
+ if (shouldDeprecate) {
+ globalScope.moment = deprecate(
+ "Accessing Moment through the global scope is " +
+ "deprecated, and will be removed in an upcoming " +
+ "release.",
+ moment);
} else {
- global['moment'] = moment;
+ globalScope.moment = moment;
}
}
// CommonJS module is defined
if (hasModule) {
module.exports = moment;
- makeGlobal(true);
} else if (typeof define === "function" && define.amd) {
define("moment", function (require, exports, module) {
- if (module.config && module.config() && module.config().noGlobal !== true) {
- // If user provided noGlobal, he is aware of global
- makeGlobal(module.config().noGlobal === undefined);
+ if (module.config && module.config() && module.config().noGlobal === true) {
+ // release the global variable
+ globalScope.moment = oldGlobalMoment;
}
return moment;
});
+ makeGlobal(true);
} else {
makeGlobal();
}
diff --git a/dist/vis.js.tmp b/dist/vis.js.tmp
new file mode 100644
index 00000000..57f04f85
--- /dev/null
+++ b/dist/vis.js.tmp
@@ -0,0 +1,17549 @@
+/**
+ * vis.js module imports
+ */
+
+// 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 Emitter = require('emitter-component');
+
+var Hammer;
+if (typeof window !== 'undefined') {
+ // load hammer.js only when running in a browser (where window is available)
+ Hammer = window['Hammer'] || require('hammerjs');
+}
+else {
+ Hammer = function () {
+ throw Error('hammer.js is only available in a browser, not in node.js.');
+ }
+}
+
+var mousetrap;
+if (typeof window !== 'undefined') {
+ // load mousetrap.js only when running in a browser (where window is available)
+ mousetrap = window['mousetrap'] || require('mousetrap');
+}
+else {
+ mousetrap = function () {
+ throw Error('mouseTrap is only available in a browser, not in node.js.');
+ }
+}
+
+
+// Internet Explorer 8 and older does not support Array.indexOf, so we define
+// it here in that case.
+// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
+if(!Array.prototype.indexOf) {
+ Array.prototype.indexOf = function(obj){
+ for(var i = 0; i < this.length; i++){
+ if(this[i] == obj){
+ return i;
+ }
+ }
+ return -1;
+ };
+
+ try {
+ console.log("Warning: Ancient browser detected. Please update your browser");
+ }
+ catch (err) {
+ }
+}
+
+// Internet Explorer 8 and older does not support Array.forEach, so we define
+// it here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
+if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function(fn, scope) {
+ for(var i = 0, len = this.length; i < len; ++i) {
+ fn.call(scope || this, this[i], i, this);
+ }
+ }
+}
+
+// Internet Explorer 8 and older does not support Array.map, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
+// Production steps of ECMA-262, Edition 5, 15.4.4.19
+// Reference: http://es5.github.com/#x15.4.4.19
+if (!Array.prototype.map) {
+ Array.prototype.map = function(callback, thisArg) {
+
+ var T, A, k;
+
+ if (this == null) {
+ throw new TypeError(" this is null or not defined");
+ }
+
+ // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
+ var O = Object(this);
+
+ // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
+ // 3. Let len be ToUint32(lenValue).
+ var len = O.length >>> 0;
+
+ // 4. If IsCallable(callback) is false, throw a TypeError exception.
+ // See: http://es5.github.com/#x9.11
+ if (typeof callback !== "function") {
+ throw new TypeError(callback + " is not a function");
+ }
+
+ // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (thisArg) {
+ T = thisArg;
+ }
+
+ // 6. Let A be a new array created as if by the expression new Array(len) where Array is
+ // the standard built-in constructor with that name and len is the value of len.
+ A = new Array(len);
+
+ // 7. Let k be 0
+ k = 0;
+
+ // 8. Repeat, while k < len
+ while(k < len) {
+
+ var kValue, mappedValue;
+
+ // a. Let Pk be ToString(k).
+ // This is implicit for LHS operands of the in operator
+ // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
+ // This step can be combined with c
+ // c. If kPresent is true, then
+ if (k in O) {
+
+ // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
+ kValue = O[ k ];
+
+ // ii. Let mappedValue be the result of calling the Call internal method of callback
+ // with T as the this value and argument list containing kValue, k, and O.
+ mappedValue = callback.call(T, kValue, k, O);
+
+ // iii. Call the DefineOwnProperty internal method of A with arguments
+ // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
+ // and false.
+
+ // In browsers that support Object.defineProperty, use the following:
+ // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
+
+ // For best browser support, use the following:
+ A[ k ] = mappedValue;
+ }
+ // d. Increase k by 1.
+ k++;
+ }
+
+ // 9. return A
+ return A;
+ };
+}
+
+// Internet Explorer 8 and older does not support Array.filter, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
+if (!Array.prototype.filter) {
+ Array.prototype.filter = function(fun /*, thisp */) {
+ "use strict";
+
+ if (this == null) {
+ throw new TypeError();
+ }
+
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (typeof fun != "function") {
+ throw new TypeError();
+ }
+
+ var res = [];
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++) {
+ if (i in t) {
+ var val = t[i]; // in case fun mutates this
+ if (fun.call(thisp, val, i, t))
+ res.push(val);
+ }
+ }
+
+ return res;
+ };
+}
+
+
+// Internet Explorer 8 and older does not support Object.keys, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
+if (!Object.keys) {
+ Object.keys = (function () {
+ var hasOwnProperty = Object.prototype.hasOwnProperty,
+ hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
+ dontEnums = [
+ 'toString',
+ 'toLocaleString',
+ 'valueOf',
+ 'hasOwnProperty',
+ 'isPrototypeOf',
+ 'propertyIsEnumerable',
+ 'constructor'
+ ],
+ dontEnumsLength = dontEnums.length;
+
+ return function (obj) {
+ if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
+ throw new TypeError('Object.keys called on non-object');
+ }
+
+ var result = [];
+
+ for (var prop in obj) {
+ if (hasOwnProperty.call(obj, prop)) result.push(prop);
+ }
+
+ if (hasDontEnumBug) {
+ for (var i=0; i < dontEnumsLength; i++) {
+ if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
+ }
+ }
+ return result;
+ }
+ })()
+}
+
+// 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");
+ }
+
+ 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");
+ }
+
+ 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;
+ };
+}
+
+/**
+ * utility functions
+ */
+var util = {};
+
+/**
+ * Test whether given object is a number
+ * @param {*} object
+ * @return {Boolean} isNumber
+ */
+util.isNumber = function isNumber(object) {
+ return (object instanceof Number || typeof object == 'number');
+};
+
+/**
+ * Test whether given object is a string
+ * @param {*} object
+ * @return {Boolean} isString
+ */
+util.isString = function isString(object) {
+ return (object instanceof String || typeof object == 'string');
+};
+
+/**
+ * Test whether given object is a Date, or a String containing a Date
+ * @param {Date | String} object
+ * @return {Boolean} isDate
+ */
+util.isDate = function isDate(object) {
+ if (object instanceof Date) {
+ return true;
+ }
+ else if (util.isString(object)) {
+ // test whether this string contains a date
+ var match = ASPDateRegex.exec(object);
+ if (match) {
+ return true;
+ }
+ else if (!isNaN(Date.parse(object))) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Test whether given object is an instance of google.visualization.DataTable
+ * @param {*} object
+ * @return {Boolean} isDataTable
+ */
+util.isDataTable = function isDataTable(object) {
+ return (typeof (google) !== 'undefined') &&
+ (google.visualization) &&
+ (google.visualization.DataTable) &&
+ (object instanceof google.visualization.DataTable);
+};
+
+/**
+ * Create a semi UUID
+ * source: http://stackoverflow.com/a/105074/1262753
+ * @return {String} uuid
+ */
+util.randomUUID = function randomUUID () {
+ var S4 = function () {
+ return Math.floor(
+ Math.random() * 0x10000 /* 65536 */
+ ).toString(16);
+ };
+
+ return (
+ S4() + S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + S4() + S4()
+ );
+};
+
+/**
+ * Extend object a with the properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+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];
+ }
+ }
+ }
+
+ return a;
+};
+
+/**
+ * Test whether all elements in two arrays are equal.
+ * @param {Array} a
+ * @param {Array} b
+ * @return {boolean} Returns true if both arrays have the same length and same
+ * elements.
+ */
+util.equalArray = function (a, b) {
+ if (a.length != b.length) return false;
+
+ for (var i = 1, len = a.length; i < len; i++) {
+ if (a[i] != b[i]) return false;
+ }
+
+ return true;
+};
+
+/**
+ * Convert an object to another type
+ * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
+ * @param {String | undefined} type Name of the type. Available types:
+ * 'Boolean', 'Number', 'String',
+ * 'Date', 'Moment', ISODate', 'ASPDate'.
+ * @return {*} object
+ * @throws Error
+ */
+util.convert = function convert(object, type) {
+ var match;
+
+ if (object === undefined) {
+ return undefined;
+ }
+ if (object === null) {
+ return null;
+ }
+
+ if (!type) {
+ return object;
+ }
+ if (!(typeof type === 'string') && !(type instanceof String)) {
+ throw new Error('Type must be a string');
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'boolean':
+ case 'Boolean':
+ return Boolean(object);
+
+ 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 moment(object).toDate(); // parse string
+ }
+ }
+ else {
+ 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(object);
+ }
+ if (util.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return moment(Number(match[1])); // parse number
+ }
+ else {
+ return moment(object); // parse string
+ }
+ }
+ 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';
+ }
+
+ return type;
+};
+
+/**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} left The absolute left position of this element
+ * in the browser page.
+ */
+util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
+ var doc = document.documentElement;
+ var body = document.body;
+
+ var left = elem.offsetLeft;
+ var e = elem.offsetParent;
+ while (e != null && e != body && e != doc) {
+ left += e.offsetLeft;
+ left -= e.scrollLeft;
+ e = e.offsetParent;
+ }
+ return left;
+};
+
+/**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} top The absolute top position of this element
+ * in the browser page.
+ */
+util.getAbsoluteTop = function getAbsoluteTop (elem) {
+ var doc = document.documentElement;
+ var body = document.body;
+
+ var top = elem.offsetTop;
+ var e = elem.offsetParent;
+ while (e != null && e != body && e != doc) {
+ top += e.offsetTop;
+ top -= e.scrollTop;
+ e = e.offsetParent;
+ }
+ return top;
+};
+
+/**
+ * Get the absolute, vertical mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageY
+ */
+util.getPageY = function getPageY (event) {
+ if ('pageY' in event) {
+ return event.pageY;
+ }
+ else {
+ var clientY;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientY = event.targetTouches[0].clientY;
+ }
+ else {
+ clientY = event.clientY;
+ }
+
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientY +
+ ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
+ ( doc && doc.clientTop || body && body.clientTop || 0 );
+ }
+};
+
+/**
+ * Get the absolute, horizontal mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageX
+ */
+util.getPageX = function getPageX (event) {
+ if ('pageY' in event) {
+ return event.pageX;
+ }
+ else {
+ var clientX;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientX = event.targetTouches[0].clientX;
+ }
+ else {
+ clientX = event.clientX;
+ }
+
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientX +
+ ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
+ ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+ }
+};
+
+/**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+util.addClassName = function addClassName(elem, className) {
+ var classes = elem.className.split(' ');
+ if (classes.indexOf(className) == -1) {
+ classes.push(className); // add the class to the array
+ elem.className = classes.join(' ');
+ }
+};
+
+/**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+util.removeClassName = function removeClassname(elem, className) {
+ var classes = elem.className.split(' ');
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1); // remove the class from the array
+ elem.className = classes.join(' ');
+ }
+};
+
+/**
+ * For each method for both arrays and objects.
+ * In case of an array, the built-in Array.forEach() is applied.
+ * In case of an Object, the method loops over all properties of the object.
+ * @param {Object | Array} object An Object or Array
+ * @param {function} callback Callback method, called for each item in
+ * the object or array with three parameters:
+ * callback(value, index, object)
+ */
+util.forEach = function forEach (object, callback) {
+ 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);
+ }
+ }
+ }
+};
+
+/**
+ * Convert an object into an array: all objects properties are put into the
+ * array. The resulting array is unordered.
+ * @param {Object} object
+ * @param {Array} array
+ */
+util.toArray = function toArray(object) {
+ var array = [];
+
+ for (var prop in object) {
+ if (object.hasOwnProperty(prop)) array.push(object[prop]);
+ }
+
+ return array;
+}
+
+/**
+ * Update a property in an object
+ * @param {Object} object
+ * @param {String} key
+ * @param {*} value
+ * @return {Boolean} changed
+ */
+util.updateProperty = function updateProperty (object, key, value) {
+ if (object[key] !== value) {
+ object[key] = value;
+ return true;
+ }
+ else {
+ return false;
+ }
+};
+
+/**
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example "click",
+ * without the prefix "on"
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} [useCapture]
+ */
+util.addEventListener = function addEventListener(element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.addEventListener(action, listener, useCapture);
+ } else {
+ element.attachEvent("on" + action, listener); // IE browsers
+ }
+};
+
+/**
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example "mousedown"
+ * @param {function} listener The listener function
+ * @param {boolean} [useCapture]
+ */
+util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ // non-IE browsers
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.removeEventListener(action, listener, useCapture);
+ } else {
+ // IE browsers
+ element.detachEvent("on" + action, listener);
+ }
+};
+
+
+/**
+ * Get HTML element which is the target of the event
+ * @param {Event} event
+ * @return {Element} target element
+ */
+util.getTarget = function getTarget(event) {
+ // code from http://www.quirksmode.org/js/events_properties.html
+ if (!event) {
+ event = window.event;
+ }
+
+ var target;
+
+ if (event.target) {
+ target = event.target;
+ }
+ else if (event.srcElement) {
+ target = event.srcElement;
+ }
+
+ if (target.nodeType != undefined && target.nodeType == 3) {
+ // defeat Safari bug
+ target = target.parentNode;
+ }
+
+ return target;
+};
+
+/**
+ * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
+ * @param {Element} element
+ * @param {Event} event
+ */
+util.fakeGesture = function fakeGesture (element, event) {
+ var eventType = null;
+
+ // for hammer.js 1.0.5
+ var gesture = Hammer.event.collectEventData(this, eventType, event);
+
+ // for hammer.js 1.0.6
+ //var touches = Hammer.event.getTouchList(event, eventType);
+ // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
+
+ // on IE in standards mode, no touches are recognized by hammer.js,
+ // resulting in NaN values for center.pageX and center.pageY
+ if (isNaN(gesture.center.pageX)) {
+ gesture.center.pageX = event.pageX;
+ }
+ if (isNaN(gesture.center.pageY)) {
+ gesture.center.pageY = event.pageY;
+ }
+
+ return gesture;
+};
+
+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();
+ }
+
+ if (value != null) {
+ return (value != false);
+ }
+
+ return defaultValue || null;
+};
+
+/**
+ * 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();
+ }
+
+ if (value != null) {
+ return Number(value) || defaultValue || null;
+ }
+
+ return defaultValue || null;
+};
+
+/**
+ * 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();
+ }
+
+ if (value != null) {
+ return String(value);
+ }
+
+ return defaultValue || 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();
+ }
+
+ 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();
+ }
+
+ return value || defaultValue || null;
+};
+
+
+
+util.GiveDec = function GiveDec(Hex) {
+ var Value;
+
+ if (Hex == "A")
+ Value = 10;
+ else if (Hex == "B")
+ Value = 11;
+ else if (Hex == "C")
+ Value = 12;
+ else if (Hex == "D")
+ Value = 13;
+ else if (Hex == "E")
+ Value = 14;
+ else if (Hex == "F")
+ Value = 15;
+ else
+ Value = eval(Hex);
+
+ return Value;
+};
+
+util.GiveHex = function GiveHex(Dec) {
+ var Value;
+
+ if(Dec == 10)
+ Value = "A";
+ else if (Dec == 11)
+ Value = "B";
+ else if (Dec == 12)
+ Value = "C";
+ else if (Dec == 13)
+ Value = "D";
+ else if (Dec == 14)
+ Value = "E";
+ else if (Dec == 15)
+ Value = "F";
+ else
+ Value = "" + Dec;
+
+ return Value;
+};
+
+/**
+ * Parse a color property into an object with border, background, and
+ * highlight colors
+ * @param {Object | String} color
+ * @return {Object} colorObject
+ */
+util.parseColor = function(color) {
+ var c;
+ if (util.isString(color)) {
+ if (util.isValidHex(color)) {
+ var hsv = util.hexToHSV(color);
+ var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
+ var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
+ var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
+ var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
+
+ c = {
+ background: color,
+ border:darkerColorHex,
+ highlight: {
+ background:lighterColorHex,
+ border:darkerColorHex
+ }
+ };
+ }
+ else {
+ c = {
+ background:color,
+ border:color,
+ highlight: {
+ background:color,
+ border:color
+ }
+ };
+ }
+ }
+ else {
+ c = {};
+ c.background = color.background || 'white';
+ c.border = color.border || c.background;
+
+ if (util.isString(color.highlight)) {
+ c.highlight = {
+ border: color.highlight,
+ background: color.highlight
+ }
+ }
+ else {
+ c.highlight = {};
+ c.highlight.background = color.highlight && color.highlight.background || c.background;
+ c.highlight.border = color.highlight && color.highlight.border || c.border;
+ }
+ }
+
+ return c;
+};
+
+/**
+ * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
+ *
+ * @param {String} hex
+ * @returns {{r: *, g: *, b: *}}
+ */
+util.hexToRGB = function hexToRGB(hex) {
+ hex = hex.replace("#","").toUpperCase();
+
+ var a = util.GiveDec(hex.substring(0, 1));
+ var b = util.GiveDec(hex.substring(1, 2));
+ var c = util.GiveDec(hex.substring(2, 3));
+ var d = util.GiveDec(hex.substring(3, 4));
+ var e = util.GiveDec(hex.substring(4, 5));
+ var f = util.GiveDec(hex.substring(5, 6));
+
+ var r = (a * 16) + b;
+ var g = (c * 16) + d;
+ var b = (e * 16) + f;
+
+ return {r:r,g:g,b:b};
+};
+
+util.RGBToHex = function RGBToHex(red,green,blue) {
+ var a = util.GiveHex(Math.floor(red / 16));
+ var b = util.GiveHex(red % 16);
+ var c = util.GiveHex(Math.floor(green / 16));
+ var d = util.GiveHex(green % 16);
+ var e = util.GiveHex(Math.floor(blue / 16));
+ var f = util.GiveHex(blue % 16);
+
+ var hex = a + b + c + d + e + f;
+ return "#" + hex;
+};
+
+
+/**
+ * http://www.javascripter.net/faq/rgb2hsv.htm
+ *
+ * @param red
+ * @param green
+ * @param blue
+ * @returns {*}
+ * @constructor
+ */
+util.RGBToHSV = function RGBToHSV (red,green,blue) {
+ red=red/255; green=green/255; blue=blue/255;
+ var minRGB = Math.min(red,Math.min(green,blue));
+ var maxRGB = Math.max(red,Math.max(green,blue));
+
+ // Black-gray-white
+ if (minRGB == maxRGB) {
+ return {h:0,s:0,v:minRGB};
+ }
+
+ // Colors other than black-gray-white:
+ var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
+ var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
+ var hue = 60*(h - d/(maxRGB - minRGB))/360;
+ var saturation = (maxRGB - minRGB)/maxRGB;
+ var value = maxRGB;
+ return {h:hue,s:saturation,v:value};
+};
+
+
+/**
+ * https://gist.github.com/mjijackson/5311256
+ * @param hue
+ * @param saturation
+ * @param value
+ * @returns {{r: number, g: number, b: number}}
+ * @constructor
+ */
+util.HSVToRGB = function HSVToRGB(h, s, v) {
+ var r, g, b;
+
+ var i = Math.floor(h * 6);
+ var f = h * 6 - i;
+ var p = v * (1 - s);
+ var q = v * (1 - f * s);
+ var t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0: r = v, g = t, b = p; break;
+ case 1: r = q, g = v, b = p; break;
+ case 2: r = p, g = v, b = t; break;
+ case 3: r = p, g = q, b = v; break;
+ case 4: r = t, g = p, b = v; break;
+ case 5: r = v, g = p, b = q; break;
+ }
+
+ return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
+};
+
+util.HSVToHex = function HSVToHex(h, s, v) {
+ var rgb = util.HSVToRGB(h, s, v);
+ return util.RGBToHex(rgb.r, rgb.g, rgb.b);
+};
+
+util.hexToHSV = function hexToHSV(hex) {
+ var rgb = util.hexToRGB(hex);
+ return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
+};
+
+util.isValidHex = function isValidHex(hex) {
+ var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
+ return isOk;
+};
+
+util.copyObject = function copyObject(objectFrom, objectTo) {
+ for (var i in objectFrom) {
+ if (objectFrom.hasOwnProperty(i)) {
+ if (typeof objectFrom[i] == "object") {
+ objectTo[i] = {};
+ util.copyObject(objectFrom[i], objectTo[i]);
+ }
+ else {
+ objectTo[i] = objectFrom[i];
+ }
+ }
+ }
+};
+
+/**
+ * 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 {Array | DataTable} [data] Optional array with initial data
+ * @param {Object} [options] Available options:
+ * {String} fieldId Field name of the id in the
+ * items, 'id' by default.
+ * {Object.} [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;
+ var globalShowInternalIds = this.showInternalIds;
+
+ // parse the arguments
+ var id, ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number') {
+ // get(id [, options] [, data])
+ id = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else if (firstType == 'Array') {
+ // get(ids [, options] [, data])
+ ids = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
+
+ // determine the return type
+ var type;
+ if (options && options.type) {
+ type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
+
+ if (data && (type != util.getType(data))) {
+ throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
+ 'does not correspond with specified options.type (' + options.type + ')');
+ }
+ if (type == 'DataTable' && !util.isDataTable(data)) {
+ throw new Error('Parameter "data" must be a DataTable ' +
+ 'when options.type is "DataTable"');
+ }
+ }
+ else if (data) {
+ type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
+ }
+ else {
+ type = 'Array';
+ }
+
+ // we allow the setting of this value for a single get request.
+ if (options != undefined) {
+ if (options.showInternalIds != undefined) {
+ this.showInternalIds = options.showInternalIds;
+ }
+ }
+
+ // 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);
+ }
+ }
+ }
+ 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);
+ }
+ }
+ }
+ }
+
+ // restore the global value of showInternalIds
+ this.showInternalIds = globalShowInternalIds;
+
+ // order the results
+ if (options && options.order && id == undefined) {
+ this._sort(items, options.order);
+ }
+
+ // filter fields of the items
+ if (options && options.fields) {
+ var fields = options.fields;
+ if (id != undefined) {
+ item = this._filterFields(item, fields);
+ }
+ else {
+ for (i = 0, len = items.length; i < len; i++) {
+ items[i] = this._filterFields(items[i], fields);
+ }
+ }
+ }
+
+ // return the results
+ if (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 {
+ // return an array
+ if (id != undefined) {
+ // a single item
+ return item;
+ }
+ else {
+ // multiple items
+ if (data) {
+ // copy the items to the provided array
+ for (i = 0, len = items.length; i < len; i++) {
+ data.push(items[i]);
+ }
+ return data;
+ }
+ else {
+ // just return our array
+ return items;
+ }
+ }
+ }
+};
+
+/**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+DataSet.prototype.getIds = function (options) {
+ var data = this.data,
+ filter = options && options.filter,
+ order = options && options.order,
+ convert = options && options.convert || this.options.convert,
+ i,
+ len,
+ id,
+ item,
+ items,
+ ids = [];
+
+ if (filter) {
+ // get filtered items
+ if (order) {
+ // create ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, 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]);
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this.fieldId];
+ }
+ }
+ else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = data[id];
+ ids.push(item[this.fieldId]);
+ }
+ }
+ }
+ }
+
+ return ids;
+};
+
+/**
+ * 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);
+ }
+ }
+ else {
+ // unordered
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, convert);
+ if (!filter || filter(item)) {
+ callback(item, id);
+ }
+ }
+ }
+ }
+};
+
+/**
+ * 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;
+
+ // 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));
+ }
+ }
+ }
+
+ // order items
+ if (options && options.order) {
+ this._sort(mappedItems, options.order);
+ }
+
+ return mappedItems;
+};
+
+/**
+ * Filter the fields of an item
+ * @param {Object} item
+ * @param {String[]} fields Field names
+ * @return {Object} filteredItem
+ * @private
+ */
+DataSet.prototype._filterFields = function (item, fields) {
+ var filteredItem = {};
+
+ for (var field in item) {
+ if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
+ filteredItem[field] = item[field];
+ }
+ }
+
+ return filteredItem;
+};
+
+/**
+ * Sort the provided array with items
+ * @param {Object[]} items
+ * @param {String | function} order A field name or custom sort function.
+ * @private
+ */
+DataSet.prototype._sort = function (items, order) {
+ if (util.isString(order)) {
+ // order by provided field name
+ var name = order; // field name
+ items.sort(function (a, b) {
+ var av = a[name];
+ var bv = b[name];
+ return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
+ });
+ }
+ else if (typeof order === 'function') {
+ // order by sort function
+ items.sort(order);
+ }
+ // TODO: extend order by an Object {field:String, direction:String}
+ // where direction can be 'asc' or 'desc'
+ else {
+ throw new TypeError('Order must be a function or a string');
+ }
+};
+
+/**
+ * Remove an object by pointer or by id
+ * @param {String | Number | Object | Array} id Object or id, or an array with
+ * objects or ids to be removed
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds
+ */
+DataSet.prototype.remove = function (id, senderId) {
+ var removedIds = [],
+ i, len, removedId;
+
+ if (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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * Clear the data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds The ids of all removed items
+ */
+DataSet.prototype.clear = function (senderId) {
+ var ids = Object.keys(this.data);
+
+ this.data = {};
+ this.internalIds = {};
+
+ this._trigger('remove', {items: ids}, senderId);
+
+ return ids;
+};
+
+/**
+ * Find the item with maximum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+DataSet.prototype.max = function (field) {
+ var data = this.data,
+ max = null,
+ maxField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!max || itemField > maxField)) {
+ max = item;
+ maxField = itemField;
+ }
+ }
+ }
+
+ return max;
+};
+
+/**
+ * Find the item with minimum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+DataSet.prototype.min = function (field) {
+ var data = this.data,
+ min = null,
+ minField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!min || itemField < minField)) {
+ min = item;
+ minField = itemField;
+ }
+ }
+ }
+
+ return min;
+};
+
+/**
+ * Find all distinct values of a specified field
+ * @param {String} field
+ * @return {Array} values Array containing all distinct values. If 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;
+
+ 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;
+};
+
+/**
+ * Add a single item. Will fail when an item with the same id already exists.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._addItem = function (item) {
+ var id = item[this.fieldId];
+
+ if (id != undefined) {
+ // check whether this id is already taken
+ if (this.data[id]) {
+ // item already exists
+ throw new Error('Cannot add item: item with id ' + id + ' already exists');
+ }
+ }
+ else {
+ // generate an id
+ id = util.randomUUID();
+ item[this.fieldId] = id;
+ 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;
+};
+
+/**
+ * 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;
+
+ // 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) || this.showInternalIds)) {
+ converted[field] = util.convert(value, convert[field]);
+ }
+ }
+ }
+ }
+ else {
+ // no field types specified, no converting needed
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ // output all fields, except internal ids
+ if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
+ converted[field] = value;
+ }
+ }
+ }
+ }
+ return converted;
+};
+
+/**
+ * Update a single item: merge with existing item.
+ * Will fail when the item has no id, or when there does not exist an item
+ * with the same id.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._updateItem = function (item) {
+ var id = item[this.fieldId];
+ if (id == undefined) {
+ throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
+ }
+ var d = this.data[id];
+ if (!d) {
+ // item doesn't exist
+ throw new Error('Cannot update item: no item with id ' + id + ' found');
+ }
+
+ // merge with current item
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this.convert[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
+
+ return id;
+};
+
+/**
+ * check if an id is an internal or external id
+ * @param id
+ * @returns {boolean}
+ * @private
+ */
+DataSet.prototype.isInternalId = function(id) {
+ return (id in this.internalIds);
+};
+
+
+/**
+ * Get an array with the column names of a Google DataTable
+ * @param {DataTable} dataTable
+ * @return {String[]} columnNames
+ * @private
+ */
+DataSet.prototype._getColumnNames = function (dataTable) {
+ var columns = [];
+ for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
+ columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
+ }
+ return columns;
+};
+
+/**
+ * Append an item as a row to the dataTable
+ * @param dataTable
+ * @param columns
+ * @param item
+ * @private
+ */
+DataSet.prototype._appendRow = function (dataTable, columns, item) {
+ var row = dataTable.addRow();
+
+ for (var col = 0, cols = columns.length; col < cols; col++) {
+ var field = columns[col];
+ dataTable.setValue(row, col, item[field]);
+ }
+};
+
+/**
+ * DataView
+ *
+ * a dataview offers a filtered view on a dataset or an other dataview.
+ *
+ * @param {DataSet | DataView} data
+ * @param {Object} [options] Available options: see method get
+ *
+ * @constructor DataView
+ */
+function DataView (data, options) {
+ this.id = util.randomUUID();
+
+ this.data = null;
+ this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
+ this.options = options || {};
+ this.fieldId = 'id'; // name of the field containing id
+ this.subscribers = {}; // event subscribers
+
+ var me = this;
+ this.listener = function () {
+ me._onEvent.apply(me, arguments);
+ };
+
+ this.setData(data);
+}
+
+// TODO: implement a function .config() to dynamically update things like configured filter
+// and trigger changes accordingly
+
+/**
+ * Set a data source for the view
+ * @param {DataSet | DataView} data
+ */
+DataView.prototype.setData = function (data) {
+ var ids, dataItems, i, len;
+
+ if (this.data) {
+ // unsubscribe from current dataset
+ if (this.data.unsubscribe) {
+ this.data.unsubscribe('*', this.listener);
+ }
+
+ // trigger a remove of all items in memory
+ ids = [];
+ for (var id in this.ids) {
+ if (this.ids.hasOwnProperty(id)) {
+ ids.push(id);
+ }
+ }
+ this.ids = {};
+ this._trigger('remove', {items: ids});
+ }
+
+ this.data = data;
+
+ if (this.data) {
+ // update fieldId
+ this.fieldId = this.options.fieldId ||
+ (this.data && this.data.options && this.data.options.fieldId) ||
+ 'id';
+
+ // trigger an add of all added items
+ ids = this.data.getIds({filter: this.options && this.options.filter});
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ this.ids[id] = true;
+ }
+ this._trigger('add', {items: ids});
+
+ // subscribe to new dataset
+ if (this.data.on) {
+ this.data.on('*', this.listener);
+ }
+ }
+};
+
+/**
+ * Get data from the data view
+ *
+ * Usage:
+ *
+ * get()
+ * get(options: Object)
+ * get(options: Object, data: Array | DataTable)
+ *
+ * get(id: Number)
+ * get(id: Number, options: Object)
+ * get(id: Number, options: Object, data: Array | DataTable)
+ *
+ * get(ids: Number[])
+ * get(ids: Number[], options: Object)
+ * get(ids: Number[], options: Object, data: Array | DataTable)
+ *
+ * Where:
+ *
+ * {Number | String} id The id of an item
+ * {Number[] | String{}} ids An array with ids of items
+ * {Object} options An Object with options. Available options:
+ * {String} [type] Type of data to be returned. Can
+ * be 'DataTable' or 'Array' (default)
+ * {Object.} [convert]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ * @param args
+ */
+DataView.prototype.get = function (args) {
+ var me = this;
+
+ // parse the arguments
+ var ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
+ // get(id(s) [, options] [, data])
+ ids = arguments[0]; // can be a single id or an array with ids
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
+
+ // extend the options with the default options and provided options
+ var viewOptions = util.extend({}, this.options, options);
+
+ // create a combined filter method when needed
+ if (this.options.filter && options && options.filter) {
+ viewOptions.filter = function (item) {
+ return me.options.filter(item) && options.filter(item);
+ }
+ }
+
+ // build up the call to the linked data set
+ var getArguments = [];
+ if (ids != undefined) {
+ getArguments.push(ids);
+ }
+ getArguments.push(viewOptions);
+ getArguments.push(data);
+
+ return this.data && this.data.get.apply(this.data, getArguments);
+};
+
+/**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+DataView.prototype.getIds = function (options) {
+ var ids;
+
+ if (this.data) {
+ var defaultFilter = this.options.filter;
+ var filter;
+
+ if (options && options.filter) {
+ if (defaultFilter) {
+ filter = function (item) {
+ return defaultFilter(item) && options.filter(item);
+ }
+ }
+ else {
+ filter = options.filter;
+ }
+ }
+ else {
+ filter = defaultFilter;
+ }
+
+ ids = this.data.getIds({
+ filter: filter,
+ order: options && options.order
+ });
+ }
+ else {
+ ids = [];
+ }
+
+ return ids;
+};
+
+/**
+ * Event listener. Will propagate all events from the connected data set to
+ * the subscribers of the DataView, but will filter the items and only trigger
+ * when there are changes in the filtered data set.
+ * @param {String} event
+ * @param {Object | null} params
+ * @param {String} senderId
+ * @private
+ */
+DataView.prototype._onEvent = function (event, params, senderId) {
+ var i, len, id, item,
+ ids = params && params.items,
+ data = this.data,
+ added = [],
+ updated = [],
+ removed = [];
+
+ if (ids && data) {
+ switch (event) {
+ case 'add':
+ // filter the ids of the added items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+ if (item) {
+ this.ids[id] = true;
+ added.push(id);
+ }
+ }
+
+ break;
+
+ case 'update':
+ // determine the event from the views viewpoint: an updated
+ // item can be added, updated, or removed from this view.
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+
+ if (item) {
+ if (this.ids[id]) {
+ updated.push(id);
+ }
+ else {
+ this.ids[id] = true;
+ added.push(id);
+ }
+ }
+ else {
+ if (this.ids[id]) {
+ delete this.ids[id];
+ removed.push(id);
+ }
+ else {
+ // nothing interesting for me :-(
+ }
+ }
+ }
+
+ break;
+
+ case 'remove':
+ // filter the ids of the removed items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ if (this.ids[id]) {
+ delete this.ids[id];
+ removed.push(id);
+ }
+ }
+
+ break;
+ }
+
+ if (added.length) {
+ this._trigger('add', {items: added}, senderId);
+ }
+ if (updated.length) {
+ this._trigger('update', {items: updated}, senderId);
+ }
+ if (removed.length) {
+ this._trigger('remove', {items: removed}, senderId);
+ }
+ }
+};
+
+// copy subscription functionality from DataSet
+DataView.prototype.on = DataSet.prototype.on;
+DataView.prototype.off = DataSet.prototype.off;
+DataView.prototype._trigger = DataSet.prototype._trigger;
+
+// TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
+DataView.prototype.subscribe = DataView.prototype.on;
+DataView.prototype.unsubscribe = DataView.prototype.off;
+
+/**
+ * @constructor TimeStep
+ * The class TimeStep is an iterator for dates. You provide a start date and an
+ * end date. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
+ *
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ *
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
+ *
+ * Version: 1.2
+ *
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ */
+TimeStep = function(start, end, minimumStep) {
+ // variables
+ this.current = new Date();
+ this._start = new Date();
+ this._end = new Date();
+
+ this.autoScale = true;
+ this.scale = TimeStep.SCALE.DAY;
+ this.step = 1;
+
+ // initialize the range
+ this.setRange(start, end, minimumStep);
+};
+
+/// enum scale
+TimeStep.SCALE = {
+ MILLISECOND: 1,
+ SECOND: 2,
+ MINUTE: 3,
+ HOUR: 4,
+ DAY: 5,
+ WEEKDAY: 6,
+ MONTH: 7,
+ YEAR: 8
+};
+
+
+/**
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Date} [start] The start date and time.
+ * @param {Date} [end] The end date and time.
+ * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
+ */
+TimeStep.prototype.setRange = function(start, end, minimumStep) {
+ if (!(start instanceof Date) || !(end instanceof Date)) {
+ throw "No legal start or end date in method setRange";
+ }
+
+ this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
+ this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
+
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep);
+ }
+};
+
+/**
+ * Set the range iterator to the start date.
+ */
+TimeStep.prototype.first = function() {
+ this.current = new Date(this._start.valueOf());
+ this.roundToMinor();
+};
+
+/**
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
+ */
+TimeStep.prototype.roundToMinor = function() {
+ // round to floor
+ // IMPORTANT: we have no breaks in this switch! (this is no bug)
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.scale) {
+ case TimeStep.SCALE.YEAR:
+ this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
+ this.current.setMonth(0);
+ case TimeStep.SCALE.MONTH: this.current.setDate(1);
+ case TimeStep.SCALE.DAY: // intentional fall through
+ case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
+ case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
+ case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
+ case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
+ //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
+ }
+
+ if (this.step != 1) {
+ // round down to the first minor value that is a multiple of the current step size
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
+ case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
+ case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
+ case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
+ default: break;
+ }
+ }
+};
+
+/**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+TimeStep.prototype.hasNext = function () {
+ return (this.current.valueOf() <= this._end.valueOf());
+};
+
+/**
+ * Do the next step
+ */
+TimeStep.prototype.next = function() {
+ var prev = this.current.valueOf();
+
+ // Two cases, needed to prevent issues with switching daylight savings
+ // (end of March and end of October)
+ if (this.current.getMonth() < 6) {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:
+
+ this.current = new Date(this.current.valueOf() + this.step); break;
+ case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
+ case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
+ case TimeStep.SCALE.HOUR:
+ this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
+ // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
+ var h = this.current.getHours();
+ this.current.setHours(h - (h % this.step));
+ break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
+ }
+ }
+ else {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
+ case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
+ case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
+ case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
+ case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
+ case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
+ default: break;
+ }
+ }
+
+ if (this.step != 1) {
+ // round down to the correct major value
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
+ case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
+ case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
+ case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
+ case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
+ case TimeStep.SCALE.YEAR: break; // nothing to do for year
+ default: break;
+ }
+ }
+
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current.valueOf() == prev) {
+ this.current = new Date(this._end.valueOf());
+ }
+};
+
+
+/**
+ * Get the current datetime
+ * @return {Date} current The current date
+ */
+TimeStep.prototype.getCurrent = function() {
+ return this.current;
+};
+
+/**
+ * Set a custom scale. Autoscaling will be disabled.
+ * For example setScale(SCALE.MINUTES, 5) will result
+ * in minor steps of 5 minutes, and major steps of an hour.
+ *
+ * @param {TimeStep.SCALE} newScale
+ * A scale. Choose from SCALE.MILLISECOND,
+ * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
+ * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
+ * SCALE.YEAR.
+ * @param {Number} newStep A step size, by default 1. Choose for
+ * example 1, 2, 5, or 10.
+ */
+TimeStep.prototype.setScale = function(newScale, newStep) {
+ this.scale = newScale;
+
+ if (newStep > 0) {
+ this.step = newStep;
+ }
+
+ this.autoScale = false;
+};
+
+/**
+ * Enable or disable autoscaling
+ * @param {boolean} enable If true, autoascaling is set true
+ */
+TimeStep.prototype.setAutoScale = function (enable) {
+ this.autoScale = enable;
+};
+
+
+/**
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
+ */
+TimeStep.prototype.setMinimumStep = function(minimumStep) {
+ if (minimumStep == undefined) {
+ return;
+ }
+
+ var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
+ var stepMonth = (1000 * 60 * 60 * 24 * 30);
+ var stepDay = (1000 * 60 * 60 * 24);
+ var stepHour = (1000 * 60 * 60);
+ var stepMinute = (1000 * 60);
+ var stepSecond = (1000);
+ var stepMillisecond= (1);
+
+ // find the smallest step that is larger than the provided minimumStep
+ if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
+ if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
+ if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
+ if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
+ if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
+ if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
+ if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
+ if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
+ if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
+ if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
+ if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
+ if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
+ if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
+ if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
+ if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
+ if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
+ if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
+ if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
+ if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
+ if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
+ if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
+ if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
+ if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
+ if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
+ if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
+ if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
+ if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
+ if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
+ if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
+};
+
+/**
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
+ */
+TimeStep.prototype.snap = function(date) {
+ var clone = new Date(date.valueOf());
+
+ if (this.scale == TimeStep.SCALE.YEAR) {
+ var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
+ clone.setFullYear(Math.round(year / this.step) * this.step);
+ clone.setMonth(0);
+ clone.setDate(0);
+ clone.setHours(0);
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.MONTH) {
+ if (clone.getDate() > 15) {
+ clone.setDate(1);
+ clone.setMonth(clone.getMonth() + 1);
+ // important: first set Date to 1, after that change the month.
+ }
+ else {
+ clone.setDate(1);
+ }
+
+ clone.setHours(0);
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.DAY ||
+ this.scale == TimeStep.SCALE.WEEKDAY) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 5:
+ case 2:
+ clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
+ default:
+ clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
+ }
+ clone.setMinutes(0);
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.HOUR) {
+ switch (this.step) {
+ case 4:
+ clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
+ default:
+ clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
+ }
+ clone.setSeconds(0);
+ clone.setMilliseconds(0);
+ } else if (this.scale == TimeStep.SCALE.MINUTE) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
+ clone.setSeconds(0);
+ break;
+ case 5:
+ clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
+ default:
+ clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
+ }
+ clone.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.SECOND) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
+ clone.setMilliseconds(0);
+ break;
+ case 5:
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
+ default:
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
+ }
+ }
+ else if (this.scale == TimeStep.SCALE.MILLISECOND) {
+ var step = this.step > 5 ? this.step / 2 : 1;
+ clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
+ }
+
+ return clone;
+};
+
+/**
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
+ */
+TimeStep.prototype.isMajor = function() {
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:
+ return (this.current.getMilliseconds() == 0);
+ case TimeStep.SCALE.SECOND:
+ return (this.current.getSeconds() == 0);
+ case TimeStep.SCALE.MINUTE:
+ return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
+ // Note: this is no bug. Major label is equal for both minute and hour scale
+ case TimeStep.SCALE.HOUR:
+ return (this.current.getHours() == 0);
+ case TimeStep.SCALE.WEEKDAY: // intentional fall through
+ case TimeStep.SCALE.DAY:
+ return (this.current.getDate() == 1);
+ case TimeStep.SCALE.MONTH:
+ return (this.current.getMonth() == 0);
+ case TimeStep.SCALE.YEAR:
+ return false;
+ default:
+ return false;
+ }
+};
+
+
+/**
+ * Returns formatted text for the minor axislabel, depending on the current
+ * date and the scale. For example when scale is MINUTE, the current time is
+ * formatted as "hh:mm".
+ * @param {Date} [date] custom date. if not provided, current date is taken
+ */
+TimeStep.prototype.getLabelMinor = function(date) {
+ if (date == undefined) {
+ date = this.current;
+ }
+
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
+ case TimeStep.SCALE.SECOND: return moment(date).format('s');
+ case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
+ case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
+ case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
+ case TimeStep.SCALE.DAY: return moment(date).format('D');
+ case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
+ case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
+ default: return '';
+ }
+};
+
+
+/**
+ * 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
+ */
+TimeStep.prototype.getLabelMajor = function(date) {
+ if (date == undefined) {
+ date = this.current;
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.scale) {
+ case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
+ case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
+ case TimeStep.SCALE.MINUTE:
+ case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
+ case TimeStep.SCALE.WEEKDAY:
+ case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
+ case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
+ case TimeStep.SCALE.YEAR: return '';
+ default: return '';
+ }
+};
+
+// TODO: turn Stack into a Mixin?
+
+/**
+ * @constructor Stack
+ * Stacks items on top of each other.
+ * @param {Object} [options]
+ */
+function Stack (options) {
+ this.options = options || {};
+ this.defaultOptions = {
+ order: function (a, b) {
+ // Order: ranges over non-ranges, ranged ordered by width,
+ // and non-ranges ordered by start.
+ if (a instanceof ItemRange) {
+ if (b instanceof ItemRange) {
+ var aInt = (a.data.end - a.data.start);
+ 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,
+ axis: 20
+ }
+ };
+}
+
+/**
+ * Set options for the stack
+ * @param {Object} options Available options:
+ * {Number} [margin.item=10]
+ * {Number} [margin.axis=20]
+ * {function} [order] Stacking order
+ */
+Stack.prototype.setOptions = function setOptions (options) {
+ util.extend(this.options, options);
+};
+
+/**
+ * Order an array with items using a predefined order function for items
+ * @param {Item[]} items
+ */
+Stack.prototype.order = function order(items) {
+ //order the items
+ var order = this.options.order || this.defaultOptions.order;
+ if (!(typeof order === 'function')) {
+ throw new Error('Option order must be a function');
+ }
+ items.sort(order);
+};
+
+/**
+ * Order items by their start data
+ * @param {Item[]} items
+ */
+Stack.prototype.orderByStart = function orderByStart(items) {
+ items.sort(function (a, b) {
+ return a.data.start - b.data.start;
+ });
+};
+
+/**
+ * Order items by their end date. If they have no end date, their start date
+ * is used.
+ * @param {Item[]} items
+ */
+Stack.prototype.orderByEnd = function orderByEnd(items) {
+ items.sort(function (a, b) {
+ var aTime = ('end' in a.data) ? a.data.end : a.data.start,
+ bTime = ('end' in b.data) ? b.data.end : b.data.start;
+
+ return aTime - bTime;
+ });
+};
+
+/**
+ * Adjust vertical positions of the events such that they don't overlap each
+ * other.
+ * @param {Item[]} items All visible items
+ * @param {boolean} [force=false] If true, all items will be re-stacked.
+ * If false (default), only items having a
+ * top===null will be re-stacked
+ * @private
+ */
+Stack.prototype.stack = function stack (items, force) {
+ var i,
+ iMax,
+ options = this.options,
+ marginItem,
+ marginAxis;
+
+ if (options.margin && options.margin.item !== undefined) {
+ marginItem = options.margin.item;
+ }
+ else {
+ marginItem = this.defaultOptions.margin.item
+ }
+ if (options.margin && options.margin.axis !== undefined) {
+ marginAxis = options.margin.axis;
+ }
+ else {
+ marginAxis = this.defaultOptions.margin.axis
+ }
+
+ if (force) {
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ items[i].top = null;
+ }
+ }
+
+ // calculate new, non-overlapping positions
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ var item = items[i];
+ if (item.top === null) {
+ // initialize top position
+ item.top = marginAxis;
+
+ do {
+ // TODO: optimize checking for overlap. when there is a gap without items,
+ // you only need to check for items from the next item on, not from zero
+ var collidingItem = null;
+ for (var j = 0, jj = items.length; j < jj; j++) {
+ var other = items[j];
+ if (other.top !== null && other !== item && this.collision(item, other, marginItem)) {
+ collidingItem = other;
+ break;
+ }
+ }
+
+ if (collidingItem != null) {
+ // There is a collision. Reposition the event above the colliding element
+ item.top = collidingItem.top + collidingItem.height + marginItem;
+ }
+ } while (collidingItem);
+ }
+ }
+};
+
+/**
+ * Test if the two provided items collide
+ * The items must have parameters left, width, top, and height.
+ * @param {Component} a The first item
+ * @param {Component} b The second item
+ * @param {Number} margin A minimum required margin.
+ * If margin is provided, the two items will be
+ * marked colliding when they overlap or
+ * when the margin between the two is smaller than
+ * the requested margin.
+ * @return {boolean} true if a and b collide, else false
+ */
+Stack.prototype.collision = function 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);
+};
+
+/**
+ * @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 {RootPanel} root Root panel, used to subscribe to events
+ * @param {Panel} parent Parent panel, used to attach to the DOM
+ * @param {Object} [options] See description at Range.setOptions
+ */
+function Range(root, parent, options) {
+ this.id = util.randomUUID();
+ this.start = null; // Number
+ this.end = null; // Number
+
+ this.root = root;
+ this.parent = parent;
+ this.options = options || {};
+
+ // drag listeners for dragging
+ this.root.on('dragstart', this._onDragStart.bind(this));
+ this.root.on('drag', this._onDrag.bind(this));
+ this.root.on('dragend', this._onDragEnd.bind(this));
+
+ // ignore dragging when holding
+ this.root.on('hold', this._onHold.bind(this));
+
+ // mouse wheel for zooming
+ this.root.on('mousewheel', this._onMouseWheel.bind(this));
+ this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
+
+ // pinch to zoom
+ this.root.on('touch', this._onTouch.bind(this));
+ this.root.on('pinch', this._onPinch.bind(this));
+
+ this.setOptions(options);
+}
+
+// turn Range into an event emitter
+Emitter(Range.prototype);
+
+/**
+ * Set options for the range controller
+ * @param {Object} options Available options:
+ * {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);
+
+ // re-apply range with new limitations
+ if (this.start !== null && this.end !== null) {
+ this.setRange(this.start, this.end);
+ }
+};
+
+/**
+ * Test whether direction has a valid value
+ * @param {String} direction 'horizontal' or 'vertical'
+ */
+function validateDirection (direction) {
+ if (direction != 'horizontal' && direction != 'vertical') {
+ throw new TypeError('Unknown direction "' + direction + '". ' +
+ 'Choose "horizontal" or "vertical".');
+ }
+}
+
+/**
+ * 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) {
+ var params = {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ };
+ this.emit('rangechange', params);
+ this.emit('rangechanged', params);
+ }
+};
+
+/**
+ * Set a new start and end range. This method is the same as setRange, but
+ * does not trigger a range change and range changed event, and it returns
+ * true when the range is changed
+ * @param {Number} [start]
+ * @param {Number} [end]
+ * @return {Boolean} changed
+ * @private
+ */
+Range.prototype._applyRange = function(start, end) {
+ var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
+ newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
+ max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
+ min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
+ diff;
+
+ // check for valid number
+ if (isNaN(newStart) || newStart === null) {
+ throw new Error('Invalid start "' + start + '"');
+ }
+ if (isNaN(newEnd) || newEnd === null) {
+ throw new Error('Invalid end "' + end + '"');
+ }
+
+ // prevent start < end
+ if (newEnd < newStart) {
+ newEnd = newStart;
+ }
+
+ // prevent start < min
+ if (min !== null) {
+ if (newStart < min) {
+ diff = (min - newStart);
+ newStart += diff;
+ newEnd += diff;
+
+ // prevent end > max
+ if (max != null) {
+ if (newEnd > max) {
+ newEnd = max;
+ }
+ }
+ }
+ }
+
+ // prevent end > max
+ if (max !== null) {
+ if (newEnd > max) {
+ diff = (newEnd - max);
+ newStart -= diff;
+ newEnd -= diff;
+
+ // prevent start < min
+ if (min != null) {
+ if (newStart < min) {
+ newStart = min;
+ }
+ }
+ }
+ }
+
+ // prevent (end-start) < zoomMin
+ if (this.options.zoomMin !== null) {
+ var zoomMin = parseFloat(this.options.zoomMin);
+ if (zoomMin < 0) {
+ zoomMin = 0;
+ }
+ if ((newEnd - newStart) < zoomMin) {
+ if ((this.end - this.start) === zoomMin) {
+ // ignore this action, we are already zoomed to the minimum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ else {
+ // zoom to the minimum
+ diff = (zoomMin - (newEnd - newStart));
+ newStart -= diff / 2;
+ newEnd += diff / 2;
+ }
+ }
+ }
+
+ // prevent (end-start) > zoomMax
+ if (this.options.zoomMax !== null) {
+ var zoomMax = parseFloat(this.options.zoomMax);
+ if (zoomMax < 0) {
+ zoomMax = 0;
+ }
+ if ((newEnd - newStart) > zoomMax) {
+ if ((this.end - this.start) === zoomMax) {
+ // ignore this action, we are already zoomed to the maximum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ else {
+ // zoom to the maximum
+ diff = ((newEnd - newStart) - zoomMax);
+ newStart += diff / 2;
+ newEnd -= diff / 2;
+ }
+ }
+ }
+
+ var changed = (this.start != newStart || this.end != newEnd);
+
+ this.start = newStart;
+ this.end = newEnd;
+
+ return changed;
+};
+
+/**
+ * Retrieve the current range.
+ * @return {Object} An object with start and end properties
+ */
+Range.prototype.getRange = function() {
+ return {
+ start: this.start,
+ end: this.end
+ };
+};
+
+/**
+ * Calculate the conversion offset and scale for current range, based on
+ * the provided width
+ * @param {Number} width
+ * @returns {{offset: number, scale: number}} conversion
+ */
+Range.prototype.conversion = function (width) {
+ return Range.conversion(this.start, this.end, width);
+};
+
+/**
+ * Static method to calculate the conversion offset and scale for a range,
+ * based on the provided start, end, and width
+ * @param {Number} start
+ * @param {Number} end
+ * @param {Number} width
+ * @returns {{offset: number, scale: number}} conversion
+ */
+Range.conversion = function (start, end, width) {
+ if (width != 0 && (end - start != 0)) {
+ return {
+ offset: start,
+ scale: width / (end - start)
+ }
+ }
+ else {
+ return {
+ offset: 0,
+ scale: 1
+ };
+ }
+};
+
+// global (private) object to store drag params
+var touchParams = {};
+
+/**
+ * Start dragging horizontally or vertically
+ * @param {Event} event
+ * @private
+ */
+Range.prototype._onDragStart = function(event) {
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (touchParams.ignore) return;
+
+ // TODO: reckon with option movable
+
+ touchParams.start = this.start;
+ touchParams.end = this.end;
+
+ var frame = this.parent.frame;
+ if (frame) {
+ frame.style.cursor = 'move';
+ }
+};
+
+/**
+ * Perform dragging operating.
+ * @param {Event} event
+ * @private
+ */
+Range.prototype._onDrag = function (event) {
+ var direction = this.options.direction;
+ validateDirection(direction);
+
+ // TODO: reckon with option movable
+
+
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (touchParams.ignore) return;
+
+ var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
+ interval = (touchParams.end - touchParams.start),
+ width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
+ diffRange = -delta / width * interval;
+
+ this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
+
+ this.emit('rangechange', {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ });
+};
+
+/**
+ * Stop dragging operating.
+ * @param {event} event
+ * @private
+ */
+Range.prototype._onDragEnd = function (event) {
+ // refuse to drag when we where pinching to prevent the timeline make a jump
+ // when releasing the fingers in opposite order from the touch screen
+ if (touchParams.ignore) return;
+
+ // TODO: reckon with option movable
+
+ if (this.parent.frame) {
+ this.parent.frame.style.cursor = 'auto';
+ }
+
+ // fire a rangechanged event
+ this.emit('rangechanged', {
+ start: new Date(this.start),
+ end: new Date(this.end)
+ });
+};
+
+/**
+ * Event handler for mouse wheel event, used to zoom
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {Event} event
+ * @private
+ */
+Range.prototype._onMouseWheel = function(event) {
+ // TODO: reckon with option zoomable
+
+ // retrieve delta
+ var delta = 0;
+ if (event.wheelDelta) { /* IE/Opera. */
+ delta = event.wheelDelta / 120;
+ } else if (event.detail) { /* Mozilla case. */
+ // In Mozilla, sign of delta is different than in IE.
+ // Also, delta is multiple of 3.
+ delta = -event.detail / 3;
+ }
+
+ // If delta is nonzero, handle it.
+ // Basically, delta is now positive if wheel was scrolled up,
+ // and negative, if wheel was scrolled down.
+ if (delta) {
+ // perform the zoom action. Delta is normally 1 or -1
+
+ // adjust a negative delta such that zooming in with delta 0.1
+ // equals zooming out with a delta -0.1
+ var scale;
+ if (delta < 0) {
+ scale = 1 - (delta / 5);
+ }
+ else {
+ scale = 1 / (1 + (delta / 5)) ;
+ }
+
+ // calculate center, the date to zoom around
+ var gesture = util.fakeGesture(this, event),
+ pointer = getPointer(gesture.center, this.parent.frame),
+ pointerDate = this._pointerToDate(pointer);
+
+ this.zoom(scale, pointerDate);
+ }
+
+ // Prevent default actions caused by mouse wheel
+ // (else the page and timeline both zoom and scroll)
+ event.preventDefault();
+};
+
+/**
+ * Start of a touch gesture
+ * @private
+ */
+Range.prototype._onTouch = function (event) {
+ touchParams.start = this.start;
+ touchParams.end = this.end;
+ touchParams.ignore = false;
+ touchParams.center = null;
+
+ // don't move the range when dragging a selected event
+ // TODO: it's not so neat to have to know about the state of the ItemSet
+ var item = ItemSet.itemFromTarget(event);
+ if (item && item.selected && this.options.editable) {
+ touchParams.ignore = true;
+ }
+};
+
+/**
+ * On start of a hold gesture
+ * @private
+ */
+Range.prototype._onHold = function () {
+ touchParams.ignore = true;
+};
+
+/**
+ * Handle pinch event
+ * @param {Event} event
+ * @private
+ */
+Range.prototype._onPinch = function (event) {
+ var direction = this.options.direction;
+ touchParams.ignore = true;
+
+ // TODO: reckon with option zoomable
+
+ if (event.gesture.touches.length > 1) {
+ if (!touchParams.center) {
+ touchParams.center = getPointer(event.gesture.center, this.parent.frame);
+ }
+
+ var scale = 1 / event.gesture.scale,
+ initDate = this._pointerToDate(touchParams.center),
+ center = getPointer(event.gesture.center, this.parent.frame),
+ date = this._pointerToDate(this.parent, center),
+ delta = date - initDate; // TODO: utilize delta
+
+ // calculate new start and end
+ var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
+ var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
+
+ // apply new range
+ this.setRange(newStart, newEnd);
+ }
+};
+
+/**
+ * Helper function to calculate the center date for zooming
+ * @param {{x: Number, y: Number}} pointer
+ * @return {number} date
+ * @private
+ */
+Range.prototype._pointerToDate = function (pointer) {
+ var conversion;
+ var direction = this.options.direction;
+
+ validateDirection(direction);
+
+ if (direction == 'horizontal') {
+ var width = this.parent.width;
+ conversion = this.conversion(width);
+ return pointer.x / conversion.scale + conversion.offset;
+ }
+ else {
+ var height = this.parent.height;
+ conversion = this.conversion(height);
+ return pointer.y / conversion.scale + conversion.offset;
+ }
+};
+
+/**
+ * Get the pointer location relative to the location of the dom element
+ * @param {{pageX: Number, pageY: Number}} touch
+ * @param {Element} element HTML DOM element
+ * @return {{x: Number, y: Number}} pointer
+ * @private
+ */
+function getPointer (touch, element) {
+ return {
+ x: touch.pageX - vis.util.getAbsoluteLeft(element),
+ y: touch.pageY - vis.util.getAbsoluteTop(element)
+ };
+}
+
+/**
+ * Zoom the range the given scale in or out. Start and end date will
+ * be adjusted, and the timeline will be redrawn. You can optionally give a
+ * date around which to zoom.
+ * For example, try scale = 0.9 or 1.1
+ * @param {Number} scale Scaling factor. Values above 1 will zoom out,
+ * values below 1 will zoom in.
+ * @param {Number} [center] Value representing a date around which will
+ * be zoomed.
+ */
+Range.prototype.zoom = function(scale, center) {
+ // if centerDate is not provided, take it half between start Date and end Date
+ if (center == null) {
+ center = (this.start + this.end) / 2;
+ }
+
+ // calculate new start and end
+ var newStart = center + (this.start - center) * scale;
+ var newEnd = center + (this.end - center) * scale;
+
+ this.setRange(newStart, newEnd);
+};
+
+/**
+ * Move the range with a given delta to the left or right. Start and end
+ * value will be adjusted. For example, try delta = 0.1 or -0.1
+ * @param {Number} delta Moving amount. Positive value will move right,
+ * negative value will move left
+ */
+Range.prototype.move = function(delta) {
+ // zoom start Date and end Date relative to the centerDate
+ var diff = (this.end - this.start);
+
+ // apply new values
+ var newStart = this.start + diff * delta;
+ var newEnd = this.end + diff * delta;
+
+ // TODO: reckon with min and max range
+
+ this.start = newStart;
+ this.end = newEnd;
+};
+
+/**
+ * Move the range to a new center point
+ * @param {Number} moveTo New center point of the range
+ */
+Range.prototype.moveTo = function(moveTo) {
+ var center = (this.start + this.end) / 2;
+
+ var diff = center - moveTo;
+
+ // calculate new start and end
+ var newStart = this.start - diff;
+ var newEnd = this.end - diff;
+
+ this.setRange(newStart, newEnd);
+};
+
+/**
+ * Prototype for visual components
+ */
+function Component () {
+ this.id = null;
+ this.parent = null;
+ this.childs = null;
+ this.options = null;
+
+ this.top = 0;
+ this.left = 0;
+ this.width = 0;
+ this.height = 0;
+}
+
+// Turn the Component into an event emitter
+Emitter(Component.prototype);
+
+/**
+ * Set parameters for the frame. Parameters will be merged in current parameter
+ * set.
+ * @param {Object} options Available parameters:
+ * {String | function} [className]
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ */
+Component.prototype.setOptions = function setOptions(options) {
+ if (options) {
+ util.extend(this.options, options);
+
+ this.repaint();
+ }
+};
+
+/**
+ * Get an option value by name
+ * The function will first check this.options object, and else will check
+ * this.defaultOptions.
+ * @param {String} name
+ * @return {*} value
+ */
+Component.prototype.getOption = function getOption(name) {
+ var value;
+ if (this.options) {
+ value = this.options[name];
+ }
+ if (value === undefined && this.defaultOptions) {
+ value = this.defaultOptions[name];
+ }
+ return value;
+};
+
+/**
+ * Get the frame element of the component, the outer HTML DOM element.
+ * @returns {HTMLElement | null} frame
+ */
+Component.prototype.getFrame = function getFrame() {
+ // should be implemented by the component
+ return null;
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+Component.prototype.repaint = function repaint() {
+ // should be implemented by the component
+ return false;
+};
+
+/**
+ * Test whether the component is resized since the last time _isResized() was
+ * called.
+ * @return {Boolean} Returns true if the component is resized
+ * @private
+ */
+Component.prototype._isResized = function _isResized() {
+ var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
+
+ this._previousWidth = this.width;
+ this._previousHeight = this.height;
+
+ return resized;
+};
+
+/**
+ * A panel can contain components
+ * @param {Object} [options] Available parameters:
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ * {String | function} [className]
+ * @constructor Panel
+ * @extends Component
+ */
+function Panel(options) {
+ this.id = util.randomUUID();
+ this.parent = null;
+ this.childs = [];
+
+ this.options = options || {};
+
+ // create frame
+ this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
+}
+
+Panel.prototype = new Component();
+
+/**
+ * Set options. Will extend the current options.
+ * @param {Object} [options] Available parameters:
+ * {String | function} [className]
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ */
+Panel.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Get the outer frame of the panel
+ * @returns {HTMLElement} frame
+ */
+Panel.prototype.getFrame = function () {
+ return this.frame;
+};
+
+/**
+ * Append a child to the panel
+ * @param {Component} child
+ */
+Panel.prototype.appendChild = function (child) {
+ this.childs.push(child);
+ child.parent = this;
+
+ // attach to the DOM
+ var frame = child.getFrame();
+ if (frame) {
+ if (frame.parentNode) {
+ frame.parentNode.removeChild(frame);
+ }
+ this.frame.appendChild(frame);
+ }
+};
+
+/**
+ * Insert a child to the panel
+ * @param {Component} child
+ * @param {Component} beforeChild
+ */
+Panel.prototype.insertBefore = function (child, beforeChild) {
+ var index = this.childs.indexOf(beforeChild);
+ if (index != -1) {
+ this.childs.splice(index, 0, child);
+ child.parent = this;
+
+ // attach to the DOM
+ var frame = child.getFrame();
+ if (frame) {
+ if (frame.parentNode) {
+ frame.parentNode.removeChild(frame);
+ }
+
+ var beforeFrame = beforeChild.getFrame();
+ if (beforeFrame) {
+ this.frame.insertBefore(frame, beforeFrame);
+ }
+ else {
+ this.frame.appendChild(frame);
+ }
+ }
+ }
+};
+
+/**
+ * Remove a child from the panel
+ * @param {Component} child
+ */
+Panel.prototype.removeChild = function (child) {
+ var index = this.childs.indexOf(child);
+ if (index != -1) {
+ this.childs.splice(index, 1);
+ child.parent = null;
+
+ // remove from the DOM
+ var frame = child.getFrame();
+ if (frame && frame.parentNode) {
+ this.frame.removeChild(frame);
+ }
+ }
+};
+
+/**
+ * Test whether the panel contains given child
+ * @param {Component} child
+ */
+Panel.prototype.hasChild = function (child) {
+ var index = this.childs.indexOf(child);
+ return (index != -1);
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component was resized since previous repaint
+ */
+Panel.prototype.repaint = function () {
+ var asString = util.option.asString,
+ options = this.options,
+ frame = this.getFrame();
+
+ // update className
+ frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
+
+ // repaint the child components
+ var childsResized = this._repaintChilds();
+
+ // update frame size
+ this._updateSize();
+
+ return this._isResized() || childsResized;
+};
+
+/**
+ * Repaint all childs of the panel
+ * @return {boolean} Returns true if the component is resized
+ * @private
+ */
+Panel.prototype._repaintChilds = function () {
+ var resized = false;
+ for (var i = 0, ii = this.childs.length; i < ii; i++) {
+ resized = this.childs[i].repaint() || resized;
+ }
+ return resized;
+};
+
+/**
+ * Apply the size from options to the panel, and recalculate it's actual size.
+ * @private
+ */
+Panel.prototype._updateSize = function () {
+ // apply size
+ this.frame.style.top = util.option.asSize(this.options.top);
+ this.frame.style.bottom = util.option.asSize(this.options.bottom);
+ this.frame.style.left = util.option.asSize(this.options.left);
+ this.frame.style.right = util.option.asSize(this.options.right);
+ this.frame.style.width = util.option.asSize(this.options.width, '100%');
+ this.frame.style.height = util.option.asSize(this.options.height, '');
+
+ // get actual size
+ this.top = this.frame.offsetTop;
+ this.left = this.frame.offsetLeft;
+ this.width = this.frame.offsetWidth;
+ this.height = this.frame.offsetHeight;
+};
+
+/**
+ * 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
+ };
+
+ // create the HTML DOM
+ this._create();
+
+ // attach the root panel to the provided container
+ if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
+ this.container.appendChild(this.getFrame());
+
+
+ this._initWatch();
+}
+
+RootPanel.prototype = new Panel();
+
+/**
+ * Create the HTML DOM for the root panel
+ */
+RootPanel.prototype._create = function _create() {
+ // create frame
+ this.frame = document.createElement('div');
+
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.frame, {
+ prevent_default: true
+ });
+ this.listeners = {};
+
+ var me = this;
+ var events = [
+ 'touch', 'pinch', 'tap', 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
+ ];
+ events.forEach(function (event) {
+ var listener = function () {
+ var args = [event].concat(Array.prototype.slice.call(arguments, 0));
+ me.emit.apply(me, args);
+ };
+ me.hammer.on(event, listener);
+ me.listeners[event] = listener;
+ });
+};
+
+/**
+ * Set options. Will extend the current options.
+ * @param {Object} [options] Available parameters:
+ * {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 = function setOptions(options) {
+ if (options) {
+ util.extend(this.options, options);
+
+ this.repaint();
+
+ this._initWatch();
+ }
+};
+
+/**
+ * Get the frame of the root panel
+ */
+RootPanel.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
+/**
+ * Repaint the root panel
+ */
+RootPanel.prototype.repaint = function repaint() {
+ // update class name
+ var options = this.options;
+ var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
+ if (options.className) className += ' ' + util.option.asString(className);
+ this.frame.className = className;
+
+ // repaint the child components
+ var childsResized = this._repaintChilds();
+
+ // update frame size
+ this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
+ this._updateSize();
+
+ // if the root panel or any of its childs is resized, repaint again,
+ // as other components may need to be resized accordingly
+ var resized = this._isResized() || childsResized;
+ if (resized) {
+ setTimeout(this.repaint.bind(this), 0);
+ }
+};
+
+/**
+ * Initialize watching when option autoResize is true
+ * @private
+ */
+RootPanel.prototype._initWatch = function _initWatch() {
+ var autoResize = this.getOption('autoResize');
+ if (autoResize) {
+ this._watch();
+ }
+ else {
+ this._unwatch();
+ }
+};
+
+/**
+ * Watch for changes in the size of the frame. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
+ */
+RootPanel.prototype._watch = function _watch() {
+ var me = this;
+
+ this._unwatch();
+
+ var checkSize = function checkSize() {
+ var autoResize = me.getOption('autoResize');
+ if (!autoResize) {
+ // stop watching when the option autoResize is changed to false
+ me._unwatch();
+ return;
+ }
+
+ if (me.frame) {
+ // check whether the frame is resized
+ if ((me.frame.clientWidth != me.lastWidth) ||
+ (me.frame.clientHeight != me.lastHeight)) {
+ me.lastWidth = me.frame.clientWidth;
+ me.lastHeight = me.frame.clientHeight;
+ me.repaint();
+ // TODO: emit a resize event instead?
+ }
+ }
+ };
+
+ // TODO: automatically cleanup the event listener when the frame is deleted
+ util.addEventListener(window, 'resize', checkSize);
+
+ this.watchTimer = setInterval(checkSize, 1000);
+};
+
+/**
+ * Stop watching for a resize of the frame.
+ * @private
+ */
+RootPanel.prototype._unwatch = function _unwatch() {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
+ }
+
+ // TODO: remove event listener on window.resize
+};
+
+/**
+ * A horizontal time axis
+ * @param {Object} [options] See TimeAxis.setOptions for the available
+ * options.
+ * @constructor TimeAxis
+ * @extends Component
+ */
+function TimeAxis (options) {
+ this.id = util.randomUUID();
+
+ this.dom = {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: [],
+ redundant: {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: []
+ }
+ };
+ this.props = {
+ range: {
+ start: 0,
+ end: 0,
+ minimumStep: 0
+ },
+ lineTop: 0
+ };
+
+ this.options = options || {};
+ this.defaultOptions = {
+ orientation: 'bottom', // supported: 'top', 'bottom'
+ // TODO: implement timeaxis orientations 'left' and 'right'
+ showMinorLabels: true,
+ showMajorLabels: true
+ };
+
+ this.range = null;
+
+ // create the HTML DOM
+ this._create();
+}
+
+TimeAxis.prototype = new Component();
+
+// TODO: comment options
+TimeAxis.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Create the HTML DOM for the TimeAxis
+ */
+TimeAxis.prototype._create = function _create() {
+ this.frame = document.createElement('div');
+};
+
+/**
+ * Set a range (start and end)
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+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;
+};
+
+/**
+ * Get the outer frame of the time axis
+ * @return {HTMLElement} frame
+ */
+TimeAxis.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+TimeAxis.prototype.repaint = function () {
+ var asSize = util.option.asSize,
+ options = this.options,
+ props = this.props,
+ frame = this.frame;
+
+ // update classname
+ frame.className = 'timeaxis'; // TODO: add className from options if defined
+
+ var parent = frame.parentNode;
+ if (parent) {
+ // calculate character width and height
+ this._calculateCharSize();
+
+ // TODO: recalculate sizes only needed when parent is resized or options is changed
+ var orientation = this.getOption('orientation'),
+ showMinorLabels = this.getOption('showMinorLabels'),
+ showMajorLabels = this.getOption('showMajorLabels');
+
+ // determine the width and height of the elemens for the axis
+ var parentHeight = this.parent.height;
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+ this.height = props.minorLabelHeight + props.majorLabelHeight;
+ this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
+
+ props.minorLineHeight = parentHeight + props.minorLabelHeight;
+ props.minorLineWidth = 1; // TODO: really calculate width
+ props.majorLineHeight = parentHeight + this.height;
+ props.majorLineWidth = 1; // TODO: really calculate width
+
+ // take frame offline while updating (is almost twice as fast)
+ var beforeChild = frame.nextSibling;
+ parent.removeChild(frame);
+
+ // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
+ if (orientation == 'top') {
+ frame.style.top = '0';
+ frame.style.left = '0';
+ frame.style.bottom = '';
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = this.height + 'px';
+ }
+ else { // bottom
+ frame.style.top = '';
+ frame.style.bottom = '0';
+ frame.style.left = '0';
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = this.height + 'px';
+ }
+
+ this._repaintLabels();
+
+ this._repaintLine();
+
+ // put frame online again
+ if (beforeChild) {
+ parent.insertBefore(frame, beforeChild);
+ }
+ else {
+ parent.appendChild(frame)
+ }
+ }
+
+ return this._isResized();
+};
+
+/**
+ * Repaint major and minor text labels and vertical grid lines
+ * @private
+ */
+TimeAxis.prototype._repaintLabels = function () {
+ var orientation = this.getOption('orientation');
+
+ // calculate range and step
+ var start = util.convert(this.range.start, 'Number'),
+ end = util.convert(this.range.end, 'Number'),
+ minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 5).valueOf()
+ -this.options.toTime(0).valueOf();
+ var step = new TimeStep(new Date(start), new Date(end), minimumStep);
+ this.step = step;
+
+ // Move all DOM elements to a "redundant" list, where they
+ // can be picked for re-use, and clear the lists with lines and texts.
+ // At the end of the function _repaintLabels, left over elements will be cleaned up
+ var dom = this.dom;
+ dom.redundant.majorLines = dom.majorLines;
+ dom.redundant.majorTexts = dom.majorTexts;
+ dom.redundant.minorLines = dom.minorLines;
+ dom.redundant.minorTexts = dom.minorTexts;
+ dom.majorLines = [];
+ dom.majorTexts = [];
+ dom.minorLines = [];
+ dom.minorTexts = [];
+
+ step.first();
+ var xFirstMajorLabel = undefined;
+ var max = 0;
+ while (step.hasNext() && max < 1000) {
+ max++;
+ var cur = step.getCurrent(),
+ x = this.options.toScreen(cur),
+ isMajor = step.isMajor();
+
+ // TODO: lines must have a width, such that we can create css backgrounds
+
+ if (this.getOption('showMinorLabels')) {
+ this._repaintMinorText(x, step.getLabelMinor(), orientation);
+ }
+
+ if (isMajor && this.getOption('showMajorLabels')) {
+ if (x > 0) {
+ if (xFirstMajorLabel == undefined) {
+ xFirstMajorLabel = x;
+ }
+ this._repaintMajorText(x, step.getLabelMajor(), orientation);
+ }
+ this._repaintMajorLine(x, orientation);
+ }
+ else {
+ this._repaintMinorLine(x, orientation);
+ }
+
+ step.next();
+ }
+
+ // create a major label on the left when needed
+ if (this.getOption('showMajorLabels')) {
+ var leftTime = this.options.toTime(0),
+ leftText = step.getLabelMajor(leftTime),
+ widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
+
+ if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
+ this._repaintMajorText(0, leftText, orientation);
+ }
+ }
+
+ // Cleanup leftover DOM elements from the redundant list
+ util.forEach(this.dom.redundant, function (arr) {
+ while (arr.length) {
+ var elem = arr.pop();
+ if (elem && elem.parentNode) {
+ elem.parentNode.removeChild(elem);
+ }
+ }
+ });
+};
+
+/**
+ * Create a minor label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ * @param {String} orientation "top" or "bottom" (default)
+ * @private
+ */
+TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
+ // reuse redundant label
+ var label = this.dom.redundant.minorTexts.shift();
+
+ if (!label) {
+ // create new label
+ var content = document.createTextNode('');
+ label = document.createElement('div');
+ label.appendChild(content);
+ label.className = 'text minor';
+ this.frame.appendChild(label);
+ }
+ this.dom.minorTexts.push(label);
+
+ label.childNodes[0].nodeValue = text;
+
+ if (orientation == 'top') {
+ label.style.top = this.props.majorLabelHeight + 'px';
+ label.style.bottom = '';
+ }
+ else {
+ label.style.top = '';
+ label.style.bottom = this.props.majorLabelHeight + 'px';
+ }
+ label.style.left = x + 'px';
+ //label.title = title; // TODO: this is a heavy operation
+};
+
+/**
+ * Create a Major label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ * @param {String} orientation "top" or "bottom" (default)
+ * @private
+ */
+TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
+ // reuse redundant label
+ var label = this.dom.redundant.majorTexts.shift();
+
+ if (!label) {
+ // create label
+ var content = document.createTextNode(text);
+ label = document.createElement('div');
+ label.className = 'text major';
+ label.appendChild(content);
+ this.frame.appendChild(label);
+ }
+ this.dom.majorTexts.push(label);
+
+ label.childNodes[0].nodeValue = text;
+ //label.title = title; // TODO: this is a heavy operation
+
+ if (orientation == 'top') {
+ label.style.top = '0px';
+ label.style.bottom = '';
+ }
+ else {
+ label.style.top = '';
+ label.style.bottom = '0px';
+ }
+ label.style.left = x + 'px';
+};
+
+/**
+ * Create a minor line for the axis at position x
+ * @param {Number} x
+ * @param {String} orientation "top" or "bottom" (default)
+ * @private
+ */
+TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
+ // reuse redundant line
+ var line = this.dom.redundant.minorLines.shift();
+
+ if (!line) {
+ // create vertical line
+ line = document.createElement('div');
+ line.className = 'grid vertical minor';
+ this.frame.appendChild(line);
+ }
+ this.dom.minorLines.push(line);
+
+ var props = this.props;
+ if (orientation == 'top') {
+ line.style.top = this.props.majorLabelHeight + 'px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = this.props.majorLabelHeight + 'px';
+ }
+ line.style.height = props.minorLineHeight + 'px';
+ line.style.left = (x - props.minorLineWidth / 2) + 'px';
+};
+
+/**
+ * Create a Major line for the axis at position x
+ * @param {Number} x
+ * @param {String} orientation "top" or "bottom" (default)
+ * @private
+ */
+TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
+ // reuse redundant line
+ var line = this.dom.redundant.majorLines.shift();
+
+ if (!line) {
+ // create vertical line
+ line = document.createElement('DIV');
+ line.className = 'grid vertical major';
+ this.frame.appendChild(line);
+ }
+ this.dom.majorLines.push(line);
+
+ var props = this.props;
+ if (orientation == 'top') {
+ line.style.top = '0px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = '0px';
+ }
+ line.style.left = (x - props.majorLineWidth / 2) + 'px';
+ line.style.height = props.majorLineHeight + 'px';
+};
+
+
+/**
+ * Repaint the horizontal line for the axis
+ * @private
+ */
+TimeAxis.prototype._repaintLine = function() {
+ var line = this.dom.line,
+ frame = this.frame,
+ orientation = this.getOption('orientation');
+
+ // line before all axis elements
+ if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
+ if (line) {
+ // put this line at the end of all childs
+ frame.removeChild(line);
+ frame.appendChild(line);
+ }
+ else {
+ // create the axis line
+ line = document.createElement('div');
+ line.className = 'grid horizontal major';
+ frame.appendChild(line);
+ this.dom.line = line;
+ }
+
+ if (orientation == 'top') {
+ line.style.top = this.height + 'px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = this.height + 'px';
+ }
+ }
+ else {
+ if (line && line.parentNode) {
+ line.parentNode.removeChild(line);
+ delete this.dom.line;
+ }
+ }
+};
+
+/**
+ * Determine the size of text on the axis (both major and minor axis).
+ * The size is calculated only once and then cached in this.props.
+ * @private
+ */
+TimeAxis.prototype._calculateCharSize = function () {
+ // determine the char width and height on the minor axis
+ if (!('minorCharHeight' in this.props)) {
+ var textMinor = document.createTextNode('0');
+ var measureCharMinor = document.createElement('DIV');
+ measureCharMinor.className = 'text minor measure';
+ measureCharMinor.appendChild(textMinor);
+ this.frame.appendChild(measureCharMinor);
+
+ this.props.minorCharHeight = measureCharMinor.clientHeight;
+ this.props.minorCharWidth = measureCharMinor.clientWidth;
+
+ this.frame.removeChild(measureCharMinor);
+ }
+
+ if (!('majorCharHeight' in this.props)) {
+ var textMajor = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'text major measure';
+ measureCharMajor.appendChild(textMajor);
+ this.frame.appendChild(measureCharMajor);
+
+ this.props.majorCharHeight = measureCharMajor.clientHeight;
+ this.props.majorCharWidth = measureCharMajor.clientWidth;
+
+ this.frame.removeChild(measureCharMajor);
+ }
+};
+
+/**
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
+ */
+TimeAxis.prototype.snap = function snap (date) {
+ return this.step.snap(date);
+};
+
+/**
+ * A current time bar
+ * @param {Range} range
+ * @param {Object} [options] Available parameters:
+ * {Boolean} [showCurrentTime]
+ * @constructor CurrentTime
+ * @extends Component
+ */
+
+function CurrentTime (range, options) {
+ this.id = util.randomUUID();
+
+ this.range = range;
+ this.options = options || {};
+ this.defaultOptions = {
+ showCurrentTime: false
+ };
+
+ this._create();
+}
+
+CurrentTime.prototype = new Component();
+
+CurrentTime.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Create the HTML DOM for the current time bar
+ * @private
+ */
+CurrentTime.prototype._create = function _create () {
+ var bar = document.createElement('div');
+ bar.className = 'currenttime';
+ bar.style.position = 'absolute';
+ bar.style.top = '0px';
+ bar.style.height = '100%';
+
+ this.bar = bar;
+};
+
+/**
+ * Get the frame element of the current time bar
+ * @returns {HTMLElement} frame
+ */
+CurrentTime.prototype.getFrame = function getFrame() {
+ return this.bar;
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+CurrentTime.prototype.repaint = function repaint() {
+ var parent = this.parent;
+
+ var now = new Date();
+ var x = this.options.toScreen(now);
+
+ this.bar.style.left = x + 'px';
+ this.bar.title = 'Current time: ' + now;
+
+ return false;
+};
+
+/**
+ * Start auto refreshing the current time bar
+ */
+CurrentTime.prototype.start = function start() {
+ var me = this;
+
+ function update () {
+ me.stop();
+
+ // determine interval to refresh
+ var scale = me.range.conversion(me.parent.width).scale;
+ var interval = 1 / scale / 10;
+ if (interval < 30) interval = 30;
+ if (interval > 1000) interval = 1000;
+
+ me.repaint();
+
+ // start a timer to adjust for the new time
+ me.currentTimeTimer = setTimeout(update, interval);
+ }
+
+ update();
+};
+
+/**
+ * Stop auto refreshing the current time bar
+ */
+CurrentTime.prototype.stop = function stop() {
+ if (this.currentTimeTimer !== undefined) {
+ clearTimeout(this.currentTimeTimer);
+ delete this.currentTimeTimer;
+ }
+};
+
+/**
+ * A custom time bar
+ * @param {Object} [options] Available parameters:
+ * {Boolean} [showCustomTime]
+ * @constructor CustomTime
+ * @extends Component
+ */
+
+function CustomTime (options) {
+ this.id = util.randomUUID();
+
+ this.options = options || {};
+ this.defaultOptions = {
+ showCustomTime: false
+ };
+
+ this.customTime = new Date();
+ this.eventParams = {}; // stores state parameters while dragging the bar
+
+ // create the DOM
+ this._create();
+}
+
+CustomTime.prototype = new Component();
+
+CustomTime.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Create the DOM for the custom time
+ * @private
+ */
+CustomTime.prototype._create = function _create () {
+ var bar = document.createElement('div');
+ bar.className = 'customtime';
+ bar.style.position = 'absolute';
+ bar.style.top = '0px';
+ bar.style.height = '100%';
+ this.bar = bar;
+
+ var drag = document.createElement('div');
+ drag.style.position = 'relative';
+ drag.style.top = '0px';
+ drag.style.left = '-10px';
+ drag.style.height = '100%';
+ drag.style.width = '20px';
+ bar.appendChild(drag);
+
+ // attach event listeners
+ this.hammer = Hammer(bar, {
+ prevent_default: true
+ });
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
+ this.hammer.on('drag', this._onDrag.bind(this));
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
+};
+
+/**
+ * Get the frame element of the custom time bar
+ * @returns {HTMLElement} frame
+ */
+CustomTime.prototype.getFrame = function getFrame() {
+ return this.bar;
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+CustomTime.prototype.repaint = function () {
+ var x = this.options.toScreen(this.customTime);
+
+ this.bar.style.left = x + 'px';
+ this.bar.title = 'Time: ' + this.customTime;
+
+ return false;
+};
+
+/**
+ * Set custom time.
+ * @param {Date} time
+ */
+CustomTime.prototype.setCustomTime = function(time) {
+ this.customTime = new Date(time.valueOf());
+ this.repaint();
+};
+
+/**
+ * Retrieve the current custom time.
+ * @return {Date} customTime
+ */
+CustomTime.prototype.getCustomTime = function() {
+ return new Date(this.customTime.valueOf());
+};
+
+/**
+ * Start moving horizontally
+ * @param {Event} event
+ * @private
+ */
+CustomTime.prototype._onDragStart = function(event) {
+ this.eventParams.dragging = true;
+ this.eventParams.customTime = this.customTime;
+
+ event.stopPropagation();
+ event.preventDefault();
+};
+
+/**
+ * Perform moving operating.
+ * @param {Event} event
+ * @private
+ */
+CustomTime.prototype._onDrag = function (event) {
+ if (!this.eventParams.dragging) return;
+
+ var deltaX = event.gesture.deltaX,
+ x = this.options.toScreen(this.eventParams.customTime) + deltaX,
+ time = this.options.toTime(x);
+
+ this.setCustomTime(time);
+
+ // fire a timechange event
+ this.emit('timechange', {
+ time: new Date(this.customTime.valueOf())
+ });
+
+ event.stopPropagation();
+ event.preventDefault();
+};
+
+/**
+ * Stop moving operating.
+ * @param {event} event
+ * @private
+ */
+CustomTime.prototype._onDragEnd = function (event) {
+ if (!this.eventParams.dragging) return;
+
+ // fire a timechanged event
+ this.emit('timechanged', {
+ time: new Date(this.customTime.valueOf())
+ });
+
+ event.stopPropagation();
+ event.preventDefault();
+};
+
+/**
+ * An ItemSet holds a set of items and ranges which can be displayed in a
+ * range. The width is determined by the parent of the ItemSet, and the height
+ * is determined by the size of the items.
+ * @param {Panel} backgroundPanel Panel which can be used to display the
+ * vertical lines of box items.
+ * @param {Panel} axisPanel Panel on the axis where the dots of box-items
+ * can be displayed.
+ * @param {Object} [options] See ItemSet.setOptions for the available options.
+ * @constructor ItemSet
+ * @extends Panel
+ */
+function ItemSet(backgroundPanel, axisPanel, options) {
+ this.id = util.randomUUID();
+
+ // one options object is shared by this itemset and all its items
+ this.options = options || {};
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
+ this.itemOptions = Object.create(this.options);
+ this.dom = {};
+ this.hammer = null;
+
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.range = null; // Range or Object {start: number, end: number}
+
+ // data change listeners
+ this.listeners = {
+ 'add': function (event, params, senderId) {
+ if (senderId != me.id) me._onAdd(params.items);
+ },
+ '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.orderedItems = {
+ byStart: [],
+ byEnd: []
+ };
+ this.visibleItems = []; // visible, ordered items
+ this.visibleItemsStart = 0; // start index of visible items in this.orderedItems // TODO: cleanup
+ this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems // TODO: cleanup
+ this.selection = []; // list with the ids of all selected nodes
+ this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
+ this.stack = new Stack(Object.create(this.options));
+ this.stackDirty = true; // if true, all items will be restacked on next repaint
+
+ this.touchParams = {}; // stores properties while dragging
+
+ // create the HTML DOM
+ this._create();
+}
+
+ItemSet.prototype = new Panel();
+
+// available item types will be registered here
+ItemSet.types = {
+ box: ItemBox,
+ range: ItemRange,
+ rangeoverflow: ItemRangeOverflow,
+ point: ItemPoint
+};
+
+/**
+ * Create the HTML DOM for the ItemSet
+ */
+ItemSet.prototype._create = function _create(){
+ var frame = document.createElement('div');
+ frame['timeline-itemset'] = this;
+ this.frame = frame;
+
+ // create background panel
+ var background = document.createElement('div');
+ background.className = 'background';
+ this.backgroundPanel.frame.appendChild(background);
+ this.dom.background = background;
+
+ // create foreground panel
+ var foreground = document.createElement('div');
+ foreground.className = 'foreground';
+ frame.appendChild(foreground);
+ this.dom.foreground = foreground;
+
+ // create axis panel
+ var axis = document.createElement('div');
+ axis.className = 'axis';
+ this.dom.axis = axis;
+ this.axisPanel.frame.appendChild(axis);
+
+ // attach event listeners
+ // TODO: use event listeners from the rootpanel to improve performance?
+ this.hammer = Hammer(frame, {
+ prevent_default: true
+ });
+ this.hammer.on('dragstart', this._onDragStart.bind(this));
+ this.hammer.on('drag', this._onDrag.bind(this));
+ this.hammer.on('dragend', this._onDragEnd.bind(this));
+};
+
+/**
+ * Set options for the ItemSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {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.
+ * {Function} snap
+ * Function to let items snap to nice dates when
+ * dragging items.
+ */
+ItemSet.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Hide the component from the DOM
+ */
+ItemSet.prototype.hide = function hide() {
+ // remove the axis with dots
+ if (this.dom.axis.parentNode) {
+ this.dom.axis.parentNode.removeChild(this.dom.axis);
+ }
+
+ // remove the background with vertical lines
+ if (this.dom.background.parentNode) {
+ this.dom.background.parentNode.removeChild(this.dom.background);
+ }
+};
+
+/**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+ItemSet.prototype.show = function show() {
+ // show axis with dots
+ if (!this.dom.axis.parentNode) {
+ this.axisPanel.frame.appendChild(this.dom.axis);
+ }
+
+ // show background with vertical lines
+ if (!this.dom.background.parentNode) {
+ this.backgroundPanel.frame.appendChild(this.dom.background);
+ }
+};
+
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+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;
+};
+
+/**
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {Array} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
+ */
+ItemSet.prototype.setSelection = function setSelection(ids) {
+ var i, ii, id, item;
+
+ if (ids) {
+ if (!Array.isArray(ids)) {
+ throw new TypeError('Array expected');
+ }
+
+ // unselect currently selected items
+ for (i = 0, ii = this.selection.length; i < ii; i++) {
+ id = this.selection[i];
+ item = this.items[id];
+ if (item) item.unselect();
+ }
+
+ // select items
+ this.selection = [];
+ for (i = 0, ii = ids.length; i < ii; i++) {
+ id = ids[i];
+ item = this.items[id];
+ if (item) {
+ this.selection.push(id);
+ item.select();
+ }
+ }
+ }
+};
+
+/**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+ItemSet.prototype.getSelection = function getSelection() {
+ return this.selection.concat([]);
+};
+
+/**
+ * Deselect a selected item
+ * @param {String | Number} id
+ * @private
+ */
+ItemSet.prototype._deselect = function _deselect(id) {
+ var selection = this.selection;
+ for (var i = 0, ii = selection.length; i < ii; i++) {
+ if (selection[i] == id) { // non-strict comparison!
+ selection.splice(i, 1);
+ break;
+ }
+ }
+};
+
+/**
+ * Return the item sets frame
+ * @returns {HTMLElement} frame
+ */
+ItemSet.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+ItemSet.prototype.repaint = function repaint() {
+ var asSize = util.option.asSize,
+ asString = util.option.asString,
+ options = this.options,
+ orientation = this.getOption('orientation'),
+ frame = this.frame;
+
+ // update className
+ frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
+
+ // check whether zoomed (in that case we need to re-stack everything)
+ var visibleInterval = this.range.end - this.range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ this.lastVisibleInterval = visibleInterval;
+ this.lastWidth = this.width;
+
+ /* TODO: implement+fix smarter way to update visible items
+ // find the first visible item
+ // TODO: use faster search, not linear
+ var byEnd = this.orderedItems.byEnd;
+ var start = 0;
+ var item = null;
+ while ((item = byEnd[start]) &&
+ (('end' in item.data) ? item.data.end : item.data.start) < this.range.start) {
+ start++;
+ }
+
+ // find the last visible item
+ // TODO: use faster search, not linear
+ var byStart = this.orderedItems.byStart;
+ var end = 0;
+ while ((item = byStart[end]) && item.data.start < this.range.end) {
+ end++;
+ }
+
+ console.log('visible items', start, end); // TODO: cleanup
+ console.log('visible item ids', byStart[start] && byStart[start].id, byEnd[end-1] && byEnd[end-1].id); // TODO: cleanup
+
+ this.visibleItems = [];
+ var i = start;
+ item = byStart[i];
+ var lastItem = byEnd[end];
+ while (item && item !== lastItem) {
+ this.visibleItems.push(item);
+ item = byStart[++i];
+ }
+ this.stack.order(this.visibleItems);
+
+ // show visible items
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ item = this.visibleItems[i];
+
+ if (!item.displayed) item.show();
+ item.top = null; // reset stacking position
+
+ // reposition item horizontally
+ item.repositionX();
+ }
+ */
+
+ // simple, brute force calculation of visible items
+ // TODO: replace with a faster, more sophisticated solution
+ this.visibleItems = [];
+ for (var id in this.items) {
+ if (this.items.hasOwnProperty(id)) {
+ var item = this.items[id];
+ if (item.isVisible(this.range)) {
+ if (!item.displayed) item.show();
+
+ // reposition item horizontally
+ item.repositionX();
+
+ this.visibleItems.push(item);
+ }
+ else {
+ if (item.displayed) item.hide();
+ }
+ }
+ }
+
+ // reposition visible items vertically
+ //this.stack.order(this.visibleItems); // TODO: improve ordering
+ var force = this.stackDirty || zoomed; // force re-stacking of all items if true
+ this.stack.stack(this.visibleItems, force);
+ this.stackDirty = false;
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ this.visibleItems[i].repositionY();
+ }
+
+ // recalculate the height of the itemset
+ var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis,
+ marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item,
+ height;
+
+ // determine the height from the stacked items
+ var visibleItems = this.visibleItems;
+ if (visibleItems.length) {
+ var min = visibleItems[0].top;
+ var max = visibleItems[0].top + visibleItems[0].height;
+ util.forEach(visibleItems, function (item) {
+ min = Math.min(min, item.top);
+ max = Math.max(max, (item.top + item.height));
+ });
+ height = (max - min) + marginAxis + marginItem;
+ }
+ else {
+ height = marginAxis + marginItem;
+ }
+
+ // reposition frame
+ frame.style.left = asSize(options.left, '');
+ frame.style.right = asSize(options.right, '');
+ frame.style.top = asSize((orientation == 'top') ? '0' : '');
+ frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = asSize(height);
+ //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
+
+ // calculate actual size and position
+ this.top = frame.offsetTop;
+ this.left = frame.offsetLeft;
+ this.width = frame.offsetWidth;
+ this.height = height;
+
+ // reposition axis
+ this.dom.axis.style.left = asSize(options.left, '0');
+ this.dom.axis.style.right = asSize(options.right, '');
+ this.dom.axis.style.width = asSize(options.width, '100%');
+ this.dom.axis.style.height = asSize(0);
+ this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
+ this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
+
+ return this._isResized();
+};
+
+/**
+ * Get the foreground container element
+ * @return {HTMLElement} foreground
+ */
+ItemSet.prototype.getForeground = function getForeground() {
+ return this.dom.foreground;
+};
+
+/**
+ * Get the background container element
+ * @return {HTMLElement} background
+ */
+ItemSet.prototype.getBackground = function getBackground() {
+ return this.dom.background;
+};
+
+/**
+ * Get the axis container element
+ * @return {HTMLElement} axis
+ */
+ItemSet.prototype.getAxis = function getAxis() {
+ return this.dom.axis;
+};
+
+/**
+ * Set items
+ * @param {vis.DataSet | null} items
+ */
+ItemSet.prototype.setItems = function setItems(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
+
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet');
+ }
+
+ 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);
+ }
+
+ if (this.itemsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.listeners, function (callback, event) {
+ me.itemsData.on(event, callback, id);
+ });
+
+ // draw all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
+ }
+};
+
+/**
+ * Get the current items items
+ * @returns {vis.DataSet | null}
+ */
+ItemSet.prototype.getItems = function getItems() {
+ return this.itemsData;
+};
+
+/**
+ * Remove an item by its id
+ * @param {String | Number} id
+ */
+ItemSet.prototype.removeItem = function removeItem (id) {
+ var item = this.itemsData.get(id),
+ dataset = this._myDataSet();
+
+ if (item) {
+ // confirm deletion
+ this.options.onRemove(item, function (item) {
+ if (item) {
+ // remove by id here, it is possible that an item has no id defined
+ // itself, so better not delete by the item itself
+ dataset.remove(id);
+ }
+ });
+ }
+};
+
+/**
+ * Handle updated items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onUpdate = function _onUpdate(ids) {
+ var me = this,
+ items = this.items,
+ itemOptions = this.itemOptions;
+
+ ids.forEach(function (id) {
+ var itemData = me.itemsData.get(id),
+ item = items[id],
+ type = itemData.type ||
+ (itemData.start && itemData.end && 'range') ||
+ me.options.type ||
+ 'box';
+
+ var constructor = ItemSet.types[type];
+
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, hide and delete the item
+ item.hide();
+ item = null;
+ }
+ else {
+ item.data = itemData; // TODO: create a method item.setData ?
+ }
+ }
+
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(me, itemData, me.options, itemOptions);
+ item.id = id;
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
+
+ me.items[id] = item;
+ });
+
+ this._order();
+
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
+};
+
+/**
+ * Handle added items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
+
+/**
+ * Handle removed items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onRemove = function _onRemove(ids) {
+ var count = 0;
+ var me = this;
+ ids.forEach(function (id) {
+ var item = me.items[id];
+ if (item) {
+ count++;
+ item.hide();
+ delete me.items[id];
+ delete me.visibleItems[id];
+
+ // remove from selection
+ var index = me.selection.indexOf(id);
+ if (index != -1) me.selection.splice(index, 1);
+ }
+ });
+
+ if (count) {
+ // update order
+ this._order();
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
+ }
+};
+
+/**
+ * Order the items
+ * @private
+ */
+ItemSet.prototype._order = function _order() {
+ var array = util.toArray(this.items);
+ this.orderedItems.byStart = array;
+ this.orderedItems.byEnd = [].concat(array);
+
+ // reorder the items
+ this.stack.orderByStart(this.orderedItems.byStart);
+ this.stack.orderByEnd(this.orderedItems.byEnd);
+};
+
+/**
+ * Start dragging the selected events
+ * @param {Event} event
+ * @private
+ */
+ItemSet.prototype._onDragStart = function (event) {
+ if (!this.options.editable) {
+ return;
+ }
+
+ var item = ItemSet.itemFromTarget(event),
+ me = this;
+
+ if (item && item.selected) {
+ var dragLeftItem = event.target.dragLeftItem;
+ var dragRightItem = event.target.dragRightItem;
+
+ if (dragLeftItem) {
+ this.touchParams.itemProps = [{
+ item: dragLeftItem,
+ start: item.data.start.valueOf()
+ }];
+ }
+ else if (dragRightItem) {
+ this.touchParams.itemProps = [{
+ item: dragRightItem,
+ end: item.data.end.valueOf()
+ }];
+ }
+ else {
+ this.touchParams.itemProps = this.getSelection().map(function (id) {
+ var item = me.items[id];
+ var props = {
+ item: item
+ };
+
+ if ('start' in item.data) {
+ props.start = item.data.start.valueOf()
+ }
+ if ('end' in item.data) {
+ props.end = item.data.end.valueOf()
+ }
+
+ return props;
+ });
+ }
+
+ event.stopPropagation();
+ }
+};
+
+/**
+ * Drag selected items
+ * @param {Event} event
+ * @private
+ */
+ItemSet.prototype._onDrag = function (event) {
+ if (this.touchParams.itemProps) {
+ var snap = this.options.snap || null,
+ deltaX = event.gesture.deltaX,
+ scale = (this.width / (this.range.end - this.range.start)),
+ offset = deltaX / scale;
+
+ // move
+ this.touchParams.itemProps.forEach(function (props) {
+ if ('start' in props) {
+ var start = new Date(props.start + offset);
+ props.item.data.start = snap ? snap(start) : start;
+ }
+ if ('end' in props) {
+ var end = new Date(props.end + offset);
+ props.item.data.end = snap ? snap(end) : end;
+ }
+ });
+
+ // TODO: implement onMoving handler
+
+ // TODO: implement dragging from one group to another
+
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
+
+ event.stopPropagation();
+ }
+};
+
+/**
+ * End of dragging selected items
+ * @param {Event} event
+ * @private
+ */
+ItemSet.prototype._onDragEnd = function (event) {
+ if (this.touchParams.itemProps) {
+ // prepare a change set for the changed items
+ var changes = [],
+ me = this,
+ dataset = this._myDataSet();
+
+ this.touchParams.itemProps.forEach(function (props) {
+ var id = props.item.id,
+ item = me.itemsData.get(id);
+
+ var changed = false;
+ if ('start' in props.item.data) {
+ changed = (props.start != props.item.data.start.valueOf());
+ item.start = util.convert(props.item.data.start, dataset.convert['start']);
+ }
+ if ('end' in props.item.data) {
+ changed = changed || (props.end != props.item.data.end.valueOf());
+ item.end = util.convert(props.item.data.end, dataset.convert['end']);
+ }
+
+ // only apply changes when start or end is actually changed
+ if (changed) {
+ me.options.onMove(item, function (item) {
+ if (item) {
+ // apply changes
+ item[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
+ changes.push(item);
+ }
+ else {
+ // restore original values
+ if ('start' in props) props.item.data.start = props.start;
+ if ('end' in props) props.item.data.end = props.end;
+
+ me.stackDirty = true; // force re-stacking of all items next repaint
+ me.emit('change');
+ }
+ });
+ }
+ });
+ this.touchParams.itemProps = null;
+
+ // apply the changes to the data (if there are changes)
+ if (changes.length) {
+ dataset.update(changes);
+ }
+
+ event.stopPropagation();
+ }
+};
+
+/**
+ * Find an item from an event target:
+ * searches for the attribute 'timeline-item' in the event target's element tree
+ * @param {Event} event
+ * @return {Item | null} item
+ */
+ItemSet.itemFromTarget = function itemFromTarget (event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-item')) {
+ return target['timeline-item'];
+ }
+ target = target.parentNode;
+ }
+
+ return null;
+};
+
+/**
+ * Find the ItemSet from an event target:
+ * searches for the attribute 'timeline-itemset' in the event target's element tree
+ * @param {Event} event
+ * @return {ItemSet | null} item
+ */
+ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-itemset')) {
+ return target['timeline-itemset'];
+ }
+ target = target.parentNode;
+ }
+
+ return null;
+};
+
+/**
+ * Find the DataSet to which this ItemSet is connected
+ * @returns {null | DataSet} dataset
+ * @private
+ */
+ItemSet.prototype._myDataSet = function _myDataSet() {
+ // find the root DataSet
+ var dataset = this.itemsData;
+ while (dataset instanceof DataView) {
+ dataset = dataset.data;
+ }
+ return dataset;
+};
+/**
+ * @constructor Item
+ * @param {ItemSet} parent
+ * @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 || {};
+
+ this.selected = false;
+ this.displayed = false;
+ this.dirty = true;
+
+ this.top = null;
+ this.left = null;
+ this.width = null;
+ this.height = null;
+}
+
+/**
+ * Select current item
+ */
+Item.prototype.select = function select() {
+ this.selected = true;
+ if (this.displayed) this.repaint();
+};
+
+/**
+ * Unselect current item
+ */
+Item.prototype.unselect = function unselect() {
+ this.selected = false;
+ if (this.displayed) this.repaint();
+};
+
+/**
+ * Show the Item in the DOM (when not already visible)
+ * @return {Boolean} changed
+ */
+Item.prototype.show = function show() {
+ return false;
+};
+
+/**
+ * Hide the Item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+Item.prototype.hide = function hide() {
+ return false;
+};
+
+/**
+ * Repaint the item
+ */
+Item.prototype.repaint = function repaint() {
+ // should be implemented by the item
+};
+
+/**
+ * Reposition the Item horizontally
+ */
+Item.prototype.repositionX = function repositionX() {
+ // should be implemented by the item
+};
+
+/**
+ * Reposition the Item vertically
+ */
+Item.prototype.repositionY = function repositionY() {
+ // should be implemented by the item
+};
+
+/**
+ * Repaint a delete button on the top right of the item when the item is selected
+ * @param {HTMLElement} anchor
+ * @private
+ */
+Item.prototype._repaintDeleteButton = function (anchor) {
+ if (this.selected && this.options.editable && !this.dom.deleteButton) {
+ // create and show button
+ var parent = this.parent;
+ var id = this.id;
+
+ var deleteButton = document.createElement('div');
+ deleteButton.className = 'delete';
+ deleteButton.title = 'Delete this item';
+
+ Hammer(deleteButton, {
+ preventDefault: true
+ }).on('tap', function (event) {
+ parent.removeItem(id);
+ event.stopPropagation();
+ });
+
+ anchor.appendChild(deleteButton);
+ this.dom.deleteButton = deleteButton;
+ }
+ else if (!this.selected && this.dom.deleteButton) {
+ // remove button
+ if (this.dom.deleteButton.parentNode) {
+ this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
+ }
+ this.dom.deleteButton = null;
+ }
+};
+
+/**
+ * @constructor ItemBox
+ * @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: {
+ width: 0,
+ height: 0
+ },
+ line: {
+ width: 0,
+ height: 0
+ }
+ };
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
+ Item.call(this, parent, data, options, defaultOptions);
+}
+
+ItemBox.prototype = new Item (null, null);
+
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemBox.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ // TODO: account for the real width of the item. Right now we just add 1/4 to the window
+ var interval = (range.end - range.start) / 4;
+ return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
+};
+
+/**
+ * Repaint the item
+ */
+ItemBox.prototype.repaint = function repaint() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
+
+ // create main box
+ dom.box = document.createElement('DIV');
+
+ // contents box (inside the background box). used for making margins
+ dom.content = document.createElement('DIV');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
+
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
+ }
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.getForeground();
+ if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
+ foreground.appendChild(dom.box);
+ }
+ if (!dom.line.parentNode) {
+ var background = this.parent.getBackground();
+ if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
+ background.appendChild(dom.line);
+ }
+ if (!dom.dot.parentNode) {
+ var axis = this.parent.getAxis();
+ if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
+ axis.appendChild(dom.dot);
+ }
+ this.displayed = true;
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+
+ this.dirty = true;
+ }
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item box' + className;
+ dom.line.className = 'item line' + className;
+ dom.dot.className = 'item dot' + className;
+
+ this.dirty = true;
+ }
+
+ // recalculate size
+ if (this.dirty) {
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.line.width = dom.line.offsetWidth;
+ this.width = dom.box.offsetWidth;
+ this.height = dom.box.offsetHeight;
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.box);
+};
+
+/**
+ * Show the item in the DOM (when not already displayed). The items DOM will
+ * be created when needed.
+ */
+ItemBox.prototype.show = function show() {
+ if (!this.displayed) {
+ this.repaint();
+ }
+};
+
+/**
+ * Hide the item from the DOM (when visible)
+ */
+ItemBox.prototype.hide = function hide() {
+ if (this.displayed) {
+ var dom = this.dom;
+
+ if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
+ if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
+ if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
+ }
+};
+
+/**
+ * Reposition the item horizontally
+ * @Override
+ */
+ItemBox.prototype.repositionX = function repositionX() {
+ var start = this.defaultOptions.toScreen(this.data.start),
+ align = this.options.align || this.defaultOptions.align,
+ left,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
+
+ // calculate left position of the box
+ if (align == 'right') {
+ this.left = start - this.width;
+ }
+ else if (align == 'left') {
+ this.left = start;
+ }
+ else {
+ // default or 'center'
+ this.left = start - this.width / 2;
+ }
+
+ // reposition box
+ box.style.left = this.left + 'px';
+
+ // reposition line
+ line.style.left = (start - this.props.line.width / 2) + 'px';
+
+ // reposition dot
+ dot.style.left = (start - this.props.dot.width / 2) + 'px';
+};
+
+/**
+ * Reposition the item vertically
+ * @Override
+ */
+ItemBox.prototype.repositionY = function repositionY () {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
+
+ if (orientation == 'top') {
+ box.style.top = (this.top || 0) + 'px';
+ box.style.bottom = '';
+
+ line.style.top = '0';
+ line.style.bottom = '';
+ line.style.height = (this.parent.top + this.top + 1) + 'px';
+ }
+ else { // orientation 'bottom'
+ box.style.top = '';
+ box.style.bottom = (this.top || 0) + 'px';
+
+ line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
+ line.style.bottom = '0';
+ line.style.height = '';
+ }
+
+ dot.style.top = (-this.props.dot.height / 2) + '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
+ */
+function ItemPoint (parent, data, options, defaultOptions) {
+ this.props = {
+ dot: {
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ content: {
+ height: 0,
+ marginLeft: 0
+ }
+ };
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
+ Item.call(this, parent, data, options, defaultOptions);
+}
+
+ItemPoint.prototype = new Item (null, null);
+
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemPoint.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ var interval = (range.end - range.start);
+ return (this.data.start > range.start - interval) && (this.data.start < range.end);
+}
+
+/**
+ * Repaint the item
+ */
+ItemPoint.prototype.repaint = function repaint() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.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);
+
+ // attach this item as attribute
+ dom.point['timeline-item'] = this;
+ }
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.point.parentNode) {
+ var foreground = this.parent.getForeground();
+ if (!foreground) {
+ throw new Error('Cannot repaint time axis: parent has no foreground container element');
+ }
+ foreground.appendChild(dom.point);
+ }
+ this.displayed = true;
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+
+ this.dirty = true;
+ }
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.point.className = 'item point' + className;
+
+ this.dirty = true;
+ }
+
+ // recalculate size
+ if (this.dirty) {
+ this.width = dom.point.offsetWidth;
+ this.height = dom.point.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.content.height = dom.content.offsetHeight;
+
+ // resize contents
+ dom.content.style.marginLeft = 1.5 * this.props.dot.width + 'px';
+ //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
+
+ dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.point);
+};
+
+/**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ */
+ItemPoint.prototype.show = function show() {
+ if (!this.displayed) {
+ this.repaint();
+ }
+};
+
+/**
+ * Hide the item from the DOM (when visible)
+ */
+ItemPoint.prototype.hide = function hide() {
+ if (this.displayed) {
+ if (this.dom.point.parentNode) {
+ this.dom.point.parentNode.removeChild(this.dom.point);
+ }
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
+ }
+};
+
+/**
+ * Reposition the item horizontally
+ * @Override
+ */
+ItemPoint.prototype.repositionX = function repositionX() {
+ var start = this.defaultOptions.toScreen(this.data.start);
+
+ this.left = start - this.props.dot.width / 2;
+
+ // reposition point
+ this.dom.point.style.left = this.left + 'px';
+};
+
+/**
+ * Reposition the item vertically
+ * @Override
+ */
+ItemPoint.prototype.repositionY = function repositionY () {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ point = this.dom.point;
+
+ if (orientation == 'top') {
+ point.style.top = this.top + 'px';
+ point.style.bottom = '';
+ }
+ else {
+ point.style.top = '';
+ point.style.bottom = this.top + 'px';
+ }
+}
+
+/**
+ * @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: {
+ width: 0
+ }
+ };
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data.id);
+ }
+ if (data.end == undefined) {
+ throw new Error('Property "end" missing in item ' + data.id);
+ }
+ }
+
+ Item.call(this, parent, data, options, defaultOptions);
+}
+
+ItemRange.prototype = new Item (null, null);
+
+ItemRange.prototype.baseClassName = 'item range';
+
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemRange.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ return (this.data.start < range.end) && (this.data.end > range.start);
+};
+
+/**
+ * Repaint the item
+ */
+ItemRange.prototype.repaint = function repaint() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
+
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
+ }
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.getForeground();
+ if (!foreground) {
+ throw new Error('Cannot repaint time axis: parent has no foreground container element');
+ }
+ foreground.appendChild(dom.box);
+ }
+ this.displayed = true;
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+
+ this.dirty = true;
+ }
+
+ // update class
+ var className = (this.data.className ? (' ' + this.data.className) : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = this.baseClassName + className;
+
+ this.dirty = true;
+ }
+
+ // recalculate size
+ if (this.dirty) {
+ this.props.content.width = this.dom.content.offsetWidth;
+ this.height = this.dom.box.offsetHeight;
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.box);
+ this._repaintDragLeft();
+ this._repaintDragRight();
+};
+
+/**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ */
+ItemRange.prototype.show = function show() {
+ if (!this.displayed) {
+ this.repaint();
+ }
+};
+
+/**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ItemRange.prototype.hide = function hide() {
+ if (this.displayed) {
+ var box = this.dom.box;
+
+ if (box.parentNode) {
+ box.parentNode.removeChild(box);
+ }
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
+ }
+};
+
+/**
+ * Reposition the item horizontally
+ * @Override
+ */
+ItemRange.prototype.repositionX = function repositionX() {
+ var props = this.props,
+ parentWidth = this.parent.width,
+ start = this.defaultOptions.toScreen(this.data.start),
+ end = this.defaultOptions.toScreen(this.data.end),
+ padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
+ contentLeft;
+
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
+ }
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
+ }
+
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ if (start < 0) {
+ contentLeft = Math.min(-start,
+ (end - start - props.content.width - 2 * padding));
+ // TODO: remove the need for options.padding. it's terrible.
+ }
+ else {
+ contentLeft = 0;
+ }
+
+ this.left = start;
+ this.width = Math.max(end - start, 1);
+
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = this.width + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
+};
+
+/**
+ * Reposition the item vertically
+ * @Override
+ */
+ItemRange.prototype.repositionY = function repositionY() {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ box = this.dom.box;
+
+ if (orientation == 'top') {
+ box.style.top = this.top + 'px';
+ box.style.bottom = '';
+ }
+ else {
+ box.style.top = '';
+ box.style.bottom = this.top + 'px';
+ }
+};
+
+/**
+ * Repaint a drag area on the left side of the range when the range is selected
+ * @private
+ */
+ItemRange.prototype._repaintDragLeft = function () {
+ if (this.selected && this.options.editable && !this.dom.dragLeft) {
+ // create and show drag area
+ var dragLeft = document.createElement('div');
+ dragLeft.className = 'drag-left';
+ dragLeft.dragLeftItem = this;
+
+ // TODO: this should be redundant?
+ Hammer(dragLeft, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag left')
+ });
+
+ this.dom.box.appendChild(dragLeft);
+ this.dom.dragLeft = dragLeft;
+ }
+ else if (!this.selected && this.dom.dragLeft) {
+ // delete drag area
+ if (this.dom.dragLeft.parentNode) {
+ this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
+ }
+ this.dom.dragLeft = null;
+ }
+};
+
+/**
+ * Repaint a drag area on the right side of the range when the range is selected
+ * @private
+ */
+ItemRange.prototype._repaintDragRight = function () {
+ if (this.selected && this.options.editable && !this.dom.dragRight) {
+ // create and show drag area
+ var dragRight = document.createElement('div');
+ dragRight.className = 'drag-right';
+ dragRight.dragRightItem = this;
+
+ // TODO: this should be redundant?
+ Hammer(dragRight, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag right')
+ });
+
+ this.dom.box.appendChild(dragRight);
+ this.dom.dragRight = dragRight;
+ }
+ else if (!this.selected && this.dom.dragRight) {
+ // delete drag area
+ if (this.dom.dragRight.parentNode) {
+ this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
+ }
+ this.dom.dragRight = null;
+ }
+};
+
+/**
+ * @constructor ItemRangeOverflow
+ * @extends ItemRange
+ * @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 ItemRangeOverflow (parent, data, options, defaultOptions) {
+ this.props = {
+ content: {
+ left: 0,
+ width: 0
+ }
+ };
+
+ ItemRange.call(this, parent, data, options, defaultOptions);
+}
+
+ItemRangeOverflow.prototype = new ItemRange (null, null);
+
+ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
+
+/**
+ * Reposition the item horizontally
+ * @Override
+ */
+ItemRangeOverflow.prototype.repositionX = function repositionX() {
+ var parentWidth = this.parent.width,
+ start = this.defaultOptions.toScreen(this.data.start),
+ end = this.defaultOptions.toScreen(this.data.end),
+ padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
+ contentLeft;
+
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
+ }
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
+ }
+
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ contentLeft = Math.max(-start, 0);
+
+ this.left = start;
+ var boxWidth = Math.max(end - start, 1);
+ this.width = (this.props.content.width < boxWidth) ?
+ boxWidth :
+ start + contentLeft + this.props.content.width;
+
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = boxWidth + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
+};
+
+/**
+ * @constructor Group
+ * @param {Panel} groupPanel
+ * @param {Panel} labelPanel
+ * @param {Panel} backgroundPanel
+ * @param {Panel} axisPanel
+ * @param {Number | String} groupId
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ * @extends Component
+ */
+function Group (groupPanel, labelPanel, backgroundPanel, axisPanel, groupId, options) {
+ this.id = util.randomUUID();
+ this.groupPanel = groupPanel;
+ this.labelPanel = labelPanel;
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
+
+ this.groupId = groupId;
+ this.itemSet = null; // ItemSet
+ this.options = options || {};
+ this.options.top = 0;
+
+ this.props = {
+ label: {
+ width: 0,
+ height: 0
+ }
+ };
+
+ this.dom = {};
+
+ this.top = 0;
+ this.left = 0;
+ this.width = 0;
+ this.height = 0;
+
+ this._create();
+}
+
+Group.prototype = new Component();
+
+// TODO: comment
+Group.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Create DOM elements for the group
+ * @private
+ */
+Group.prototype._create = function() {
+ var label = document.createElement('div');
+ label.className = 'vlabel';
+ this.dom.label = label;
+
+ var inner = document.createElement('div');
+ inner.className = 'inner';
+ label.appendChild(inner);
+ this.dom.inner = inner;
+};
+
+/**
+ * Set the group data for this group
+ * @param {Object} data Group data, can contain properties content and className
+ */
+Group.prototype.setData = function setData(data) {
+ // update contents
+ var content = data && data.content;
+ if (content instanceof Element) {
+ this.dom.inner.appendChild(content);
+ }
+ else if (content != undefined) {
+ this.dom.inner.innerHTML = content;
+ }
+ else {
+ this.dom.inner.innerHTML = this.groupId;
+ }
+
+ // update className
+ var className = data && data.className;
+ if (className) {
+ util.addClassName(this.dom.label, className);
+ }
+};
+
+/**
+ * Set item set for the group. The group will create a view on the itemSet,
+ * filtered by the groups id.
+ * @param {DataSet | DataView} itemsData
+ */
+Group.prototype.setItems = function setItems(itemsData) {
+ if (this.itemSet) {
+ // remove current item set
+ this.itemSet.setItems();
+ this.itemSet.hide();
+ this.groupPanel.frame.removeChild(this.itemSet.getFrame());
+ this.itemSet = null;
+ }
+
+ if (itemsData) {
+ var groupId = this.groupId;
+
+ var me = this;
+ var itemSetOptions = util.extend(this.options, {
+ height: function () {
+ // FIXME: setting height doesn't yet work
+ return Math.max(me.props.label.height, me.itemSet.height);
+ }
+ });
+ this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, itemSetOptions);
+ this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event
+ this.itemSet.parent = this;
+ this.groupPanel.frame.appendChild(this.itemSet.getFrame());
+
+ if (this.range) this.itemSet.setRange(this.range);
+
+ this.view = new DataView(itemsData, {
+ filter: function (item) {
+ return item.group == groupId;
+ }
+ });
+ this.itemSet.setItems(this.view);
+ }
+};
+
+/**
+ * hide the group, detach from DOM if needed
+ */
+Group.prototype.show = function show() {
+ if (!this.dom.label.parentNode) {
+ this.labelPanel.frame.appendChild(this.dom.label);
+ }
+
+ var itemSetFrame = this.itemSet && this.itemSet.getFrame();
+ if (itemSetFrame) {
+ if (itemSetFrame.parentNode) {
+ itemSetFrame.parentNode.removeChild(itemSetFrame);
+ }
+ this.groupPanel.frame.appendChild(itemSetFrame);
+
+ this.itemSet.show();
+ }
+};
+
+/**
+ * hide the group, detach from DOM if needed
+ */
+Group.prototype.hide = function hide() {
+ if (this.dom.label.parentNode) {
+ this.dom.label.parentNode.removeChild(this.dom.label);
+ }
+
+ if (this.itemSet) {
+ this.itemSet.hide();
+ }
+
+ var itemSetFrame = this.itemset && this.itemSet.getFrame();
+ if (itemSetFrame && itemSetFrame.parentNode) {
+ itemSetFrame.parentNode.removeChild(itemSetFrame);
+ }
+};
+
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+Group.prototype.setRange = function (range) {
+ this.range = range;
+
+ if (this.itemSet) this.itemSet.setRange(range);
+};
+
+/**
+ * Set selected items by their id. Replaces the current selection.
+ * Unknown id's are silently ignored.
+ * @param {Array} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
+ */
+Group.prototype.setSelection = function setSelection(ids) {
+ if (this.itemSet) this.itemSet.setSelection(ids);
+};
+
+/**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+Group.prototype.getSelection = function getSelection() {
+ return this.itemSet ? this.itemSet.getSelection() : [];
+};
+
+/**
+ * Repaint the group
+ * @return {boolean} Returns true if the component is resized
+ */
+Group.prototype.repaint = function repaint() {
+ var resized = false;
+
+ this.show();
+
+ if (this.itemSet) {
+ resized = this.itemSet.repaint() || resized;
+ }
+
+ // calculate inner size of the label
+ resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
+ resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
+
+ this.height = this.itemSet ? this.itemSet.height : 0;
+
+ this.dom.label.style.height = this.height + 'px';
+
+ return resized;
+};
+
+/**
+ * An GroupSet holds a set of groups
+ * @param {Panel} contentPanel Panel where the ItemSets will be created
+ * @param {Panel} labelPanel Panel where the labels will be created
+ * @param {Panel} backgroundPanel Panel where the vertical lines of box
+ * items are created
+ * @param {Panel} axisPanel Panel on the axis where the dots of box
+ * items will be created
+ * @param {Object} [options] See GroupSet.setOptions for the available
+ * options.
+ * @constructor GroupSet
+ * @extends Panel
+ */
+function GroupSet(contentPanel, labelPanel, backgroundPanel, axisPanel, options) {
+ this.id = util.randomUUID();
+
+ this.contentPanel = contentPanel;
+ this.labelPanel = labelPanel;
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
+ this.options = options || {};
+
+ this.range = null; // Range or Object {start: number, end: number}
+ this.itemsData = null; // DataSet with items
+ this.groupsData = null; // DataSet with groups
+
+ this.groups = {}; // map with groups
+ this.groupIds = []; // list with ordered group ids
+
+ this.dom = {};
+ this.props = {
+ labels: {
+ width: 0
+ }
+ };
+
+ // TODO: implement right orientation of the labels (left/right)
+
+ 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);
+ }
+ };
+
+ // create HTML DOM
+ this._create();
+}
+
+GroupSet.prototype = new Panel();
+
+/**
+ * Create the HTML DOM elements for the GroupSet
+ * @private
+ */
+GroupSet.prototype._create = function _create () {
+ // TODO: reimplement groupSet DOM elements
+ var frame = document.createElement('div');
+ frame.className = 'groupset';
+ frame['timeline-groupset'] = this;
+ this.frame = frame;
+
+ this.labelSet = new Panel({
+ className: 'labelset',
+ width: '100%',
+ height: '100%'
+ });
+ this.labelPanel.appendChild(this.labelSet);
+};
+
+/**
+ * Get the frame element of component
+ * @returns {null} Get frame is not supported by GroupSet
+ */
+GroupSet.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
+/**
+ * Set options for the GroupSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {String | function} groupsOrder
+ * TODO: describe options
+ */
+GroupSet.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+GroupSet.prototype.setRange = function (range) {
+ this.range = range;
+
+ for (var id in this.groups) {
+ if (this.groups.hasOwnProperty(id)) {
+ this.groups[id].setRange(range);
+ }
+ }
+};
+
+/**
+ * Set items
+ * @param {vis.DataSet | null} items
+ */
+GroupSet.prototype.setItems = function setItems(items) {
+ this.itemsData = items;
+
+ for (var id in this.groups) {
+ if (this.groups.hasOwnProperty(id)) {
+ var group = this.groups[id];
+ // TODO: every group will emit a change event, causing a lot of unnecessary repaints. improve this.
+ group.setItems(items);
+ }
+ }
+};
+
+/**
+ * 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
+ */
+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.on(event, callback, id);
+ });
+
+ // draw all new groups
+ ids = this.groupsData.getIds();
+ this._onAdd(ids);
+ }
+
+ this.emit('change');
+};
+
+/**
+ * Get groups
+ * @return {vis.DataSet | null} groups
+ */
+GroupSet.prototype.getGroups = function getGroups() {
+ return this.groupsData;
+};
+
+/**
+ * Set selected items by their id. Replaces the current selection.
+ * Unknown id's are silently ignored.
+ * @param {Array} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
+ */
+GroupSet.prototype.setSelection = function setSelection(ids) {
+ var selection = [],
+ groups = this.groups;
+
+ // iterate over each of the groups
+ for (var id in groups) {
+ if (groups.hasOwnProperty(id)) {
+ var group = groups[id];
+ group.setSelection(ids);
+ }
+ }
+
+ return selection;
+};
+
+/**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+GroupSet.prototype.getSelection = function getSelection() {
+ var selection = [],
+ groups = this.groups;
+
+ // iterate over each of the groups
+ for (var id in groups) {
+ if (groups.hasOwnProperty(id)) {
+ var group = groups[id];
+ selection = selection.concat(group.getSelection());
+ }
+ }
+
+ return selection;
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component was resized since previous repaint
+ */
+GroupSet.prototype.repaint = function repaint() {
+ var i, id, group,
+ asSize = util.option.asSize,
+ asString = util.option.asString,
+ options = this.options,
+ orientation = this.getOption('orientation'),
+ frame = this.frame,
+ resized = false,
+ groups = this.groups;
+
+ // repaint all groups in order
+ this.groupIds.forEach(function (id) {
+ var groupResized = groups[id].repaint();
+ resized = resized || groupResized;
+ });
+
+ // reposition the labels and calculate the maximum label width
+ var maxWidth = 0;
+ for (id in groups) {
+ if (groups.hasOwnProperty(id)) {
+ group = groups[id];
+ maxWidth = Math.max(maxWidth, group.props.label.width);
+ }
+ }
+ resized = util.updateProperty(this.props.labels, 'width', maxWidth) || resized;
+
+ // recalculate the height of the groupset, and recalculate top positions of the groups
+ var fixedHeight = (asSize(options.height) != null);
+ var height;
+ if (!fixedHeight) {
+ // height is not specified, calculate the sum of the height of all groups
+ height = 0;
+
+ this.groupIds.forEach(function (id) {
+ var group = groups[id];
+ group.top = height;
+ if (group.itemSet) group.itemSet.top = group.top; // TODO: this is an ugly hack
+ height += group.height;
+ });
+ }
+
+ // update classname
+ frame.className = 'groupset' + (options.className ? (' ' + asString(options.className)) : '');
+
+ // calculate actual size and position
+ this.top = frame.offsetTop;
+ this.left = frame.offsetLeft;
+ this.width = frame.offsetWidth;
+ this.height = height;
+
+ return resized;
+};
+
+/**
+ * Update the groupIds. Requires a repaint afterwards
+ * @private
+ */
+GroupSet.prototype._updateGroupIds = function () {
+ // reorder the groups
+ this.groupIds = this.groupsData.getIds({
+ order: this.options.groupOrder
+ });
+
+ // hide the groups now, they will be shown again in the next repaint
+ // in correct order
+ var groups = this.groups;
+ this.groupIds.forEach(function (id) {
+ groups[id].hide();
+ });
+};
+
+/**
+ * Get the width of the group labels
+ * @return {Number} width
+ */
+GroupSet.prototype.getLabelsWidth = function getLabelsWidth() {
+ return this.props.labels.width;
+};
+
+/**
+ * Hide the component from the DOM
+ */
+GroupSet.prototype.hide = function hide() {
+ // hide labelset
+ this.labelPanel.removeChild(this.labelSet);
+
+ // hide each of the groups
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].hide();
+ }
+ }
+};
+
+/**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+GroupSet.prototype.show = function show() {
+ // show label set
+ if (!this.labelPanel.hasChild(this.labelSet)) {
+ this.labelPanel.removeChild(this.labelSet);
+ }
+
+ // show each of the groups
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].show();
+ }
+ }
+};
+
+/**
+ * Handle updated groups
+ * @param {Number[]} ids
+ * @private
+ */
+GroupSet.prototype._onUpdate = function _onUpdate(ids) {
+ this._onAdd(ids);
+};
+
+/**
+ * Handle changed groups
+ * @param {Number[]} ids
+ * @private
+ */
+GroupSet.prototype._onAdd = function _onAdd(ids) {
+ var me = this;
+
+ ids.forEach(function (id) {
+ var group = me.groups[id];
+ if (!group) {
+ var groupOptions = Object.create(me.options);
+ util.extend(groupOptions, {
+ height: null
+ });
+
+ group = new Group(me, me.labelSet, me.backgroundPanel, me.axisPanel, id, groupOptions);
+ group.on('change', me.emit.bind(me, 'change')); // propagate change event
+ group.setRange(me.range);
+ group.setItems(me.itemsData); // attach items data
+ me.groups[id] = group;
+ group.parent = me;
+ }
+
+ // update group data
+ group.setData(me.groupsData.get(id));
+ });
+
+ this._updateGroupIds();
+
+ this.emit('change');
+};
+
+/**
+ * Handle removed groups
+ * @param {Number[]} ids
+ * @private
+ */
+GroupSet.prototype._onRemove = function _onRemove(ids) {
+ var groups = this.groups;
+ ids.forEach(function (id) {
+ var group = groups[id];
+
+ if (group) {
+ group.setItems(); // detach items data
+ group.hide(); // FIXME: for some reason when doing setItems after hide, setItems again makes the label visible
+ delete groups[id];
+ }
+ });
+
+ this._updateGroupIds();
+
+ this.emit('change');
+};
+
+/**
+ * Find the GroupSet from an event target:
+ * searches for the attribute 'timeline-groupset' in the event target's element
+ * tree, then finds the right group in this groupset
+ * @param {Event} event
+ * @return {Group | null} group
+ */
+GroupSet.groupSetFromTarget = function groupSetFromTarget (event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-groupset')) {
+ return target['timeline-groupset'];
+ }
+ target = target.parentNode;
+ }
+
+ return null;
+};
+
+/**
+ * Find the Group from an event target:
+ * searches for the two elements having attributes 'timeline-groupset' and
+ * 'timeline-itemset' in the event target's element, then finds the right group.
+ * @param {Event} event
+ * @return {Group | null} group
+ */
+GroupSet.groupFromTarget = function groupFromTarget (event) {
+ // find the groupSet
+ var groupSet = GroupSet.groupSetFromTarget(event);
+
+ // find the ItemSet
+ var itemSet = ItemSet.itemSetFromTarget(event);
+
+ // find the right group
+ if (groupSet && itemSet) {
+ for (var groupId in groupSet.groups) {
+ if (groupSet.groups.hasOwnProperty(groupId)) {
+ var group = groupSet.groups[groupId];
+ if (group.itemSet == itemSet) {
+ return group;
+ }
+ }
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
+ */
+function Timeline (container, items, options) {
+ // validate arguments
+ if (!container) throw new Error('No container element provided');
+
+ var me = this;
+ var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
+ this.options = {
+ orientation: 'bottom',
+ direction: 'horizontal', // 'horizontal' or 'vertical'
+ autoResize: true,
+ editable: false,
+ selectable: true,
+ snap: null, // will be specified after timeaxis is created
+
+ min: null,
+ max: null,
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
+ // moveable: true, // TODO: option moveable
+ // zoomable: true, // TODO: option zoomable
+
+ showMinorLabels: true,
+ showMajorLabels: true,
+ showCurrentTime: false,
+ showCustomTime: false,
+
+ type: 'box',
+ align: 'center',
+ margin: {
+ axis: 20,
+ item: 10
+ },
+ padding: 5,
+
+ onAdd: function (item, callback) {
+ callback(item);
+ },
+ onUpdate: function (item, callback) {
+ callback(item);
+ },
+ onMove: function (item, callback) {
+ callback(item);
+ },
+ onRemove: function (item, callback) {
+ callback(item);
+ },
+
+ toScreen: me._toScreen.bind(me),
+ toTime: me._toTime.bind(me)
+ };
+
+ // root panel
+ var rootOptions = util.extend(Object.create(this.options), {
+ height: function () {
+ if (me.options.height) {
+ // fixed height
+ return me.options.height;
+ }
+ else {
+ // auto height
+ // TODO: implement a css based solution to automatically have the right hight
+ return (me.timeAxis.height + me.contentPanel.height) + 'px';
+ }
+ }
+ });
+ this.rootPanel = new RootPanel(container, rootOptions);
+
+ // single select (or unselect) when tapping an item
+ this.rootPanel.on('tap', this._onSelectItem.bind(this));
+
+ // multi select when holding mouse/touch, or on ctrl+click
+ this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
+
+ // add item on doubletap
+ this.rootPanel.on('doubletap', this._onAddItem.bind(this));
+
+ // side panel
+ var sideOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (sideOptions.orientation == 'top') ? '0' : '';
+ },
+ bottom: function () {
+ return (sideOptions.orientation == 'top') ? '' : '0';
+ },
+ left: '0',
+ right: null,
+ height: '100%',
+ width: function () {
+ if (me.groupSet) {
+ return me.groupSet.getLabelsWidth();
+ }
+ else {
+ return 0;
+ }
+ },
+ className: function () {
+ return 'side' + (me.groupsData ? '' : ' hidden');
+ }
+ });
+ this.sidePanel = new Panel(sideOptions);
+ this.rootPanel.appendChild(this.sidePanel);
+
+ // main panel (contains time axis and itemsets)
+ var mainOptions = util.extend(Object.create(this.options), {
+ left: function () {
+ // we align left to enable a smooth resizing of the window
+ return me.sidePanel.width;
+ },
+ right: null,
+ height: '100%',
+ width: function () {
+ return me.rootPanel.width - me.sidePanel.width;
+ },
+ className: 'main'
+ });
+ this.mainPanel = new Panel(mainOptions);
+ this.rootPanel.appendChild(this.mainPanel);
+
+ // range
+ // TODO: move range inside rootPanel?
+ var rangeOptions = Object.create(this.options);
+ this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
+ this.range.setRange(
+ now.clone().add('days', -3).valueOf(),
+ now.clone().add('days', 4).valueOf()
+ );
+ this.range.on('rangechange', function (properties) {
+ me.rootPanel.repaint();
+ me.emit('rangechange', properties);
+ });
+ this.range.on('rangechanged', function (properties) {
+ me.rootPanel.repaint();
+ me.emit('rangechanged', properties);
+ });
+
+ // panel with time axis
+ var timeAxisOptions = util.extend(Object.create(rootOptions), {
+ range: this.range,
+ left: null,
+ top: null,
+ width: null,
+ height: null
+ });
+ this.timeAxis = new TimeAxis(timeAxisOptions);
+ this.timeAxis.setRange(this.range);
+ this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
+ this.mainPanel.appendChild(this.timeAxis);
+
+ // content panel (contains itemset(s))
+ var contentOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: null,
+ width: null,
+ className: 'content'
+ });
+ this.contentPanel = new Panel(contentOptions);
+ this.mainPanel.appendChild(this.contentPanel);
+
+ // content panel (contains the vertical lines of box items)
+ var backgroundOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: function () {
+ return me.contentPanel.height;
+ },
+ width: null,
+ className: 'background'
+ });
+ this.backgroundPanel = new Panel(backgroundOptions);
+ this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
+
+ // panel with axis holding the dots of item boxes
+ var axisPanelOptions = util.extend(Object.create(rootOptions), {
+ left: 0,
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ width: '100%',
+ height: 0,
+ className: 'axis'
+ });
+ this.axisPanel = new Panel(axisPanelOptions);
+ this.mainPanel.appendChild(this.axisPanel);
+
+ // content panel (contains itemset(s))
+ var sideContentOptions = util.extend(Object.create(this.options), {
+ top: function () {
+ return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
+ },
+ bottom: function () {
+ return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
+ },
+ left: null,
+ right: null,
+ height: null,
+ width: null,
+ className: 'side-content'
+ });
+ this.sideContentPanel = new Panel(sideContentOptions);
+ this.sidePanel.appendChild(this.sideContentPanel);
+
+ // current time bar
+ // Note: time bar will be attached in this.setOptions when selected
+ this.currentTime = new CurrentTime(this.range, rootOptions);
+
+ // custom time bar
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(rootOptions);
+ this.customTime.on('timechange', function (time) {
+ me.emit('timechange', time);
+ });
+ this.customTime.on('timechanged', function (time) {
+ me.emit('timechanged', time);
+ });
+
+ this.itemSet = null;
+ this.groupSet = null;
+
+ // create groupset
+ this.setGroups(null);
+
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
+
+ // apply options
+ if (options) {
+ this.setOptions(options);
+ }
+
+ // create itemset and groupset
+ if (items) {
+ this.setItems(items);
+ }
+}
+
+// turn Timeline into an event emitter
+Emitter(Timeline.prototype);
+
+/**
+ * Set options
+ * @param {Object} options TODO: describe the available options
+ */
+Timeline.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ // force update of range (apply new min/max etc.)
+ // both start and end are optional
+ this.range.setRange(options.start, options.end);
+
+ if ('editable' in options || 'selectable' in options) {
+ if (this.options.selectable) {
+ // force update of selection
+ this.setSelection(this.getSelection());
+ }
+ else {
+ // remove selection
+ this.setSelection([]);
+ }
+ }
+
+ // validate the callback functions
+ var validateCallback = (function (fn) {
+ if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
+ throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
+ }
+ }).bind(this);
+ ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
+
+ // add/remove the current time bar
+ if (this.options.showCurrentTime) {
+ if (!this.mainPanel.hasChild(this.currentTime)) {
+ this.mainPanel.appendChild(this.currentTime);
+ this.currentTime.start();
+ }
+ }
+ else {
+ if (this.mainPanel.hasChild(this.currentTime)) {
+ this.currentTime.stop();
+ this.mainPanel.removeChild(this.currentTime);
+ }
+ }
+
+ // add/remove the custom time bar
+ if (this.options.showCustomTime) {
+ if (!this.mainPanel.hasChild(this.customTime)) {
+ this.mainPanel.appendChild(this.customTime);
+ }
+ }
+ else {
+ if (this.mainPanel.hasChild(this.customTime)) {
+ this.mainPanel.removeChild(this.customTime);
+ }
+ }
+
+ // TODO: remove deprecation error one day (deprecated since version 0.8.0)
+ if (options && options.order) {
+ throw new Error('Option order is deprecated. There is no replacement for this feature.');
+ }
+
+ // repaint everything
+ this.rootPanel.repaint();
+};
+
+/**
+ * Set a custom time bar
+ * @param {Date} time
+ */
+Timeline.prototype.setCustomTime = function (time) {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
+
+ this.customTime.setCustomTime(time);
+};
+
+/**
+ * Retrieve the current custom time.
+ * @return {Date} customTime
+ */
+Timeline.prototype.getCustomTime = function() {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
+
+ return this.customTime.getCustomTime();
+};
+
+/**
+ * Set items
+ * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
+ */
+Timeline.prototype.setItems = function(items) {
+ var initialLoad = (this.itemsData == null);
+
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!items) {
+ newDataSet = null;
+ }
+ else if (items instanceof DataSet) {
+ newDataSet = items;
+ }
+ if (!(items instanceof DataSet)) {
+ newDataSet = new DataSet({
+ convert: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ newDataSet.add(items);
+ }
+
+ // set items
+ this.itemsData = newDataSet;
+ (this.itemSet || this.groupSet).setItems(newDataSet);
+
+ if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
+ // apply the data range as range
+ var dataRange = this.getItemRange();
+
+ // add 5% space on both sides
+ var start = dataRange.min;
+ var end = dataRange.max;
+ if (start != null && end != null) {
+ var interval = (end.valueOf() - start.valueOf());
+ if (interval <= 0) {
+ // prevent an empty interval
+ interval = 24 * 60 * 60 * 1000; // 1 day
+ }
+ start = new Date(start.valueOf() - interval * 0.05);
+ end = new Date(end.valueOf() + interval * 0.05);
+ }
+
+ // override specified start and/or end date
+ if (this.options.start != undefined) {
+ start = util.convert(this.options.start, 'Date');
+ }
+ if (this.options.end != undefined) {
+ end = util.convert(this.options.end, 'Date');
+ }
+
+ // skip range set if there is no start and end date
+ if (start === null && end === null) {
+ return;
+ }
+
+ // if start and end dates are set but cannot be satisfyed due to zoom restrictions — correct end date
+ if (start != null && end != null) {
+ var diff = end.valueOf() - start.valueOf();
+ if (this.options.zoomMax != undefined && this.options.zoomMax < diff) {
+ end = new Date(start.valueOf() + this.options.zoomMax);
+ }
+ if (this.options.zoomMin != undefined && this.options.zoomMin > diff) {
+ end = new Date(start.valueOf() + this.options.zoomMin);
+ }
+ }
+
+ this.range.setRange(start, end);
+ }
+};
+
+/**
+ * Set groups
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groupSet
+ */
+Timeline.prototype.setGroups = function(groupSet) {
+ var me = this;
+ this.groupsData = groupSet;
+
+ // create options for the itemset or groupset
+ var options = util.extend(Object.create(this.options), {
+ top: null,
+ bottom: null,
+ right: null,
+ left: null,
+ width: null,
+ height: null
+ });
+
+ if (this.groupsData) {
+ // Create a GroupSet
+
+ // remove itemset if existing
+ if (this.itemSet) {
+ this.itemSet.hide(); // TODO: not so nice having to hide here
+ this.contentPanel.removeChild(this.itemSet);
+ this.itemSet.setItems(); // disconnect from itemset
+ this.itemSet = null;
+ }
+
+ // create new GroupSet when needed
+ if (!this.groupSet) {
+ this.groupSet = new GroupSet(this.contentPanel, this.sideContentPanel, this.backgroundPanel, this.axisPanel, options);
+ this.groupSet.on('change', this.rootPanel.repaint.bind(this.rootPanel));
+ this.groupSet.setRange(this.range);
+ this.groupSet.setItems(this.itemsData);
+ this.groupSet.setGroups(this.groupsData);
+ this.contentPanel.appendChild(this.groupSet);
+ }
+ else {
+ this.groupSet.setGroups(this.groupsData);
+ }
+ }
+ else {
+ // ItemSet
+ if (this.groupSet) {
+ this.groupSet.hide(); // TODO: not so nice having to hide here
+ //this.groupSet.setGroups(); // disconnect from groupset
+ this.groupSet.setItems(); // disconnect from itemset
+ this.contentPanel.removeChild(this.groupSet);
+ this.groupSet = null;
+ }
+
+ // create new items
+ this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, options);
+ this.itemSet.setRange(this.range);
+ this.itemSet.setItems(this.itemsData);
+ this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
+ this.contentPanel.appendChild(this.itemSet);
+ }
+};
+
+/**
+ * Get the data range of the item set.
+ * @returns {{min: Date, max: Date}} range A range with a start and end Date.
+ * When no minimum is found, min==null
+ * When no maximum is found, max==null
+ */
+Timeline.prototype.getItemRange = function 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
+ };
+};
+
+/**
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {Array} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
+ */
+Timeline.prototype.setSelection = function setSelection (ids) {
+ var itemOrGroupSet = (this.itemSet || this.groupSet);
+
+ if (itemOrGroupSet) itemOrGroupSet.setSelection(ids);
+};
+
+/**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+Timeline.prototype.getSelection = function getSelection() {
+ var itemOrGroupSet = (this.itemSet || this.groupSet);
+
+ return itemOrGroupSet ? itemOrGroupSet.getSelection() : [];
+};
+
+/**
+ * Set the visible window. Both parameters are optional, you can change only
+ * start or only end. Syntax:
+ *
+ * TimeLine.setWindow(start, end)
+ * TimeLine.setWindow(range)
+ *
+ * Where start and end can be a Date, number, or string, and range is an
+ * object with properties start and end.
+ *
+ * @param {Date | Number | String} [start] Start date of visible window
+ * @param {Date | Number | String} [end] End date of visible window
+ */
+Timeline.prototype.setWindow = function setWindow(start, end) {
+ if (arguments.length == 1) {
+ var range = arguments[0];
+ this.range.setRange(range.start, range.end);
+ }
+ else {
+ this.range.setRange(start, end);
+ }
+};
+
+/**
+ * Get the visible window
+ * @return {{start: Date, end: Date}} Visible range
+ */
+Timeline.prototype.getWindow = function setWindow() {
+ var range = this.range.getRange();
+ return {
+ start: new Date(range.start),
+ end: new Date(range.end)
+ };
+};
+
+/**
+ * Handle selecting/deselecting an item when tapping it
+ * @param {Event} event
+ * @private
+ */
+// TODO: move this function to ItemSet
+Timeline.prototype._onSelectItem = function (event) {
+ if (!this.options.selectable) return;
+
+ var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
+ var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
+ if (ctrlKey || shiftKey) {
+ this._onMultiSelectItem(event);
+ return;
+ }
+
+ var oldSelection = this.getSelection();
+
+ var item = ItemSet.itemFromTarget(event);
+ var selection = item ? [item.id] : [];
+ this.setSelection(selection);
+
+ var newSelection = this.getSelection();
+
+ // if selection is changed, emit a select event
+ if (!util.equalArray(oldSelection, newSelection)) {
+ this.emit('select', {
+ items: this.getSelection()
+ });
+ }
+
+ event.stopPropagation();
+};
+
+/**
+ * Handle creation and updates of an item on double tap
+ * @param event
+ * @private
+ */
+Timeline.prototype._onAddItem = function (event) {
+ if (!this.options.selectable) return;
+ if (!this.options.editable) return;
+
+ var me = this,
+ item = ItemSet.itemFromTarget(event);
+
+ if (item) {
+ // update item
+
+ // execute async handler to update the item (or cancel it)
+ var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
+ this.options.onUpdate(itemData, function (itemData) {
+ if (itemData) {
+ me.itemsData.update(itemData);
+ }
+ });
+ }
+ else {
+ // add item
+ var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
+ var x = event.gesture.center.pageX - xAbs;
+ var newItem = {
+ start: this.timeAxis.snap(this._toTime(x)),
+ content: 'new item'
+ };
+
+ var id = util.randomUUID();
+ newItem[this.itemsData.fieldId] = id;
+
+ var group = GroupSet.groupFromTarget(event);
+ if (group) {
+ newItem.group = group.groupId;
+ }
+
+ // execute async handler to customize (or cancel) adding an item
+ this.options.onAdd(newItem, function (item) {
+ if (item) {
+ me.itemsData.add(newItem);
+ // TODO: need to trigger a repaint?
+ }
+ });
+ }
+};
+
+/**
+ * Handle selecting/deselecting multiple items when holding an item
+ * @param {Event} event
+ * @private
+ */
+// TODO: move this function to ItemSet
+Timeline.prototype._onMultiSelectItem = function (event) {
+ if (!this.options.selectable) return;
+
+ var selection,
+ item = ItemSet.itemFromTarget(event);
+
+ if (item) {
+ // multi select items
+ selection = this.getSelection(); // current selection
+ var index = selection.indexOf(item.id);
+ if (index == -1) {
+ // item is not yet selected -> select it
+ selection.push(item.id);
+ }
+ else {
+ // item is already selected -> deselect it
+ selection.splice(index, 1);
+ }
+ this.setSelection(selection);
+
+ this.emit('select', {
+ items: this.getSelection()
+ });
+
+ event.stopPropagation();
+ }
+};
+
+/**
+ * Convert a position on screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ * @private
+ */
+Timeline.prototype._toTime = function _toTime(x) {
+ var conversion = this.range.conversion(this.mainPanel.width);
+ return new Date(x / conversion.scale + conversion.offset);
+};
+
+/**
+ * Convert a datetime (Date object) into a position on the screen
+ * @param {Date} time A date
+ * @return {int} x The position on the screen in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+Timeline.prototype._toScreen = function _toScreen(time) {
+ var conversion = this.range.conversion(this.mainPanel.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+};
+
+(function(exports) {
+ /**
+ * Parse a text source containing data in DOT language into a JSON object.
+ * 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();
+ }
+
+ // token types enumeration
+ var TOKENTYPE = {
+ NULL : 0,
+ DELIMITER : 1,
+ IDENTIFIER: 2,
+ UNKNOWN : 3
+ };
+
+ // map with all delimiters
+ var DELIMITERS = {
+ '{': true,
+ '}': true,
+ '[': true,
+ ']': true,
+ ';': true,
+ '=': true,
+ ',': true,
+
+ '->': true,
+ '--': true
+ };
+
+ 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
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Preview the next character from the dot file.
+ * @return {String} cNext
+ */
+ function nextPreview() {
+ return dot.charAt(index + 1);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+ }
+ }
+ }
+
+ /**
+ * 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;
+
+ // 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;
+ }
+
+ // 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;
+ }
+ }
+ }
+
+ if (!current) {
+ // this is a new node
+ current = {
+ id: node.id
+ };
+ if (graph.node) {
+ // clone default attributes
+ current.attr = merge(current.attr, graph.node);
+ }
+ }
+
+ // add node to this (sub)graph and all its parent graphs
+ for (i = graphs.length - 1; i >= 0; i--) {
+ var g = graphs[i];
+
+ if (!g.nodes) {
+ g.nodes = [];
+ }
+ if (g.nodes.indexOf(current) == -1) {
+ g.nodes.push(current);
+ }
+ }
+
+ // merge attributes
+ if (node.attr) {
+ current.attr = merge(current.attr, node.attr);
+ }
+ }
+
+ /**
+ * 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
+ }
+ }
+
+ /**
+ * 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
+ };
+
+ if (graph.edge) {
+ edge.attr = merge({}, graph.edge); // clone default attributes
+ }
+ edge.attr = merge(edge.attr || {}, attr); // merge attributes
+
+ return 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 = '';
+
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
+ }
+
+ 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;
+ }
+
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
+ }
+ }
+ while (isComment);
+
+ // 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;
+ }
+
+ // check for delimiters consisting of 1 character
+ if (DELIMITERS[c]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c;
+ next();
+ return;
+ }
+
+ // 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;
+ }
+
+ // 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;
+ }
+
+ // something unknown is found, wrong characters, a syntax error
+ tokenType = TOKENTYPE.UNKNOWN;
+ while (c != '') {
+ token += c;
+ next();
+ }
+ 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();
+ }
+
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ graph.id = token;
+ getToken();
+ }
+
+ // open angle bracket
+ if (token != '{') {
+ throw newSyntaxError('Angle bracket { expected');
+ }
+ getToken();
+
+ // statements
+ parseStatements(graph);
+
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
+
+ // end of file
+ if (token !== '') {
+ throw newSyntaxError('End of file expected');
+ }
+ getToken();
+
+ // remove temporary default properties
+ delete graph.node;
+ delete graph.edge;
+ delete graph.graph;
+
+ return graph;
+ }
+
+ /**
+ * Parse a list with statements.
+ * @param {Object} graph
+ */
+ function parseStatements (graph) {
+ while (token !== '' && token != '}') {
+ parseStatement(graph);
+ if (token == ';') {
+ getToken();
+ }
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ // parse an attribute statement
+ var attr = parseAttributeStatement(graph);
+ if (attr) {
+ return;
+ }
+
+ // parse node
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier expected');
+ }
+ var id = token; // id can be a string or a number
+ getToken();
+
+ 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();
+
+ // 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;
+
+ // statements
+ parseStatements(subgraph);
+
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
+
+ // remove temporary default properties
+ delete subgraph.node;
+ delete subgraph.edge;
+ delete subgraph.graph;
+ delete subgraph.parent;
+
+ // register at the parent graph
+ if (!graph.subgraphs) {
+ graph.subgraphs = [];
+ }
+ graph.subgraphs.push(subgraph);
+ }
+
+ return subgraph;
+ }
+
+ /**
+ * 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();
+
+ // node attributes
+ graph.node = parseAttributeList();
+ return 'node';
+ }
+ else if (token == 'edge') {
+ getToken();
+
+ // edge attributes
+ graph.edge = parseAttributeList();
+ return 'edge';
+ }
+ else if (token == 'graph') {
+ getToken();
+
+ // graph attributes
+ graph.graph = parseAttributeList();
+ return 'graph';
+ }
+
+ 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;
+ }
+ addNode(graph, node);
+
+ // edge statements
+ parseEdge(graph, id);
+ }
+
+ /**
+ * 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();
+
+ 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();
+ }
+
+ // parse edge attributes
+ var attr = parseAttributeList();
+
+ // create edge
+ var edge = createEdge(graph, from, to, type, attr);
+ addEdge(graph, edge);
+
+ from = to;
+ }
+ }
+
+ /**
+ * Parse a set with attributes,
+ * for example [label="1.000", shape=solid]
+ * @return {Object | null} attr
+ */
+ function parseAttributeList() {
+ var attr = null;
+
+ while (token == '[') {
+ getToken();
+ attr = {};
+ while (token !== '' && token != ']') {
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute name expected');
+ }
+ var name = token;
+
+ getToken();
+ if (token != '=') {
+ throw newSyntaxError('Equal sign = expected');
+ }
+ getToken();
+
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute value expected');
+ }
+ var value = token;
+ setValue(attr, name, value); // name can be a path
+
+ getToken();
+ if (token ==',') {
+ getToken();
+ }
+ }
+
+ if (token != ']') {
+ throw newSyntaxError('Bracket ] expected');
+ }
+ getToken();
+ }
+
+ return attr;
+ }
+
+ /**
+ * 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 + ')');
+ }
+
+ /**
+ * 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) + '...');
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ /**
+ * 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);
+ });
+ }
+
+ // 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
+ }
+ }
+
+ 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);
+ });
+ }
+
+ 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 (dotEdge.to instanceof Object && dotEdge.to.edges) {
+ dotEdge.to.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
+ });
+ }
+
+ // copy the options
+ if (dotData.attr) {
+ graphData.options = dotData.attr;
+ }
+
+ return graphData;
+ }
+
+ // exports
+ exports.parseDOT = parseDOT;
+ exports.DOTToGraph = DOTToGraph;
+
+})(typeof util !== 'undefined' ? util : exports);
+
+/**
+ * Canvas shapes used by the Graph
+ */
+if (typeof CanvasRenderingContext2D !== 'undefined') {
+
+ /**
+ * Draw a circle shape
+ */
+ CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
+ this.beginPath();
+ this.arc(x, y, r, 0, 2*Math.PI, false);
+ };
+
+ /**
+ * 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);
+ };
+
+ /**
+ * 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();
+
+ 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();
+ };
+
+ /**
+ * 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();
+ };
+
+ /**
+ * 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();
+
+ 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();
+ };
+
+ /**
+ * 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);
+ };
+
+ /**
+ * 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
+
+ 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);
+ };
+
+
+
+ /**
+ * 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;
+
+ 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
+
+ this.beginPath();
+ this.moveTo(xe, ym);
+
+ this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
+ this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
+
+ this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
+ this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+
+ this.lineTo(xe, ymb);
+
+ this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
+ this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
+
+ this.lineTo(x, ym);
+ };
+
+
+ /**
+ * 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;
+
+ this.edges = []; // all edges connected to this node
+ this.dynamicEdges = [];
+ this.reroutedEdges = {};
+ this.group = constants.nodes.group;
+
+ this.fontSize = constants.nodes.fontSize;
+ this.fontFace = constants.nodes.fontFace;
+ this.fontColor = constants.nodes.fontColor;
+ this.fontDrawThreshold = 3;
+
+ this.color = constants.nodes.color;
+
+ // set defaults for the properties
+ this.id = undefined;
+ this.shape = constants.nodes.shape;
+ this.image = constants.nodes.image;
+ this.x = null;
+ this.y = null;
+ this.xFixed = false;
+ this.yFixed = false;
+ this.horizontalAlignLeft = true; // these are for the navigation controls
+ this.verticalAlignTop = true; // these are for the navigation controls
+ this.radius = constants.nodes.radius;
+ this.baseRadiusValue = constants.nodes.radius;
+ this.radiusFixed = false;
+ this.radiusMin = constants.nodes.radiusMin;
+ this.radiusMax = constants.nodes.radiusMax;
+ this.level = -1;
+ this.preassignedLevel = false;
+
+
+ this.imagelist = imagelist;
+ this.grouplist = grouplist;
+
+ // physics properties
+ this.fx = 0.0; // external force x
+ this.fy = 0.0; // external force y
+ this.vx = 0.0; // velocity x
+ this.vy = 0.0; // velocity y
+ this.minForce = constants.minForce;
+ this.damping = constants.physics.damping;
+ this.mass = 1; // kg
+ this.fixedData = {x:null,y:null};
+
+ this.setProperties(properties, constants);
+
+ // creating the variables for clustering
+ this.resetCluster();
+ this.dynamicEdgesLength = 0;
+ this.clusterSession = 0;
+ this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
+ this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
+ this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
+ this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
+ this.growthIndicator = 0;
+
+ // variables to tell the node about the graph.
+ this.graphScaleInv = 1;
+ this.graphScale = 1;
+ this.canvasTopLeft = {"x": -300, "y": -300};
+ this.canvasBottomRight = {"x": 300, "y": 300};
+ this.parentEdgeId = null;
+}
+
+/**
+ * (re)setting the clustering variables and objects
+ */
+Node.prototype.resetCluster = function() {
+ // clustering variables
+ this.formationScale = undefined; // this is used to determine when to open the cluster
+ this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
+ this.containedNodes = {};
+ this.containedEdges = {};
+ this.clusterSessions = [];
+};
+
+/**
+ * Attach a edge to the node
+ * @param {Edge} edge
+ */
+Node.prototype.attachEdge = function(edge) {
+ if (this.edges.indexOf(edge) == -1) {
+ this.edges.push(edge);
+ }
+ if (this.dynamicEdges.indexOf(edge) == -1) {
+ this.dynamicEdges.push(edge);
+ }
+ this.dynamicEdgesLength = this.dynamicEdges.length;
+};
+
+/**
+ * Detach a edge from the node
+ * @param {Edge} edge
+ */
+Node.prototype.detachEdge = function(edge) {
+ var index = this.edges.indexOf(edge);
+ if (index != -1) {
+ this.edges.splice(index, 1);
+ this.dynamicEdges.splice(index, 1);
+ }
+ this.dynamicEdgesLength = this.dynamicEdges.length;
+};
+
+
+/**
+ * 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;
+ }
+ this.originalLabel = undefined;
+ // basic properties
+ if (properties.id !== undefined) {this.id = properties.id;}
+ if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = 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;}
+ if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
+
+
+ // physics
+ if (properties.mass !== undefined) {this.mass = properties.mass;}
+
+ // navigation controls properties
+ if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
+ if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
+ if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
+
+ if (this.id === undefined) {
+ throw "Node must have an id";
+ }
+
+ // 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];
+ }
+ }
+ }
+
+ // 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 = util.parseColor(properties.color);}
+
+ if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
+ if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
+ if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
+
+ if (this.image !== undefined && this.image != "") {
+ if (this.imagelist) {
+ this.imageObj = this.imagelist.load(this.image);
+ }
+ else {
+ throw "No imagelist provided";
+ }
+ }
+
+ this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
+ this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
+ this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
+
+ if (this.shape == 'image') {
+ 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;
+ }
+ // reset the size of the node, this can be changed
+ this._reset();
+};
+
+/**
+ * select this node
+ */
+Node.prototype.select = function() {
+ this.selected = true;
+ this._reset();
+};
+
+/**
+ * unselect this node
+ */
+Node.prototype.unselect = function() {
+ this.selected = false;
+ this._reset();
+};
+
+
+/**
+ * Reset the calculated size of the node, forces it to recalculate its size
+ */
+Node.prototype.clearSizeCache = function() {
+ this._reset();
+};
+
+/**
+ * Reset the calculated size of the node, forces it to recalculate its size
+ * @private
+ */
+Node.prototype._reset = function() {
+ this.width = undefined;
+ this.height = undefined;
+};
+
+/**
+ * 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 typeof this.title === "function" ? this.title() : this.title;
+};
+
+/**
+ * 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;
+
+ if (!this.width) {
+ this.resize(ctx);
+ }
+
+ switch (this.shape) {
+ case 'circle':
+ case 'dot':
+ return this.radius + borderWidth;
+
+ 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);
+
+ // TODO: implement distanceToBorder for database
+ // TODO: implement distanceToBorder for triangle
+ // TODO: implement distanceToBorder for triangleDown
+
+ 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;
+ }
+
+ }
+ // 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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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
+ }
+
+ 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
+ }
+};
+
+
+
+/**
+ * Perform one discrete step for the node
+ * @param {number} interval Time interval in seconds
+ */
+Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
+ if (!this.xFixed) {
+ var dx = this.damping * this.vx; // damping force
+ var ax = (this.fx - dx) / this.mass; // acceleration
+ this.vx += ax * interval; // velocity
+ this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
+ this.x += this.vx * interval; // position
+ }
+ else {
+ this.fx = 0;
+ }
+
+ if (!this.yFixed) {
+ var dy = this.damping * this.vy; // damping force
+ var ay = (this.fy - dy) / this.mass; // acceleration
+ this.vy += ay * interval; // velocity
+ this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
+ this.y += this.vy * interval; // position
+ }
+ else {
+ this.fy = 0;
+ }
+};
+
+/**
+ * Check if this node has a fixed x and y position
+ * @return {boolean} true if fixed, false if not
+ */
+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);
+};
+
+/**
+ * check if this node is selecte
+ * @return {boolean} selected True if node is selected, else false
+ */
+Node.prototype.isSelected = function() {
+ return this.selected;
+};
+
+/**
+ * Retrieve the value of the node. Can be undefined
+ * @return {Number} value
+ */
+Node.prototype.getValue = function() {
+ return this.value;
+};
+
+/**
+ * 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);
+};
+
+
+/**
+ * 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) {
+ if (max == min) {
+ this.radius = (this.radiusMin + this.radiusMax) / 2;
+ }
+ else {
+ var scale = (this.radiusMax - this.radiusMin) / (max - min);
+ this.radius = (this.value - min) * scale + this.radiusMin;
+ }
+ }
+ this.baseRadiusValue = this.radius;
+};
+
+/**
+ * Draw 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.draw = function(ctx) {
+ throw "Draw method not initialized for node";
+};
+
+/**
+ * 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 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
+ */
+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);
+};
+
+Node.prototype._resizeImage = function (ctx) {
+ // TODO: pre calculate the image size
+
+ if (!this.width || !this.height) { // undefined or 0
+ var width, height;
+ if (this.value) {
+ this.radius = this.baseRadiusValue;
+ var scale = this.imageObj.height / this.imageObj.width;
+ if (scale !== undefined) {
+ width = this.radius || this.imageObj.width;
+ height = this.radius * scale || this.imageObj.height;
+ }
+ else {
+ width = 0;
+ height = 0;
+ }
+ }
+ else {
+ width = this.imageObj.width;
+ height = this.imageObj.height;
+ }
+ this.width = width;
+ this.height = height;
+
+ this.growthIndicator = 0;
+ if (this.width > 0 && this.height > 0) {
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
+ this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.width - width;
+ }
+ }
+
+};
+
+Node.prototype._drawImage = function (ctx) {
+ this._resizeImage(ctx);
+
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var yLabel;
+ if (this.imageObj.width != 0 ) {
+ // draw the shade
+ if (this.clusterSize > 1) {
+ var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
+ lineWidth *= this.graphScaleInv;
+ lineWidth = Math.min(0.2 * this.width,lineWidth);
+
+ ctx.globalAlpha = 0.5;
+ ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
+ }
+
+ // draw the image
+ ctx.globalAlpha = 1.0;
+ ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
+ yLabel = this.y + this.height / 2;
+ }
+ else {
+ // image still loading... just draw the label for now
+ yLabel = this.y;
+ }
+
+ 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;
+
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
+ this.growthIndicator = this.width - (textSize.width + 2 * margin);
+// this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
+
+ }
+};
+
+Node.prototype._drawBox = function (ctx) {
+ this._resizeBox(ctx);
+
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var clusterLineWidth = 2.5;
+ var selectionLineWidth = 2;
+
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+
+ // draw the outer border
+ if (this.clusterSize > 1) {
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
+ ctx.stroke();
+ }
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+
+ 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);
+};
+
+
+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;
+
+ // scaling used for clustering
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
+ this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.width - size;
+ }
+};
+
+Node.prototype._drawDatabase = function (ctx) {
+ this._resizeDatabase(ctx);
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var clusterLineWidth = 2.5;
+ var selectionLineWidth = 2;
+
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+
+ // draw the outer border
+ if (this.clusterSize > 1) {
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
+ ctx.stroke();
+ }
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+ 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);
+};
+
+
+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;
+
+ // scaling used for clustering
+// this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
+// this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
+ this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.radius - 0.5*diameter;
+ }
+};
+
+Node.prototype._drawCircle = function (ctx) {
+ this._resizeCircle(ctx);
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var clusterLineWidth = 2.5;
+ var selectionLineWidth = 2;
+
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+
+ // draw the outer border
+ if (this.clusterSize > 1) {
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
+ ctx.stroke();
+ }
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+ ctx.circle(this.x, this.y, this.radius);
+ ctx.fill();
+ ctx.stroke();
+
+ this._label(ctx, this.label, this.x, this.y);
+};
+
+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;
+ }
+ var defaultSize = this.width;
+
+ // scaling used for clustering
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
+ this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.width - defaultSize;
+ }
+};
+
+Node.prototype._drawEllipse = function (ctx) {
+ this._resizeEllipse(ctx);
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var clusterLineWidth = 2.5;
+ var selectionLineWidth = 2;
+
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+
+ // draw the outer border
+ if (this.clusterSize > 1) {
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
+ ctx.stroke();
+ }
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+
+ ctx.ellipse(this.left, this.top, this.width, this.height);
+ ctx.fill();
+ ctx.stroke();
+ this._label(ctx, this.label, this.x, this.y);
+};
+
+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');
+};
+
+Node.prototype._drawSquare = function (ctx) {
+ this._drawShape(ctx, 'square');
+};
+
+Node.prototype._drawStar = function (ctx) {
+ this._drawShape(ctx, 'star');
+};
+
+Node.prototype._resizeShape = function (ctx) {
+ if (!this.width) {
+ this.radius = this.baseRadiusValue;
+ var size = 2 * this.radius;
+ this.width = size;
+ this.height = size;
+
+ // scaling used for clustering
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
+ this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.width - size;
+ }
+};
+
+Node.prototype._drawShape = function (ctx, shape) {
+ this._resizeShape(ctx);
+
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var clusterLineWidth = 2.5;
+ var selectionLineWidth = 2;
+ var radiusMultiplier = 2;
+
+ // choose draw method depending on the shape
+ switch (shape) {
+ case 'dot': radiusMultiplier = 2; break;
+ case 'square': radiusMultiplier = 2; break;
+ case 'triangle': radiusMultiplier = 3; break;
+ case 'triangleDown': radiusMultiplier = 3; break;
+ case 'star': radiusMultiplier = 4; break;
+ }
+
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+
+ // draw the outer border
+ if (this.clusterSize > 1) {
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
+ ctx.stroke();
+ }
+ ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
+
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+
+ ctx[shape](this.x, this.y, this.radius);
+ ctx.fill();
+ ctx.stroke();
+
+ if (this.label) {
+ this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
+ }
+};
+
+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;
+
+ // scaling used for clustering
+ this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
+ this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
+ this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
+ this.growthIndicator = this.width - (textSize.width + 2 * margin);
+ }
+};
+
+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);
+};
+
+
+Node.prototype._label = function (ctx, text, x, y, align, baseline) {
+ if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
+ ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
+ ctx.fillStyle = this.fontColor || "black";
+ ctx.textAlign = align || "center";
+ ctx.textBaseline = baseline || "middle";
+
+ var lines = text.split('\n'),
+ lineCount = lines.length,
+ fontSize = (this.fontSize + 4),
+ yLine = y + (1 - lineCount) / 2 * fontSize;
+
+ for (var i = 0; i < lineCount; i++) {
+ ctx.fillText(lines[i], x, yLine);
+ yLine += fontSize;
+ }
+ }
+};
+
+
+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};
+ }
+};
+
+/**
+ * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
+ * there is a safety margin of 0.3 * width;
+ *
+ * @returns {boolean}
+ */
+Node.prototype.inArea = function() {
+ if (this.width !== undefined) {
+ return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
+ this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
+ this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
+ this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
+ }
+ else {
+ return true;
+ }
+};
+
+/**
+ * checks if the core of the node is in the display area, this is used for opening clusters around zoom
+ * @returns {boolean}
+ */
+Node.prototype.inView = function() {
+ return (this.x >= this.canvasTopLeft.x &&
+ this.x < this.canvasBottomRight.x &&
+ this.y >= this.canvasTopLeft.y &&
+ this.y < this.canvasBottomRight.y);
+};
+
+/**
+ * This allows the zoom level of the graph to influence the rendering
+ * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
+ *
+ * @param scale
+ * @param canvasTopLeft
+ * @param canvasBottomRight
+ */
+Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
+ this.graphScaleInv = 1.0/scale;
+ this.graphScale = scale;
+ this.canvasTopLeft = canvasTopLeft;
+ this.canvasBottomRight = canvasBottomRight;
+};
+
+
+/**
+ * This allows the zoom level of the graph to influence the rendering
+ *
+ * @param scale
+ */
+Node.prototype.setScale = function(scale) {
+ this.graphScaleInv = 1.0/scale;
+ this.graphScale = scale;
+};
+
+
+
+/**
+ * set the velocity at 0. Is called when this node is contained in another during clustering
+ */
+Node.prototype.clearVelocity = function() {
+ this.vx = 0;
+ this.vy = 0;
+};
+
+
+/**
+ * Basic preservation of (kinectic) energy
+ *
+ * @param massBeforeClustering
+ */
+Node.prototype.updateVelocity = function(massBeforeClustering) {
+ var energyBefore = this.vx * this.vx * massBeforeClustering;
+ //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
+ this.vx = Math.sqrt(energyBefore/this.mass);
+ energyBefore = this.vy * this.vy * massBeforeClustering;
+ //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
+ this.vy = Math.sqrt(energyBefore/this.mass);
+};
+
+
+/**
+ * @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
+ */
+function Edge (properties, graph, constants) {
+ if (!graph) {
+ throw "No graph provided";
+ }
+ this.graph = graph;
+
+ // initialize constants
+ this.widthMin = constants.edges.widthMin;
+ this.widthMax = constants.edges.widthMax;
+
+ // 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.physics.springLength;
+ this.customLength = false;
+ this.selected = false;
+ this.smooth = constants.smoothCurves;
+
+ this.from = null; // a node
+ this.to = null; // a node
+ this.via = null; // a temp node
+
+ // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
+ // by storing the original information we can revert to the original connection when the cluser is opened.
+ this.originalFromId = [];
+ this.originalToId = [];
+
+ this.connected = false;
+
+ // Added to support dashed lines
+ // David Jordan
+ // 2012-08-08
+ this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
+
+ this.color = {color:constants.edges.color.color,
+ highlight:constants.edges.color.highlight};
+ this.widthFixed = false;
+ this.lengthFixed = false;
+
+ this.setProperties(properties, constants);
+}
+
+/**
+ * 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;
+ }
+
+ 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;
+ this.fontFill = constants.edges.fontFill;
+
+ if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
+ if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
+ if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
+ if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
+ }
+
+ if (properties.title !== undefined) {this.title = properties.title;}
+ if (properties.width !== undefined) {this.width = properties.width;}
+ if (properties.value !== undefined) {this.value = properties.value;}
+ if (properties.length !== undefined) {this.length = properties.length;
+ this.customLength = true;}
+
+ // 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) {
+ if (util.isString(properties.color)) {
+ this.color.color = properties.color;
+ this.color.highlight = properties.color;
+ }
+ else {
+ if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
+ if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
+ }
+ }
+
+ // A node is connected when it has a from and to node.
+ this.connect();
+
+ this.widthFixed = this.widthFixed || (properties.width !== undefined);
+ this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
+
+ // 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;
+ }
+};
+
+/**
+ * Connect an edge to its nodes
+ */
+Edge.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);
+
+ 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);
+ }
+ }
+};
+
+/**
+ * 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;
+ }
+
+ 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 typeof this.title === "function" ? this.title() : this.title;
+};
+
+
+/**
+ * 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 scale = (this.widthMax - this.widthMin) / (max - min);
+ this.width = (this.value - min) * scale + this.widthMin;
+ }
+};
+
+/**
+ * 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";
+};
+
+/**
+ * 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) {
+ if (this.connected) {
+ var distMax = 10;
+ var xFrom = this.from.x;
+ var yFrom = this.from.y;
+ var xTo = this.to.x;
+ var yTo = this.to.y;
+ var xObj = obj.left;
+ var yObj = obj.top;
+
+ var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
+
+ return (dist < distMax);
+ }
+ else {
+ return false
+ }
+};
+
+
+/**
+ * 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
+ if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
+ else {ctx.strokeStyle = this.color.color;}
+ ctx.lineWidth = this._getLineWidth();
+
+ if (this.from != this.to) {
+ // draw line
+ this._line(ctx);
+
+ // draw label
+ var point;
+ if (this.label) {
+ if (this.smooth == true) {
+ var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+ this._label(ctx, this.label, point.x, point.y);
+ }
+ }
+ 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 {
+ 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);
+ }
+};
+
+/**
+ * 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.selected == true) {
+ return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
+ }
+ else {
+ return this.width*this.graphScaleInv;
+ }
+};
+
+/**
+ * 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);
+ if (this.smooth == true) {
+ ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
+ }
+ else {
+ ctx.lineTo(this.to.x, this.to.y);
+ }
+ ctx.stroke();
+};
+
+/**
+ * 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();
+};
+
+/**
+ * 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 = this.fontFill;
+ var width = ctx.measureText(text).width;
+ var height = this.fontSize;
+ var left = x - width / 2;
+ var top = y - height / 2;
+
+ ctx.fillRect(left, top, width, height);
+
+ // draw text
+ ctx.fillStyle = this.fontColor || "black";
+ ctx.textAlign = "left";
+ ctx.textBaseline = "top";
+ ctx.fillText(text, left, top);
+ }
+};
+
+/**
+ * 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
+ if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
+ else {ctx.strokeStyle = this.color.color;}
+
+ ctx.lineWidth = this._getLineWidth();
+
+ // only firefox and chrome support this method, else we use the legacy one.
+ if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
+ ctx.beginPath();
+ ctx.moveTo(this.from.x, this.from.y);
+
+ // configure the dash pattern
+ var pattern = [0];
+ if (this.dash.length !== undefined && this.dash.gap !== undefined) {
+ pattern = [this.dash.length,this.dash.gap];
+ }
+ else {
+ pattern = [5,5];
+ }
+
+ // set dash settings for chrome or firefox
+ if (typeof ctx.setLineDash !== 'undefined') { //Chrome
+ ctx.setLineDash(pattern);
+ ctx.lineDashOffset = 0;
+
+ } else { //Firefox
+ ctx.mozDash = pattern;
+ ctx.mozDashOffset = 0;
+ }
+
+ // draw the line
+ if (this.smooth == true) {
+ ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
+ }
+ else {
+ ctx.lineTo(this.to.x, this.to.y);
+ }
+ ctx.stroke();
+
+ // restore the dash settings.
+ if (typeof ctx.setLineDash !== 'undefined') { //Chrome
+ ctx.setLineDash([0]);
+ ctx.lineDashOffset = 0;
+
+ } else { //Firefox
+ ctx.mozDash = [0];
+ ctx.mozDashOffset = 0;
+ }
+ }
+ else { // unsupporting smooth lines
+ // draw dashed line
+ ctx.beginPath();
+ ctx.lineCap = 'round';
+ if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
+ }
+ else if (this.dash.length !== undefined && this.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.dash.length,this.dash.gap]);
+ }
+ else //If all else fails draw a line
+ {
+ ctx.moveTo(this.from.x, this.from.y);
+ ctx.lineTo(this.to.x, this.to.y);
+ }
+ ctx.stroke();
+ }
+
+ // draw label
+ if (this.label) {
+ var point;
+ if (this.smooth == true) {
+ var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+ this._label(ctx, this.label, point.x, point.y);
+ }
+};
+
+/**
+ * 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
+ }
+};
+
+/**
+ * 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)
+ }
+};
+
+/**
+ * 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
+ if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
+ else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
+ ctx.lineWidth = this._getLineWidth();
+
+ if (this.from != this.to) {
+ // draw line
+ this._line(ctx);
+
+ 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?
+ // draw an arrow halfway the line
+ if (this.smooth == true) {
+ var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+
+ ctx.arrow(point.x, point.y, angle, length);
+ ctx.fill();
+ ctx.stroke();
+
+ // draw label
+ if (this.label) {
+ this._label(ctx, this.label, point.x, point.y);
+ }
+ }
+ else {
+ // draw circle
+ var x, y;
+ var radius = 0.25 * Math.max(100,this.length);
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width * 0.5;
+ y = node.y - radius;
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - node.height * 0.5;
+ }
+ 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();
+
+ // draw label
+ if (this.label) {
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
+ }
+};
+
+
+
+/**
+ * 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
+ if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
+ else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
+
+ ctx.lineWidth = this._getLineWidth();
+
+ var angle, length;
+ //draw a line
+ if (this.from != this.to) {
+ angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var dx = (this.to.x - this.from.x);
+ var dy = (this.to.y - this.from.y);
+ var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+
+ var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
+ var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
+ var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
+ var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
+
+
+ if (this.smooth == true) {
+ angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
+ dx = (this.to.x - this.via.x);
+ dy = (this.to.y - this.via.y);
+ edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ }
+ var toBorderDist = this.to.distanceToBorder(ctx, angle);
+ var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
+
+ var xTo,yTo;
+ if (this.smooth == true) {
+ xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
+ }
+ else {
+ xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
+ yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
+ }
+
+ ctx.beginPath();
+ ctx.moveTo(xFrom,yFrom);
+ if (this.smooth == true) {
+ ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
+ }
+ else {
+ ctx.lineTo(xTo, yTo);
+ }
+ ctx.stroke();
+
+ // draw arrow at the end of the line
+ length = 10 + 5 * this.width;
+ ctx.arrow(xTo, yTo, angle, length);
+ ctx.fill();
+ ctx.stroke();
+
+ // draw label
+ if (this.label) {
+ var point;
+ if (this.smooth == true) {
+ var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
+ var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
+ point = {x:midpointX, y:midpointY};
+ }
+ else {
+ point = this._pointOnLine(0.5);
+ }
+ this._label(ctx, this.label, point.x, point.y);
+ }
+ }
+ else {
+ // draw circle
+ var node = this.from;
+ var x, y, arrow;
+ var radius = 0.25 * Math.max(100,this.length);
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width * 0.5;
+ 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 * 0.5;
+ arrow = {
+ x: node.x,
+ y: y,
+ angle: 0.6 * Math.PI
+ };
+ }
+ ctx.beginPath();
+ // 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);
+ }
+ }
+};
+
+
+
+/**
+ * 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.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
+ if (this.smooth == true) {
+ var minDistance = 1e9;
+ var i,t,x,y,dx,dy;
+ for (i = 0; i < 10; i++) {
+ t = 0.1*i;
+ x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
+ y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
+ dx = Math.abs(x3-x);
+ dy = Math.abs(y3-y);
+ minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
+ }
+ return minDistance
+ }
+ else {
+ var px = x2-x1,
+ py = y2-y1,
+ something = px*px + py*py,
+ u = ((x3 - x1) * px + (y3 - y1) * py) / something;
+
+ if (u > 1) {
+ u = 1;
+ }
+ else if (u < 0) {
+ u = 0;
+ }
+
+ var x = x1 + u * px,
+ y = y1 + u * py,
+ dx = x - x3,
+ dy = y - y3;
+
+ //# Note: If the actual distance does not matter,
+ //# if you only want to compare what this function
+ //# returns to other results of this function, you
+ //# can just return the squared distance instead
+ //# (i.e. remove the sqrt) to gain a little performance
+
+ return Math.sqrt(dx*dx + dy*dy);
+ }
+};
+
+
+
+/**
+ * This allows the zoom level of the graph to influence the rendering
+ *
+ * @param scale
+ */
+Edge.prototype.setScale = function(scale) {
+ this.graphScaleInv = 1.0/scale;
+};
+
+
+Edge.prototype.select = function() {
+ this.selected = true;
+};
+
+Edge.prototype.unselect = function() {
+ this.selected = false;
+};
+
+Edge.prototype.positionBezierNode = function() {
+ if (this.via !== null) {
+ this.via.x = 0.5 * (this.from.x + this.to.x);
+ this.via.y = 0.5 * (this.from.y + this.to.y);
+ }
+};
+/**
+ * Popup is a class to create a popup window with some text
+ * @param {Element} container The container object.
+ * @param {Number} [x]
+ * @param {Number} [y]
+ * @param {String} [text]
+ * @param {Object} [style] An object containing borderColor,
+ * backgroundColor, etc.
+ */
+function Popup(container, x, y, text, style) {
+ if (container) {
+ this.container = container;
+ }
+ else {
+ this.container = document.body;
+ }
+
+ // x, y and text are optional, see if a style object was passed in their place
+ if (style === undefined) {
+ if (typeof x === "object") {
+ style = x;
+ x = undefined;
+ } else if (typeof text === "object") {
+ style = text;
+ text = undefined;
+ } else {
+ // for backwards compatibility, in case clients other than Graph are creating Popup directly
+ style = {
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ color: {
+ border: '#666',
+ background: '#FFFFC6'
+ }
+ }
+ }
+ }
+
+ this.x = 0;
+ this.y = 0;
+ this.padding = 5;
+
+ if (x !== undefined && y !== undefined ) {
+ this.setPosition(x, y);
+ }
+ if (text !== undefined) {
+ this.setText(text);
+ }
+
+ // create the frame
+ this.frame = document.createElement("div");
+ var styleAttr = this.frame.style;
+ styleAttr.position = "absolute";
+ styleAttr.visibility = "hidden";
+ styleAttr.border = "1px solid " + style.color.border;
+ styleAttr.color = style.fontColor;
+ styleAttr.fontSize = style.fontSize + "px";
+ styleAttr.fontFamily = style.fontFace;
+ styleAttr.padding = this.padding + "px";
+ styleAttr.backgroundColor = style.color.background;
+ styleAttr.borderRadius = "3px";
+ styleAttr.MozBorderRadius = "3px";
+ styleAttr.WebkitBorderRadius = "3px";
+ styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
+ styleAttr.whiteSpace = "nowrap";
+ this.container.appendChild(this.frame);
+}
+
+/**
+ * @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);
+};
+
+/**
+ * Set the text for the popup window. This can be HTML code
+ * @param {string} text
+ */
+Popup.prototype.setText = function(text) {
+ this.frame.innerHTML = text;
+};
+
+/**
+ * Show the popup window
+ * @param {boolean} show Optional. Show or hide the window
+ */
+Popup.prototype.show = function (show) {
+ if (show === undefined) {
+ show = true;
+ }
+
+ if (show) {
+ var height = this.frame.clientHeight;
+ var width = this.frame.clientWidth;
+ var maxHeight = this.frame.parentNode.clientHeight;
+ var maxWidth = this.frame.parentNode.clientWidth;
+
+ var top = (this.y - height);
+ if (top + height + this.padding > maxHeight) {
+ top = maxHeight - height - this.padding;
+ }
+ if (top < this.padding) {
+ top = this.padding;
+ }
+
+ var left = this.x;
+ if (left + width + this.padding > maxWidth) {
+ left = maxWidth - width - this.padding;
+ }
+ if (left < this.padding) {
+ left = this.padding;
+ }
+
+ this.frame.style.left = left + "px";
+ this.frame.style.top = top + "px";
+ this.frame.style.visibility = "visible";
+ }
+ else {
+ this.hide();
+ }
+};
+
+/**
+ * Hide the popup window
+ */
+Popup.prototype.hide = function () {
+ this.frame.style.visibility = "hidden";
+};
+
+/**
+ * @class Groups
+ * This class can store groups and properties specific for groups.
+ */
+Groups = function () {
+ this.clear();
+ this.defaultIndex = 0;
+};
+
+
+/**
+ * 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
+];
+
+
+/**
+ * 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;
+ }
+};
+
+
+/**
+ * get group properties of a groupname. If groupname is not found, a new group
+ * is added.
+ * @param {*} groupname Can be a number, string, Date, etc.
+ * @return {Object} group The created group, containing all group properties
+ */
+Groups.prototype.get = function (groupname) {
+ var group = this.groups[groupname];
+
+ if (group == undefined) {
+ // create new group
+ var index = this.defaultIndex % Groups.DEFAULT.length;
+ this.defaultIndex++;
+ group = {};
+ group.color = Groups.DEFAULT[index];
+ this.groups[groupname] = group;
+ }
+
+ return group;
+};
+
+/**
+ * Add a custom group style
+ * @param {String} groupname
+ * @param {Object} style An object containing borderColor,
+ * backgroundColor, etc.
+ * @return {Object} group The created group object
+ */
+Groups.prototype.add = function (groupname, style) {
+ this.groups[groupname] = style;
+ if (style.color) {
+ style.color = util.parseColor(style.color);
+ }
+ return style;
+};
+
+/**
+ * @class Images
+ * This class loads images and keeps them stored.
+ */
+Images = function () {
+ this.images = {};
+
+ this.callback = undefined;
+};
+
+/**
+ * Set an onload callback function. This will be called each time an image
+ * is loaded
+ * @param {function} callback
+ */
+Images.prototype.setOnloadCallback = function(callback) {
+ this.callback = callback;
+};
+
+/**
+ *
+ * @param {string} url Url of the image
+ * @return {Image} img The image object
+ */
+Images.prototype.load = function(url) {
+ var img = this.images[url];
+ if (img == undefined) {
+ // create the image
+ var images = this;
+ img = new Image();
+ this.images[url] = img;
+ img.onload = function() {
+ if (images.callback) {
+ images.callback(this);
+ }
+ };
+ img.src = url;
+ }
+
+ return img;
+};
+
+/**
+ * Created by Alex on 2/6/14.
+ */
+
+
+var physicsMixin = {
+
+ /**
+ * Toggling barnes Hut calculation on and off.
+ *
+ * @private
+ */
+ _toggleBarnesHut: function () {
+ this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
+ this._loadSelectedForceSolver();
+ this.moving = true;
+ this.start();
+ },
+
+
+ /**
+ * This loads the node force solver based on the barnes hut or repulsion algorithm
+ *
+ * @private
+ */
+ _loadSelectedForceSolver: function () {
+ // this overloads the this._calculateNodeForces
+ if (this.constants.physics.barnesHut.enabled == true) {
+ this._clearMixin(repulsionMixin);
+ this._clearMixin(hierarchalRepulsionMixin);
+
+ this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
+ this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
+ this.constants.physics.damping = this.constants.physics.barnesHut.damping;
+
+ this._loadMixin(barnesHutMixin);
+ }
+ else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
+ this._clearMixin(barnesHutMixin);
+ this._clearMixin(repulsionMixin);
+
+ this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
+ this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
+ this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
+
+ this._loadMixin(hierarchalRepulsionMixin);
+ }
+ else {
+ this._clearMixin(barnesHutMixin);
+ this._clearMixin(hierarchalRepulsionMixin);
+ this.barnesHutTree = undefined;
+
+ this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
+ this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
+ this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
+ this.constants.physics.damping = this.constants.physics.repulsion.damping;
+
+ this._loadMixin(repulsionMixin);
+ }
+ },
+
+ /**
+ * Before calculating the forces, we check if we need to cluster to keep up performance and we check
+ * if there is more than one node. If it is just one node, we dont calculate anything.
+ *
+ * @private
+ */
+ _initializeForceCalculation: function () {
+ // stop calculation if there is only one node
+ if (this.nodeIndices.length == 1) {
+ this.nodes[this.nodeIndices[0]]._setForce(0, 0);
+ }
+ else {
+ // if there are too many nodes on screen, we cluster without repositioning
+ if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
+ this.clusterToFit(this.constants.clustering.reduceToNodes, false);
+ }
+
+ // we now start the force calculation
+ this._calculateForces();
+ }
+ },
+
+
+ /**
+ * Calculate the external forces acting on the nodes
+ * Forces are caused by: edges, repulsing forces between nodes, gravity
+ * @private
+ */
+ _calculateForces: function () {
+ // Gravity is required to keep separated groups from floating off
+ // the forces are reset to zero in this loop by using _setForce instead
+ // of _addForce
+
+ this._calculateGravitationalForces();
+ this._calculateNodeForces();
+
+ if (this.constants.smoothCurves == true) {
+ this._calculateSpringForcesWithSupport();
+ }
+ else {
+ this._calculateSpringForces();
+ }
+ },
+
+
+ /**
+ * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
+ * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
+ * This function joins the datanodes and invisible (called support) nodes into one object.
+ * We do this so we do not contaminate this.nodes with the support nodes.
+ *
+ * @private
+ */
+ _updateCalculationNodes: function () {
+ if (this.constants.smoothCurves == true) {
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
+
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.calculationNodes[nodeId] = this.nodes[nodeId];
+ }
+ }
+ var supportNodes = this.sectors['support']['nodes'];
+ for (var supportNodeId in supportNodes) {
+ if (supportNodes.hasOwnProperty(supportNodeId)) {
+ if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
+ this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
+ }
+ else {
+ supportNodes[supportNodeId]._setForce(0, 0);
+ }
+ }
+ }
+
+ for (var idx in this.calculationNodes) {
+ if (this.calculationNodes.hasOwnProperty(idx)) {
+ this.calculationNodeIndices.push(idx);
+ }
+ }
+ }
+ else {
+ this.calculationNodes = this.nodes;
+ this.calculationNodeIndices = this.nodeIndices;
+ }
+ },
+
+
+ /**
+ * this function applies the central gravity effect to keep groups from floating off
+ *
+ * @private
+ */
+ _calculateGravitationalForces: function () {
+ var dx, dy, distance, node, i;
+ var nodes = this.calculationNodes;
+ var gravity = this.constants.physics.centralGravity;
+ var gravityForce = 0;
+
+ for (i = 0; i < this.calculationNodeIndices.length; i++) {
+ node = nodes[this.calculationNodeIndices[i]];
+ node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
+ // gravity does not apply when we are in a pocket sector
+ if (this._sector() == "default" && gravity != 0) {
+ dx = -node.x;
+ dy = -node.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ gravityForce = (distance == 0) ? 0 : (gravity / distance);
+ node.fx = dx * gravityForce;
+ node.fy = dy * gravityForce;
+ }
+ else {
+ node.fx = 0;
+ node.fy = 0;
+ }
+ }
+ },
+
+
+ /**
+ * this function calculates the effects of the springs in the case of unsmooth curves.
+ *
+ * @private
+ */
+ _calculateSpringForces: function () {
+ var edgeLength, edge, edgeId;
+ var dx, dy, fx, fy, springForce, length;
+ var edges = this.edges;
+
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
+ // this implies that the edges between big clusters are longer
+ edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
+
+ dx = (edge.from.x - edge.to.x);
+ dy = (edge.from.y - edge.to.y);
+ length = Math.sqrt(dx * dx + dy * dy);
+
+ if (length == 0) {
+ length = 0.01;
+ }
+
+ springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
+
+ fx = dx * springForce;
+ fy = dy * springForce;
+
+ edge.from.fx += fx;
+ edge.from.fy += fy;
+ edge.to.fx -= fx;
+ edge.to.fy -= fy;
+ }
+ }
+ }
+ }
+ },
+
+
+ /**
+ * This function calculates the springforces on the nodes, accounting for the support nodes.
+ *
+ * @private
+ */
+ _calculateSpringForcesWithSupport: function () {
+ var edgeLength, edge, edgeId, combinedClusterSize;
+ var edges = this.edges;
+
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ if (edge.via != null) {
+ var node1 = edge.to;
+ var node2 = edge.via;
+ var node3 = edge.from;
+
+ edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
+
+ combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
+
+ // this implies that the edges between big clusters are longer
+ edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
+ this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
+ this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
+ }
+ }
+ }
+ }
+ }
+ },
+
+
+ /**
+ * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
+ *
+ * @param node1
+ * @param node2
+ * @param edgeLength
+ * @private
+ */
+ _calculateSpringForce: function (node1, node2, edgeLength) {
+ var dx, dy, fx, fy, springForce, length;
+
+ dx = (node1.x - node2.x);
+ dy = (node1.y - node2.y);
+ length = Math.sqrt(dx * dx + dy * dy);
+
+ if (length == 0) {
+ length = 0.01;
+ }
+
+ springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
+
+ fx = dx * springForce;
+ fy = dy * springForce;
+
+ node1.fx += fx;
+ node1.fy += fy;
+ node2.fx -= fx;
+ node2.fy -= fy;
+ },
+
+
+ /**
+ * Load the HTML for the physics config and bind it
+ * @private
+ */
+ _loadPhysicsConfiguration: function () {
+ if (this.physicsConfiguration === undefined) {
+ this.backupConstants = {};
+ util.copyObject(this.constants, this.backupConstants);
+
+ var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
+ this.physicsConfiguration = document.createElement('div');
+ this.physicsConfiguration.className = "PhysicsConfiguration";
+ this.physicsConfiguration.innerHTML = '' +
+ '
Simulation Mode:
' +
+ '
' +
+ '
Barnes Hut
' +
+ '
Repulsion
' +
+ '
Hierarchical
' +
+ '
' +
+ '
' +
+ '
' +
+ '
Barnes Hut
' +
+ '
' +
+ '
gravitationalConstant
0
-20000
' +
+ '
' +
+ '
' +
+ '
centralGravity
0
3
' +
+ '
' +
+ '
' +
+ '
springLength
0
500
' +
+ '
' +
+ '
' +
+ '
springConstant
0
0.5
' +
+ '
' +
+ '
' +
+ '
damping
0
0.3
' +
+ '
' +
+ '
' +
+ '
' +
+ '
Repulsion
' +
+ '
' +
+ '
nodeDistance
0
300
' +
+ '
' +
+ '
' +
+ '
centralGravity
0
3
' +
+ '
' +
+ '
' +
+ '
springLength
0
500
' +
+ '
' +
+ '
' +
+ '
springConstant
0
0.5
' +
+ '
' +
+ '
' +
+ '
damping
0
0.3
' +
+ '
' +
+ '
' +
+ '
' +
+ '
Hierarchical
' +
+ '
' +
+ '
nodeDistance
0
300
' +
+ '
' +
+ '
' +
+ '
centralGravity
0
3
' +
+ '
' +
+ '
' +
+ '
springLength
0
500
' +
+ '
' +
+ '
' +
+ '
springConstant
0
0.5
' +
+ '
' +
+ '
' +
+ '
damping
0
0.3
' +
+ '
' +
+ '
' +
+ '
direction
1
4
' +
+ '
' +
+ '
' +
+ '
levelSeparation
1
500
' +
+ '
' +
+ '
' +
+ '
nodeSpacing
1
500
' +
+ '
' +
+ '
' +
+ '
Options:
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
'
+ this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
+ this.optionsDiv = document.createElement("div");
+ this.optionsDiv.style.fontSize = "14px";
+ this.optionsDiv.style.fontFamily = "verdana";
+ this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
+
+ var rangeElement;
+ rangeElement = document.getElementById('graph_BH_gc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
+ rangeElement = document.getElementById('graph_BH_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_BH_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_BH_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_BH_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
+
+ rangeElement = document.getElementById('graph_R_nd');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
+ rangeElement = document.getElementById('graph_R_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_R_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_R_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_R_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
+
+ rangeElement = document.getElementById('graph_H_nd');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
+ rangeElement = document.getElementById('graph_H_cg');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
+ rangeElement = document.getElementById('graph_H_sc');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
+ rangeElement = document.getElementById('graph_H_sl');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
+ rangeElement = document.getElementById('graph_H_damp');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
+ rangeElement = document.getElementById('graph_H_direction');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
+ rangeElement = document.getElementById('graph_H_levsep');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
+ rangeElement = document.getElementById('graph_H_nspac');
+ rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
+
+ var radioButton1 = document.getElementById("graph_physicsMethod1");
+ var radioButton2 = document.getElementById("graph_physicsMethod2");
+ var radioButton3 = document.getElementById("graph_physicsMethod3");
+ radioButton2.checked = true;
+ if (this.constants.physics.barnesHut.enabled) {
+ radioButton1.checked = true;
+ }
+ if (this.constants.hierarchicalLayout.enabled) {
+ radioButton3.checked = true;
+ }
+
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ var graph_repositionNodes = document.getElementById("graph_repositionNodes");
+ var graph_generateOptions = document.getElementById("graph_generateOptions");
+
+ graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
+ graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
+ graph_generateOptions.onclick = graphGenerateOptions.bind(this);
+ if (this.constants.smoothCurves == true) {
+ graph_toggleSmooth.style.background = "#A4FF56";
+ }
+ else {
+ graph_toggleSmooth.style.background = "#FF8532";
+ }
+
+
+ switchConfigurations.apply(this);
+
+ radioButton1.onchange = switchConfigurations.bind(this);
+ radioButton2.onchange = switchConfigurations.bind(this);
+ radioButton3.onchange = switchConfigurations.bind(this);
+ }
+ },
+
+ _overWriteGraphConstants: function (constantsVariableName, value) {
+ var nameArray = constantsVariableName.split("_");
+ if (nameArray.length == 1) {
+ this.constants[nameArray[0]] = value;
+ }
+ else if (nameArray.length == 2) {
+ this.constants[nameArray[0]][nameArray[1]] = value;
+ }
+ else if (nameArray.length == 3) {
+ this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
+ }
+ }
+};
+
+function graphToggleSmoothCurves () {
+ this.constants.smoothCurves = !this.constants.smoothCurves;
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
+ else {graph_toggleSmooth.style.background = "#FF8532";}
+
+ this._configureSmoothCurves(false);
+};
+
+function graphRepositionNodes () {
+ for (var nodeId in this.calculationNodes) {
+ if (this.calculationNodes.hasOwnProperty(nodeId)) {
+ this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
+ this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
+ }
+ }
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ this._setupHierarchicalLayout();
+ }
+ else {
+ this.repositionNodes();
+ }
+ this.moving = true;
+ this.start();
+};
+
+function graphGenerateOptions () {
+ var options = "No options are required, default values used.";
+ var optionsSpecific = [];
+ var radioButton1 = document.getElementById("graph_physicsMethod1");
+ var radioButton2 = document.getElementById("graph_physicsMethod2");
+ if (radioButton1.checked == true) {
+ if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options = "var options = {";
+ options += "physics: {barnesHut: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
+ }
+ }
+ options += '}}'
+ }
+ if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
+ if (optionsSpecific.length == 0) {options = "var options = {";}
+ else {options += ", "}
+ options += "smoothCurves: " + this.constants.smoothCurves;
+ }
+ if (options != "No options are required, default values used.") {
+ options += '};'
+ }
+ }
+ else if (radioButton2.checked == true) {
+ options = "var options = {";
+ options += "physics: {barnesHut: {enabled: false}";
+ if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options += ", repulsion: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
+ }
+ }
+ options += '}}'
+ }
+ if (optionsSpecific.length == 0) {options += "}"}
+ if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
+ options += ", smoothCurves: " + this.constants.smoothCurves;
+ }
+ options += '};'
+ }
+ else {
+ options = "var options = {";
+ if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
+ if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
+ if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
+ if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
+ if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
+ if (optionsSpecific.length != 0) {
+ options += "physics: {hierarchicalRepulsion: {";
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", ";
+ }
+ }
+ options += '}},';
+ }
+ options += 'hierarchicalLayout: {';
+ optionsSpecific = [];
+ if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
+ if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
+ if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
+ if (optionsSpecific.length != 0) {
+ for (var i = 0; i < optionsSpecific.length; i++) {
+ options += optionsSpecific[i];
+ if (i < optionsSpecific.length - 1) {
+ options += ", "
+ }
+ }
+ options += '}'
+ }
+ else {
+ options += "enabled:true}";
+ }
+ options += '};'
+ }
+
+
+ this.optionsDiv.innerHTML = options;
+
+};
+
+
+function switchConfigurations () {
+ var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
+ var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
+ var tableId = "graph_" + radioButton + "_table";
+ var table = document.getElementById(tableId);
+ table.style.display = "block";
+ for (var i = 0; i < ids.length; i++) {
+ if (ids[i] != tableId) {
+ table = document.getElementById(ids[i]);
+ table.style.display = "none";
+ }
+ }
+ this._restoreNodes();
+ if (radioButton == "R") {
+ this.constants.hierarchicalLayout.enabled = false;
+ this.constants.physics.hierarchicalRepulsion.enabled = false;
+ this.constants.physics.barnesHut.enabled = false;
+ }
+ else if (radioButton == "H") {
+ this.constants.hierarchicalLayout.enabled = true;
+ this.constants.physics.hierarchicalRepulsion.enabled = true;
+ this.constants.physics.barnesHut.enabled = false;
+ this._setupHierarchicalLayout();
+ }
+ else {
+ this.constants.hierarchicalLayout.enabled = false;
+ this.constants.physics.hierarchicalRepulsion.enabled = false;
+ this.constants.physics.barnesHut.enabled = true;
+ }
+ this._loadSelectedForceSolver();
+ var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
+ if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
+ else {graph_toggleSmooth.style.background = "#FF8532";}
+ this.moving = true;
+ this.start();
+
+}
+
+function showValueOfRange (id,map,constantsVariableName) {
+ var valueId = id + "_value";
+ var rangeValue = document.getElementById(id).value;
+
+ if (map instanceof Array) {
+ document.getElementById(valueId).value = map[parseInt(rangeValue)];
+ this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
+ }
+ else {
+ document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
+ this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
+ }
+
+ if (constantsVariableName == "hierarchicalLayout_direction" ||
+ constantsVariableName == "hierarchicalLayout_levelSeparation" ||
+ constantsVariableName == "hierarchicalLayout_nodeSpacing") {
+ this._setupHierarchicalLayout();
+ }
+ this.moving = true;
+ this.start();
+};
+
+
+
+/**
+ * Created by Alex on 2/10/14.
+ */
+
+var hierarchalRepulsionMixin = {
+
+
+ /**
+ * Calculate the forces the nodes apply on eachother based on a repulsion field.
+ * This field is linearly approximated.
+ *
+ * @private
+ */
+ _calculateNodeForces: function () {
+ var dx, dy, distance, fx, fy, combinedClusterSize,
+ repulsingForce, node1, node2, i, j;
+
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
+
+ // approximation constants
+ var b = 5;
+ var a_base = 0.5 * -b;
+
+
+ // repulsing forces between nodes
+ var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
+ var minimumDistance = nodeDistance;
+
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (i = 0; i < nodeIndices.length - 1; i++) {
+
+ node1 = nodes[nodeIndices[i]];
+ for (j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
+
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ var a = a_base / minimumDistance;
+ if (distance < 2 * minimumDistance) {
+ repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
+
+ // normalize force with
+ if (distance == 0) {
+ distance = 0.01;
+ }
+ else {
+ repulsingForce = repulsingForce / distance;
+ }
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
+ }
+ }
+ }
+ }
+};
+/**
+ * Created by Alex on 2/10/14.
+ */
+
+var barnesHutMixin = {
+
+ /**
+ * This function calculates the forces the nodes apply on eachother based on a gravitational model.
+ * The Barnes Hut method is used to speed up this N-body simulation.
+ *
+ * @private
+ */
+ _calculateNodeForces : function() {
+ if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
+ var node;
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
+ var nodeCount = nodeIndices.length;
+
+ this._formBarnesHutTree(nodes,nodeIndices);
+
+ var barnesHutTree = this.barnesHutTree;
+
+ // place the nodes one by one recursively
+ for (var i = 0; i < nodeCount; i++) {
+ node = nodes[nodeIndices[i]];
+ // starting with root is irrelevant, it never passes the BarnesHut condition
+ this._getForceContribution(barnesHutTree.root.children.NW,node);
+ this._getForceContribution(barnesHutTree.root.children.NE,node);
+ this._getForceContribution(barnesHutTree.root.children.SW,node);
+ this._getForceContribution(barnesHutTree.root.children.SE,node);
+ }
+ }
+ },
+
+
+ /**
+ * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
+ * If a region contains a single node, we check if it is not itself, then we apply the force.
+ *
+ * @param parentBranch
+ * @param node
+ * @private
+ */
+ _getForceContribution : function(parentBranch,node) {
+ // we get no force contribution from an empty region
+ if (parentBranch.childrenCount > 0) {
+ var dx,dy,distance;
+
+ // get the distance from the center of mass to the node.
+ dx = parentBranch.centerOfMass.x - node.x;
+ dy = parentBranch.centerOfMass.y - node.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ // BarnesHut condition
+ // original condition : s/d < theta = passed === d/s > 1/theta = passed
+ // calcSize = 1/s --> d * 1/s > 1/theta = passed
+ if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
+ // duplicate code to reduce function calls to speed up program
+ if (distance == 0) {
+ distance = 0.1*Math.random();
+ dx = distance;
+ }
+ var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
+ var fx = dx * gravityForce;
+ var fy = dy * gravityForce;
+ node.fx += fx;
+ node.fy += fy;
+ }
+ else {
+ // Did not pass the condition, go into children if available
+ if (parentBranch.childrenCount == 4) {
+ this._getForceContribution(parentBranch.children.NW,node);
+ this._getForceContribution(parentBranch.children.NE,node);
+ this._getForceContribution(parentBranch.children.SW,node);
+ this._getForceContribution(parentBranch.children.SE,node);
+ }
+ else { // parentBranch must have only one node, if it was empty we wouldnt be here
+ if (parentBranch.children.data.id != node.id) { // if it is not self
+ // duplicate code to reduce function calls to speed up program
+ if (distance == 0) {
+ distance = 0.5*Math.random();
+ dx = distance;
+ }
+ var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
+ var fx = dx * gravityForce;
+ var fy = dy * gravityForce;
+ node.fx += fx;
+ node.fy += fy;
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
+ *
+ * @param nodes
+ * @param nodeIndices
+ * @private
+ */
+ _formBarnesHutTree : function(nodes,nodeIndices) {
+ var node;
+ var nodeCount = nodeIndices.length;
+
+ var minX = Number.MAX_VALUE,
+ minY = Number.MAX_VALUE,
+ maxX =-Number.MAX_VALUE,
+ maxY =-Number.MAX_VALUE;
+
+ // get the range of the nodes
+ for (var i = 0; i < nodeCount; i++) {
+ var x = nodes[nodeIndices[i]].x;
+ var y = nodes[nodeIndices[i]].y;
+ if (x < minX) { minX = x; }
+ if (x > maxX) { maxX = x; }
+ if (y < minY) { minY = y; }
+ if (y > maxY) { maxY = y; }
+ }
+ // make the range a square
+ var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
+ if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
+ else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
+
+
+ var minimumTreeSize = 1e-5;
+ var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
+ var halfRootSize = 0.5 * rootSize;
+ var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
+
+ // construct the barnesHutTree
+ var barnesHutTree = {root:{
+ centerOfMass:{x:0,y:0}, // Center of Mass
+ mass:0,
+ range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
+ minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
+
+ size: rootSize,
+ calcSize: 1 / rootSize,
+ children: {data:null},
+ maxWidth: 0,
+ level: 0,
+ childrenCount: 4
+ }};
+ this._splitBranch(barnesHutTree.root);
+
+ // place the nodes one by one recursively
+ for (i = 0; i < nodeCount; i++) {
+ node = nodes[nodeIndices[i]];
+ this._placeInTree(barnesHutTree.root,node);
+ }
+
+ // make global
+ this.barnesHutTree = barnesHutTree
+ },
+
+
+ _updateBranchMass : function(parentBranch, node) {
+ var totalMass = parentBranch.mass + node.mass;
+ var totalMassInv = 1/totalMass;
+
+ parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
+ parentBranch.centerOfMass.x *= totalMassInv;
+
+ parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
+ parentBranch.centerOfMass.y *= totalMassInv;
+
+ parentBranch.mass = totalMass;
+ var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
+ parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
+
+ },
+
+
+ _placeInTree : function(parentBranch,node,skipMassUpdate) {
+ if (skipMassUpdate != true || skipMassUpdate === undefined) {
+ // update the mass of the branch.
+ this._updateBranchMass(parentBranch,node);
+ }
+
+ if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
+ if (parentBranch.children.NW.range.maxY > node.y) { // in NW
+ this._placeInRegion(parentBranch,node,"NW");
+ }
+ else { // in SW
+ this._placeInRegion(parentBranch,node,"SW");
+ }
+ }
+ else { // in NE or SE
+ if (parentBranch.children.NW.range.maxY > node.y) { // in NE
+ this._placeInRegion(parentBranch,node,"NE");
+ }
+ else { // in SE
+ this._placeInRegion(parentBranch,node,"SE");
+ }
+ }
+ },
+
+
+ _placeInRegion : function(parentBranch,node,region) {
+ switch (parentBranch.children[region].childrenCount) {
+ case 0: // place node here
+ parentBranch.children[region].children.data = node;
+ parentBranch.children[region].childrenCount = 1;
+ this._updateBranchMass(parentBranch.children[region],node);
+ break;
+ case 1: // convert into children
+ // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
+ // we move one node a pixel and we do not put it in the tree.
+ if (parentBranch.children[region].children.data.x == node.x &&
+ parentBranch.children[region].children.data.y == node.y) {
+ node.x += Math.random();
+ node.y += Math.random();
+ }
+ else {
+ this._splitBranch(parentBranch.children[region]);
+ this._placeInTree(parentBranch.children[region],node);
+ }
+ break;
+ case 4: // place in branch
+ this._placeInTree(parentBranch.children[region],node);
+ break;
+ }
+ },
+
+
+ /**
+ * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
+ * after the split is complete.
+ *
+ * @param parentBranch
+ * @private
+ */
+ _splitBranch : function(parentBranch) {
+ // if the branch is filled with a node, replace the node in the new subset.
+ var containedNode = null;
+ if (parentBranch.childrenCount == 1) {
+ containedNode = parentBranch.children.data;
+ parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
+ }
+ parentBranch.childrenCount = 4;
+ parentBranch.children.data = null;
+ this._insertRegion(parentBranch,"NW");
+ this._insertRegion(parentBranch,"NE");
+ this._insertRegion(parentBranch,"SW");
+ this._insertRegion(parentBranch,"SE");
+
+ if (containedNode != null) {
+ this._placeInTree(parentBranch,containedNode);
+ }
+ },
+
+
+ /**
+ * This function subdivides the region into four new segments.
+ * Specifically, this inserts a single new segment.
+ * It fills the children section of the parentBranch
+ *
+ * @param parentBranch
+ * @param region
+ * @param parentRange
+ * @private
+ */
+ _insertRegion : function(parentBranch, region) {
+ var minX,maxX,minY,maxY;
+ var childSize = 0.5 * parentBranch.size;
+ switch (region) {
+ case "NW":
+ minX = parentBranch.range.minX;
+ maxX = parentBranch.range.minX + childSize;
+ minY = parentBranch.range.minY;
+ maxY = parentBranch.range.minY + childSize;
+ break;
+ case "NE":
+ minX = parentBranch.range.minX + childSize;
+ maxX = parentBranch.range.maxX;
+ minY = parentBranch.range.minY;
+ maxY = parentBranch.range.minY + childSize;
+ break;
+ case "SW":
+ minX = parentBranch.range.minX;
+ maxX = parentBranch.range.minX + childSize;
+ minY = parentBranch.range.minY + childSize;
+ maxY = parentBranch.range.maxY;
+ break;
+ case "SE":
+ minX = parentBranch.range.minX + childSize;
+ maxX = parentBranch.range.maxX;
+ minY = parentBranch.range.minY + childSize;
+ maxY = parentBranch.range.maxY;
+ break;
+ }
+
+
+ parentBranch.children[region] = {
+ centerOfMass:{x:0,y:0},
+ mass:0,
+ range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
+ size: 0.5 * parentBranch.size,
+ calcSize: 2 * parentBranch.calcSize,
+ children: {data:null},
+ maxWidth: 0,
+ level: parentBranch.level+1,
+ childrenCount: 0
+ };
+ },
+
+
+ /**
+ * This function is for debugging purposed, it draws the tree.
+ *
+ * @param ctx
+ * @param color
+ * @private
+ */
+ _drawTree : function(ctx,color) {
+ if (this.barnesHutTree !== undefined) {
+
+ ctx.lineWidth = 1;
+
+ this._drawBranch(this.barnesHutTree.root,ctx,color);
+ }
+ },
+
+
+ /**
+ * This function is for debugging purposes. It draws the branches recursively.
+ *
+ * @param branch
+ * @param ctx
+ * @param color
+ * @private
+ */
+ _drawBranch : function(branch,ctx,color) {
+ if (color === undefined) {
+ color = "#FF0000";
+ }
+
+ if (branch.childrenCount == 4) {
+ this._drawBranch(branch.children.NW,ctx);
+ this._drawBranch(branch.children.NE,ctx);
+ this._drawBranch(branch.children.SE,ctx);
+ this._drawBranch(branch.children.SW,ctx);
+ }
+ ctx.strokeStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(branch.range.minX,branch.range.minY);
+ ctx.lineTo(branch.range.maxX,branch.range.minY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.maxX,branch.range.minY);
+ ctx.lineTo(branch.range.maxX,branch.range.maxY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.maxX,branch.range.maxY);
+ ctx.lineTo(branch.range.minX,branch.range.maxY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.minX,branch.range.maxY);
+ ctx.lineTo(branch.range.minX,branch.range.minY);
+ ctx.stroke();
+
+ /*
+ if (branch.mass > 0) {
+ ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
+ ctx.stroke();
+ }
+ */
+ }
+
+};
+/**
+ * Created by Alex on 2/10/14.
+ */
+
+var repulsionMixin = {
+
+
+ /**
+ * Calculate the forces the nodes apply on eachother based on a repulsion field.
+ * This field is linearly approximated.
+ *
+ * @private
+ */
+ _calculateNodeForces: function () {
+ var dx, dy, angle, distance, fx, fy, combinedClusterSize,
+ repulsingForce, node1, node2, i, j;
+
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
+
+ // approximation constants
+ var a_base = -2 / 3;
+ var b = 4 / 3;
+
+ // repulsing forces between nodes
+ var nodeDistance = this.constants.physics.repulsion.nodeDistance;
+ var minimumDistance = nodeDistance;
+
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (i = 0; i < nodeIndices.length - 1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
+ combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
+
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
+ var a = a_base / minimumDistance;
+ if (distance < 2 * minimumDistance) {
+ if (distance < 0.5 * minimumDistance) {
+ repulsingForce = 1.0;
+ }
+ else {
+ repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
+ }
+
+ // amplify the repulsion for clusters.
+ repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
+ repulsingForce = repulsingForce / distance;
+
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
+ }
+ }
+ }
+ }
+};
+var HierarchicalLayoutMixin = {
+
+
+
+ _resetLevels : function() {
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ if (node.preassignedLevel == false) {
+ node.level = -1;
+ }
+ }
+ }
+ },
+
+ /**
+ * This is the main function to layout the nodes in a hierarchical way.
+ * It checks if the node details are supplied correctly
+ *
+ * @private
+ */
+ _setupHierarchicalLayout : function() {
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
+ this.constants.hierarchicalLayout.levelSeparation *= -1;
+ }
+ else {
+ this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
+ }
+ // get the size of the largest hubs and check if the user has defined a level for a node.
+ var hubsize = 0;
+ var node, nodeId;
+ var definedLevel = false;
+ var undefinedLevel = false;
+
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.level != -1) {
+ definedLevel = true;
+ }
+ else {
+ undefinedLevel = true;
+ }
+ if (hubsize < node.edges.length) {
+ hubsize = node.edges.length;
+ }
+ }
+ }
+
+ // if the user defined some levels but not all, alert and run without hierarchical layout
+ if (undefinedLevel == true && definedLevel == true) {
+ alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
+ this.zoomExtent(true,this.constants.clustering.enabled);
+ if (!this.constants.clustering.enabled) {
+ this.start();
+ }
+ }
+ else {
+ // setup the system to use hierarchical method.
+ this._changeConstants();
+
+ // define levels if undefined by the users. Based on hubsize
+ if (undefinedLevel == true) {
+ this._determineLevels(hubsize);
+ }
+ // check the distribution of the nodes per level.
+ var distribution = this._getDistribution();
+
+ // place the nodes on the canvas. This also stablilizes the system.
+ this._placeNodesByHierarchy(distribution);
+
+ // start the simulation.
+ this.start();
+ }
+ }
+ },
+
+
+ /**
+ * This function places the nodes on the canvas based on the hierarchial distribution.
+ *
+ * @param {Object} distribution | obtained by the function this._getDistribution()
+ * @private
+ */
+ _placeNodesByHierarchy : function(distribution) {
+ var nodeId, node;
+
+ // start placing all the level 0 nodes first. Then recursively position their branches.
+ for (nodeId in distribution[0].nodes) {
+ if (distribution[0].nodes.hasOwnProperty(nodeId)) {
+ node = distribution[0].nodes[nodeId];
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ if (node.xFixed) {
+ node.x = distribution[0].minPos;
+ node.xFixed = false;
+
+ distribution[0].minPos += distribution[0].nodeSpacing;
+ }
+ }
+ else {
+ if (node.yFixed) {
+ node.y = distribution[0].minPos;
+ node.yFixed = false;
+
+ distribution[0].minPos += distribution[0].nodeSpacing;
+ }
+ }
+ this._placeBranchNodes(node.edges,node.id,distribution,node.level);
+ }
+ }
+
+ // stabilize the system after positioning. This function calls zoomExtent.
+ this._stabilize();
+ },
+
+
+ /**
+ * This function get the distribution of levels based on hubsize
+ *
+ * @returns {Object}
+ * @private
+ */
+ _getDistribution : function() {
+ var distribution = {};
+ var nodeId, node;
+
+ // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
+ // the fix of X is removed after the x value has been set.
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ node.xFixed = true;
+ node.yFixed = true;
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ }
+ else {
+ node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
+ }
+ if (!distribution.hasOwnProperty(node.level)) {
+ distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
+ }
+ distribution[node.level].amount += 1;
+ distribution[node.level].nodes[node.id] = node;
+ }
+ }
+
+ // determine the largest amount of nodes of all levels
+ var maxCount = 0;
+ for (var level in distribution) {
+ if (distribution.hasOwnProperty(level)) {
+ if (maxCount < distribution[level].amount) {
+ maxCount = distribution[level].amount;
+ }
+ }
+ }
+
+ // set the initial position and spacing of each nodes accordingly
+ for (var level in distribution) {
+ if (distribution.hasOwnProperty(level)) {
+ distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
+ distribution[level].nodeSpacing /= (distribution[level].amount + 1);
+ distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
+ }
+ }
+
+ return distribution;
+ },
+
+
+ /**
+ * this function allocates nodes in levels based on the recursive branching from the largest hubs.
+ *
+ * @param hubsize
+ * @private
+ */
+ _determineLevels : function(hubsize) {
+ var nodeId, node;
+
+ // determine hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.edges.length == hubsize) {
+ node.level = 0;
+ }
+ }
+ }
+
+ // branch from hubs
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.level == 0) {
+ this._setLevel(1,node.edges,node.id);
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Since hierarchical layout does not support:
+ * - smooth curves (based on the physics),
+ * - clustering (based on dynamic node counts)
+ *
+ * We disable both features so there will be no problems.
+ *
+ * @private
+ */
+ _changeConstants : function() {
+ this.constants.clustering.enabled = false;
+ this.constants.physics.barnesHut.enabled = false;
+ this.constants.physics.hierarchicalRepulsion.enabled = true;
+ this._loadSelectedForceSolver();
+ this.constants.smoothCurves = false;
+ this._configureSmoothCurves();
+ },
+
+
+ /**
+ * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
+ * on a X position that ensures there will be no overlap.
+ *
+ * @param edges
+ * @param parentId
+ * @param distribution
+ * @param parentLevel
+ * @private
+ */
+ _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ if (edges[i].toId == parentId) {
+ childNode = edges[i].from;
+ }
+ else {
+ childNode = edges[i].to;
+ }
+
+ // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
+ var nodeMoved = false;
+ if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
+ if (childNode.xFixed && childNode.level > parentLevel) {
+ childNode.xFixed = false;
+ childNode.x = distribution[childNode.level].minPos;
+ nodeMoved = true;
+ }
+ }
+ else {
+ if (childNode.yFixed && childNode.level > parentLevel) {
+ childNode.yFixed = false;
+ childNode.y = distribution[childNode.level].minPos;
+ nodeMoved = true;
+ }
+ }
+
+ if (nodeMoved == true) {
+ distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
+ if (childNode.edges.length > 1) {
+ this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
+ }
+ }
+ }
+ },
+
+
+ /**
+ * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
+ *
+ * @param level
+ * @param edges
+ * @param parentId
+ * @private
+ */
+ _setLevel : function(level, edges, parentId) {
+ for (var i = 0; i < edges.length; i++) {
+ var childNode = null;
+ if (edges[i].toId == parentId) {
+ childNode = edges[i].from;
+ }
+ else {
+ childNode = edges[i].to;
+ }
+ if (childNode.level == -1 || childNode.level > level) {
+ childNode.level = level;
+ if (edges.length > 1) {
+ this._setLevel(level+1, childNode.edges, childNode.id);
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Unfix nodes
+ *
+ * @private
+ */
+ _restoreNodes : function() {
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.nodes[nodeId].xFixed = false;
+ this.nodes[nodeId].yFixed = false;
+ }
+ }
+ }
+
+
+};
+/**
+ * Created by Alex on 2/4/14.
+ */
+
+var manipulationMixin = {
+
+ /**
+ * clears the toolbar div element of children
+ *
+ * @private
+ */
+ _clearManipulatorBar : function() {
+ while (this.manipulationDiv.hasChildNodes()) {
+ this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
+ }
+ },
+
+ /**
+ * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
+ * these functions to their original functionality, we saved them in this.cachedFunctions.
+ * This function restores these functions to their original function.
+ *
+ * @private
+ */
+ _restoreOverloadedFunctions : function() {
+ for (var functionName in this.cachedFunctions) {
+ if (this.cachedFunctions.hasOwnProperty(functionName)) {
+ this[functionName] = this.cachedFunctions[functionName];
+ }
+ }
+ },
+
+ /**
+ * Enable or disable edit-mode.
+ *
+ * @private
+ */
+ _toggleEditMode : function() {
+ this.editMode = !this.editMode;
+ var toolbar = document.getElementById("graph-manipulationDiv");
+ var closeDiv = document.getElementById("graph-manipulation-closeDiv");
+ var editModeDiv = document.getElementById("graph-manipulation-editMode");
+ if (this.editMode == true) {
+ toolbar.style.display="block";
+ closeDiv.style.display="block";
+ editModeDiv.style.display="none";
+ closeDiv.onclick = this._toggleEditMode.bind(this);
+ }
+ else {
+ toolbar.style.display="none";
+ closeDiv.style.display="none";
+ editModeDiv.style.display="block";
+ closeDiv.onclick = null;
+ }
+ this._createManipulatorBar()
+ },
+
+ /**
+ * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
+ *
+ * @private
+ */
+ _createManipulatorBar : function() {
+ // remove bound functions
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
+ }
+
+ // restore overloaded functions
+ this._restoreOverloadedFunctions();
+
+ // resume calculation
+ this.freezeSimulation = false;
+
+ // reset global variables
+ this.blockConnectingEdgeSelection = false;
+ this.forceAppendSelection = false;
+
+ if (this.editMode == true) {
+ while (this.manipulationDiv.hasChildNodes()) {
+ this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
+ }
+ // add the icons to the manipulator div
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ ""+this.constants.labels['add'] +"" +
+ "" +
+ "" +
+ ""+this.constants.labels['link'] +"";
+ if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['editNode'] +"";
+ }
+ if (this._selectionIsEmpty() == false) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['del'] +"";
+ }
+
+
+ // bind the icons
+ var addNodeButton = document.getElementById("graph-manipulate-addNode");
+ addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
+ var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
+ addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
+ if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
+ var editButton = document.getElementById("graph-manipulate-editNode");
+ editButton.onclick = this._editNode.bind(this);
+ }
+ if (this._selectionIsEmpty() == false) {
+ var deleteButton = document.getElementById("graph-manipulate-delete");
+ deleteButton.onclick = this._deleteSelected.bind(this);
+ }
+ var closeDiv = document.getElementById("graph-manipulation-closeDiv");
+ closeDiv.onclick = this._toggleEditMode.bind(this);
+
+ this.boundFunction = this._createManipulatorBar.bind(this);
+ this.on('select', this.boundFunction);
+ }
+ else {
+ this.editModeDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['edit'] + "";
+ var editModeButton = document.getElementById("graph-manipulate-editModeButton");
+ editModeButton.onclick = this._toggleEditMode.bind(this);
+ }
+ },
+
+
+
+ /**
+ * Create the toolbar for adding Nodes
+ *
+ * @private
+ */
+ _createAddNodeToolbar : function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
+ }
+
+ // create the toolbar contents
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['addDescription'] + "";
+
+ // bind the icon
+ var backButton = document.getElementById("graph-manipulate-back");
+ backButton.onclick = this._createManipulatorBar.bind(this);
+
+ // we use the boundFunction so we can reference it when we unbind it from the "select" event.
+ this.boundFunction = this._addNode.bind(this);
+ this.on('select', this.boundFunction);
+ },
+
+
+ /**
+ * create the toolbar to connect nodes
+ *
+ * @private
+ */
+ _createAddEdgeToolbar : function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+ this._unselectAll(true);
+ this.freezeSimulation = true;
+
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
+ }
+
+ this._unselectAll();
+ this.forceAppendSelection = false;
+ this.blockConnectingEdgeSelection = true;
+
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['linkDescription'] + "";
+
+ // bind the icon
+ var backButton = document.getElementById("graph-manipulate-back");
+ backButton.onclick = this._createManipulatorBar.bind(this);
+
+ // we use the boundFunction so we can reference it when we unbind it from the "select" event.
+ this.boundFunction = this._handleConnect.bind(this);
+ this.on('select', this.boundFunction);
+
+ // temporarily overload functions
+ this.cachedFunctions["_handleTouch"] = this._handleTouch;
+ this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
+ this._handleTouch = this._handleConnect;
+ this._handleOnRelease = this._finishConnect;
+
+ // redraw to show the unselect
+ this._redraw();
+
+ },
+
+
+ /**
+ * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
+ * to walk the user through the process.
+ *
+ * @private
+ */
+ _handleConnect : function(pointer) {
+ if (this._getSelectedNodeCount() == 0) {
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ if (node.clusterSize > 1) {
+ alert("Cannot create edges to a cluster.")
+ }
+ else {
+ this._selectObject(node,false);
+ // create a node the temporary line can look at
+ this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
+ this.sectors['support']['nodes']['targetNode'].x = node.x;
+ this.sectors['support']['nodes']['targetNode'].y = node.y;
+ this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
+ this.sectors['support']['nodes']['targetViaNode'].x = node.x;
+ this.sectors['support']['nodes']['targetViaNode'].y = node.y;
+ this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
+
+ // create a temporary edge
+ this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
+ this.edges['connectionEdge'].from = node;
+ this.edges['connectionEdge'].connected = true;
+ this.edges['connectionEdge'].smooth = true;
+ this.edges['connectionEdge'].selected = true;
+ this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
+ this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
+
+ this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
+ this._handleOnDrag = function(event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
+ this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
+ this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
+ this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
+ };
+
+ this.moving = true;
+ this.start();
+ }
+ }
+ }
+ },
+
+ _finishConnect : function(pointer) {
+ if (this._getSelectedNodeCount() == 1) {
+
+ // restore the drag function
+ this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
+ delete this.cachedFunctions["_handleOnDrag"];
+
+ // remember the edge id
+ var connectFromId = this.edges['connectionEdge'].fromId;
+
+ // remove the temporary nodes and edge
+ delete this.edges['connectionEdge'];
+ delete this.sectors['support']['nodes']['targetNode'];
+ delete this.sectors['support']['nodes']['targetViaNode'];
+
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ if (node.clusterSize > 1) {
+ alert("Cannot create edges to a cluster.")
+ }
+ else {
+ this._createEdge(connectFromId,node.id);
+ this._createManipulatorBar();
+ }
+ }
+ this._unselectAll();
+ }
+ },
+
+
+ /**
+ * Adds a node on the specified location
+ *
+ * @param {Object} pointer
+ */
+ _addNode : function() {
+ if (this._selectionIsEmpty() && this.editMode == true) {
+ var positionObject = this._pointerToPositionObject(this.pointerPosition);
+ var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
+ if (this.triggerFunctions.add) {
+ if (this.triggerFunctions.add.length == 2) {
+ var me = this;
+ this.triggerFunctions.add(defaultData, function(finalizedData) {
+ me.nodesData.add(finalizedData);
+ me._createManipulatorBar();
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels['addError']);
+ this._createManipulatorBar();
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ this.nodesData.add(defaultData);
+ this._createManipulatorBar();
+ this.moving = true;
+ this.start();
+ }
+ }
+ },
+
+
+ /**
+ * connect two nodes with a new edge.
+ *
+ * @private
+ */
+ _createEdge : function(sourceNodeId,targetNodeId) {
+ if (this.editMode == true) {
+ var defaultData = {from:sourceNodeId, to:targetNodeId};
+ if (this.triggerFunctions.connect) {
+ if (this.triggerFunctions.connect.length == 2) {
+ var me = this;
+ this.triggerFunctions.connect(defaultData, function(finalizedData) {
+ me.edgesData.add(finalizedData);
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels["linkError"]);
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ this.edgesData.add(defaultData);
+ this.moving = true;
+ this.start();
+ }
+ }
+ },
+
+
+ /**
+ * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
+ *
+ * @private
+ */
+ _editNode : function() {
+ if (this.triggerFunctions.edit && this.editMode == true) {
+ var node = this._getSelectedNode();
+ var data = {id:node.id,
+ label: node.label,
+ group: node.group,
+ shape: node.shape,
+ color: {
+ background:node.color.background,
+ border:node.color.border,
+ highlight: {
+ background:node.color.highlight.background,
+ border:node.color.highlight.border
+ }
+ }};
+ if (this.triggerFunctions.edit.length == 2) {
+ var me = this;
+ this.triggerFunctions.edit(data, function (finalizedData) {
+ me.nodesData.update(finalizedData);
+ me._createManipulatorBar();
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels["editError"]);
+ }
+ }
+ else {
+ alert(this.constants.labels["editBoundError"]);
+ }
+ },
+
+
+ /**
+ * delete everything in the selection
+ *
+ * @private
+ */
+ _deleteSelected : function() {
+ if (!this._selectionIsEmpty() && this.editMode == true) {
+ if (!this._clusterInSelection()) {
+ var selectedNodes = this.getSelectedNodes();
+ var selectedEdges = this.getSelectedEdges();
+ if (this.triggerFunctions.del) {
+ var me = this;
+ var data = {nodes: selectedNodes, edges: selectedEdges};
+ if (this.triggerFunctions.del.length = 2) {
+ this.triggerFunctions.del(data, function (finalizedData) {
+ me.edgesData.remove(finalizedData.edges);
+ me.nodesData.remove(finalizedData.nodes);
+ me._unselectAll();
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels["deleteError"])
+ }
+ }
+ else {
+ this.edgesData.remove(selectedEdges);
+ this.nodesData.remove(selectedNodes);
+ this._unselectAll();
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ alert(this.constants.labels["deleteClusterError"]);
+ }
+ }
+ }
+};
+/**
+ * Creation of the SectorMixin var.
+ *
+ * This contains all the functions the Graph object can use to employ the sector system.
+ * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
+ * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
+ *
+ * Alex de Mulder
+ * 21-01-2013
+ */
+var SectorMixin = {
+
+ /**
+ * This function is only called by the setData function of the Graph object.
+ * This loads the global references into the active sector. This initializes the sector.
+ *
+ * @private
+ */
+ _putDataInSector : function() {
+ this.sectors["active"][this._sector()].nodes = this.nodes;
+ this.sectors["active"][this._sector()].edges = this.edges;
+ this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
+ },
+
+
+ /**
+ * /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied (active) sector. If a type is defined, do the specific type
+ *
+ * @param {String} sectorId
+ * @param {String} [sectorType] | "active" or "frozen"
+ * @private
+ */
+ _switchToSector : function(sectorId, sectorType) {
+ if (sectorType === undefined || sectorType == "active") {
+ this._switchToActiveSector(sectorId);
+ }
+ else {
+ this._switchToFrozenSector(sectorId);
+ }
+ },
+
+
+ /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied active sector.
+ *
+ * @param sectorId
+ * @private
+ */
+ _switchToActiveSector : function(sectorId) {
+ this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
+ this.nodes = this.sectors["active"][sectorId]["nodes"];
+ this.edges = this.sectors["active"][sectorId]["edges"];
+ },
+
+
+ /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied active sector.
+ *
+ * @param sectorId
+ * @private
+ */
+ _switchToSupportSector : function() {
+ this.nodeIndices = this.sectors["support"]["nodeIndices"];
+ this.nodes = this.sectors["support"]["nodes"];
+ this.edges = this.sectors["support"]["edges"];
+ },
+
+
+ /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied frozen sector.
+ *
+ * @param sectorId
+ * @private
+ */
+ _switchToFrozenSector : function(sectorId) {
+ this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
+ this.nodes = this.sectors["frozen"][sectorId]["nodes"];
+ this.edges = this.sectors["frozen"][sectorId]["edges"];
+ },
+
+
+ /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the currently active sector.
+ *
+ * @private
+ */
+ _loadLatestSector : function() {
+ this._switchToSector(this._sector());
+ },
+
+
+ /**
+ * This function returns the currently active sector Id
+ *
+ * @returns {String}
+ * @private
+ */
+ _sector : function() {
+ return this.activeSector[this.activeSector.length-1];
+ },
+
+
+ /**
+ * This function returns the previously active sector Id
+ *
+ * @returns {String}
+ * @private
+ */
+ _previousSector : function() {
+ if (this.activeSector.length > 1) {
+ return this.activeSector[this.activeSector.length-2];
+ }
+ else {
+ throw new TypeError('there are not enough sectors in the this.activeSector array.');
+ }
+ },
+
+
+ /**
+ * We add the active sector at the end of the this.activeSector array
+ * This ensures it is the currently active sector returned by _sector() and it reaches the top
+ * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
+ *
+ * @param newId
+ * @private
+ */
+ _setActiveSector : function(newId) {
+ this.activeSector.push(newId);
+ },
+
+
+ /**
+ * We remove the currently active sector id from the active sector stack. This happens when
+ * we reactivate the previously active sector
+ *
+ * @private
+ */
+ _forgetLastSector : function() {
+ this.activeSector.pop();
+ },
+
+
+ /**
+ * This function creates a new active sector with the supplied newId. This newId
+ * is the expanding node id.
+ *
+ * @param {String} newId | Id of the new active sector
+ * @private
+ */
+ _createNewSector : function(newId) {
+ // create the new sector
+ this.sectors["active"][newId] = {"nodes":{},
+ "edges":{},
+ "nodeIndices":[],
+ "formationScale": this.scale,
+ "drawingNode": undefined};
+
+ // create the new sector render node. This gives visual feedback that you are in a new sector.
+ this.sectors["active"][newId]['drawingNode'] = new Node(
+ {id:newId,
+ color: {
+ background: "#eaefef",
+ border: "495c5e"
+ }
+ },{},{},this.constants);
+ this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
+ },
+
+
+ /**
+ * This function removes the currently active sector. This is called when we create a new
+ * active sector.
+ *
+ * @param {String} sectorId | Id of the active sector that will be removed
+ * @private
+ */
+ _deleteActiveSector : function(sectorId) {
+ delete this.sectors["active"][sectorId];
+ },
+
+
+ /**
+ * This function removes the currently active sector. This is called when we reactivate
+ * the previously active sector.
+ *
+ * @param {String} sectorId | Id of the active sector that will be removed
+ * @private
+ */
+ _deleteFrozenSector : function(sectorId) {
+ delete this.sectors["frozen"][sectorId];
+ },
+
+
+ /**
+ * Freezing an active sector means moving it from the "active" object to the "frozen" object.
+ * We copy the references, then delete the active entree.
+ *
+ * @param sectorId
+ * @private
+ */
+ _freezeSector : function(sectorId) {
+ // we move the set references from the active to the frozen stack.
+ this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
+
+ // we have moved the sector data into the frozen set, we now remove it from the active set
+ this._deleteActiveSector(sectorId);
+ },
+
+
+ /**
+ * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
+ * object to the "active" object.
+ *
+ * @param sectorId
+ * @private
+ */
+ _activateSector : function(sectorId) {
+ // we move the set references from the frozen to the active stack.
+ this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
+
+ // we have moved the sector data into the active set, we now remove it from the frozen stack
+ this._deleteFrozenSector(sectorId);
+ },
+
+
+ /**
+ * This function merges the data from the currently active sector with a frozen sector. This is used
+ * in the process of reverting back to the previously active sector.
+ * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
+ * upon the creation of a new active sector.
+ *
+ * @param sectorId
+ * @private
+ */
+ _mergeThisWithFrozen : function(sectorId) {
+ // copy all nodes
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
+ }
+ }
+
+ // copy all edges (if not fully clustered, else there are no edges)
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
+ }
+ }
+
+ // merge the nodeIndices
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
+ }
+ },
+
+
+ /**
+ * This clusters the sector to one cluster. It was a single cluster before this process started so
+ * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
+ *
+ * @private
+ */
+ _collapseThisToSingleCluster : function() {
+ this.clusterToFit(1,false);
+ },
+
+
+ /**
+ * We create a new active sector from the node that we want to open.
+ *
+ * @param node
+ * @private
+ */
+ _addSector : function(node) {
+ // this is the currently active sector
+ var sector = this._sector();
+
+// // this should allow me to select nodes from a frozen set.
+// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
+// console.log("the node is part of the active sector");
+// }
+// else {
+// console.log("I dont know what the fuck happened!!");
+// }
+
+ // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
+ delete this.nodes[node.id];
+
+ var unqiueIdentifier = util.randomUUID();
+
+ // we fully freeze the currently active sector
+ this._freezeSector(sector);
+
+ // we create a new active sector. This sector has the Id of the node to ensure uniqueness
+ this._createNewSector(unqiueIdentifier);
+
+ // we add the active sector to the sectors array to be able to revert these steps later on
+ this._setActiveSector(unqiueIdentifier);
+
+ // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
+ this._switchToSector(this._sector());
+
+ // finally we add the node we removed from our previous active sector to the new active sector
+ this.nodes[node.id] = node;
+ },
+
+
+ /**
+ * We close the sector that is currently open and revert back to the one before.
+ * If the active sector is the "default" sector, nothing happens.
+ *
+ * @private
+ */
+ _collapseSector : function() {
+ // the currently active sector
+ var sector = this._sector();
+
+ // we cannot collapse the default sector
+ if (sector != "default") {
+ if ((this.nodeIndices.length == 1) ||
+ (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
+ (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
+ var previousSector = this._previousSector();
+
+ // we collapse the sector back to a single cluster
+ this._collapseThisToSingleCluster();
+
+ // we move the remaining nodes, edges and nodeIndices to the previous sector.
+ // This previous sector is the one we will reactivate
+ this._mergeThisWithFrozen(previousSector);
+
+ // the previously active (frozen) sector now has all the data from the currently active sector.
+ // we can now delete the active sector.
+ this._deleteActiveSector(sector);
+
+ // we activate the previously active (and currently frozen) sector.
+ this._activateSector(previousSector);
+
+ // we load the references from the newly active sector into the global references
+ this._switchToSector(previousSector);
+
+ // we forget the previously active sector because we reverted to the one before
+ this._forgetLastSector();
+
+ // finally, we update the node index list.
+ this._updateNodeIndexList();
+
+ // we refresh the list with calulation nodes and calculation node indices.
+ this._updateCalculationNodes();
+ }
+ }
+ },
+
+
+ /**
+ * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
+ *
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we dont pass the function itself because then the "this" is the window object
+ * | instead of the Graph object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
+ */
+ _doInAllActiveSectors : function(runFunction,argument) {
+ if (argument === undefined) {
+ for (var sector in this.sectors["active"]) {
+ if (this.sectors["active"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToActiveSector(sector);
+ this[runFunction]();
+ }
+ }
+ }
+ else {
+ for (var sector in this.sectors["active"]) {
+ if (this.sectors["active"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToActiveSector(sector);
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (args.length > 1) {
+ this[runFunction](args[0],args[1]);
+ }
+ else {
+ this[runFunction](argument);
+ }
+ }
+ }
+ }
+ // we revert the global references back to our active sector
+ this._loadLatestSector();
+ },
+
+
+ /**
+ * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
+ *
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we dont pass the function itself because then the "this" is the window object
+ * | instead of the Graph object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
+ */
+ _doInSupportSector : function(runFunction,argument) {
+ if (argument === undefined) {
+ this._switchToSupportSector();
+ this[runFunction]();
+ }
+ else {
+ this._switchToSupportSector();
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (args.length > 1) {
+ this[runFunction](args[0],args[1]);
+ }
+ else {
+ this[runFunction](argument);
+ }
+ }
+ // we revert the global references back to our active sector
+ this._loadLatestSector();
+ },
+
+
+ /**
+ * This runs a function in all frozen sectors. This is used in the _redraw().
+ *
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we don't pass the function itself because then the "this" is the window object
+ * | instead of the Graph object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
+ */
+ _doInAllFrozenSectors : function(runFunction,argument) {
+ if (argument === undefined) {
+ for (var sector in this.sectors["frozen"]) {
+ if (this.sectors["frozen"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToFrozenSector(sector);
+ this[runFunction]();
+ }
+ }
+ }
+ else {
+ for (var sector in this.sectors["frozen"]) {
+ if (this.sectors["frozen"].hasOwnProperty(sector)) {
+ // switch the global references to those of this sector
+ this._switchToFrozenSector(sector);
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (args.length > 1) {
+ this[runFunction](args[0],args[1]);
+ }
+ else {
+ this[runFunction](argument);
+ }
+ }
+ }
+ }
+ this._loadLatestSector();
+ },
+
+
+ /**
+ * This runs a function in all sectors. This is used in the _redraw().
+ *
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we don't pass the function itself because then the "this" is the window object
+ * | instead of the Graph object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
+ */
+ _doInAllSectors : function(runFunction,argument) {
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (argument === undefined) {
+ this._doInAllActiveSectors(runFunction);
+ this._doInAllFrozenSectors(runFunction);
+ }
+ else {
+ if (args.length > 1) {
+ this._doInAllActiveSectors(runFunction,args[0],args[1]);
+ this._doInAllFrozenSectors(runFunction,args[0],args[1]);
+ }
+ else {
+ this._doInAllActiveSectors(runFunction,argument);
+ this._doInAllFrozenSectors(runFunction,argument);
+ }
+ }
+ },
+
+
+ /**
+ * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
+ * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
+ *
+ * @private
+ */
+ _clearNodeIndexList : function() {
+ var sector = this._sector();
+ this.sectors["active"][sector]["nodeIndices"] = [];
+ this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
+ },
+
+
+ /**
+ * Draw the encompassing sector node
+ *
+ * @param ctx
+ * @param sectorType
+ * @private
+ */
+ _drawSectorNodes : function(ctx,sectorType) {
+ var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
+ for (var sector in this.sectors[sectorType]) {
+ if (this.sectors[sectorType].hasOwnProperty(sector)) {
+ if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
+
+ this._switchToSector(sector,sectorType);
+
+ minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ node.resize(ctx);
+ if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
+ if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
+ if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
+ if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
+ }
+ }
+ node = this.sectors[sectorType][sector]["drawingNode"];
+ node.x = 0.5 * (maxX + minX);
+ node.y = 0.5 * (maxY + minY);
+ node.width = 2 * (node.x - minX);
+ node.height = 2 * (node.y - minY);
+ node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
+ node.setScale(this.scale);
+ node._drawCircle(ctx);
+ }
+ }
+ }
+ },
+
+ _drawAllSectorNodes : function(ctx) {
+ this._drawSectorNodes(ctx,"frozen");
+ this._drawSectorNodes(ctx,"active");
+ this._loadLatestSector();
+ }
+};
+/**
+ * Creation of the ClusterMixin var.
+ *
+ * This contains all the functions the Graph object can use to employ clustering
+ *
+ * Alex de Mulder
+ * 21-01-2013
+ */
+var ClusterMixin = {
+
+ /**
+ * This is only called in the constructor of the graph object
+ *
+ */
+ startWithClustering : function() {
+ // cluster if the data set is big
+ this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
+
+ // updates the lables after clustering
+ this.updateLabels();
+
+ // this is called here because if clusterin is disabled, the start and stabilize are called in
+ // the setData function.
+ if (this.stabilize) {
+ this._stabilize();
+ }
+ this.start();
+ },
+
+ /**
+ * This function clusters until the initialMaxNodes has been reached
+ *
+ * @param {Number} maxNumberOfNodes
+ * @param {Boolean} reposition
+ */
+ clusterToFit : function(maxNumberOfNodes, reposition) {
+ var numberOfNodes = this.nodeIndices.length;
+
+ var maxLevels = 50;
+ var level = 0;
+
+ // we first cluster the hubs, then we pull in the outliers, repeat
+ while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
+ if (level % 3 == 0) {
+ this.forceAggregateHubs(true);
+ this.normalizeClusterLevels();
+ }
+ else {
+ this.increaseClusterLevel(); // this also includes a cluster normalization
+ }
+
+ numberOfNodes = this.nodeIndices.length;
+ level += 1;
+ }
+
+ // after the clustering we reposition the nodes to reduce the initial chaos
+ if (level > 0 && reposition == true) {
+ this.repositionNodes();
+ }
+ this._updateCalculationNodes();
+ },
+
+ /**
+ * This function can be called to open up a specific cluster. It is only called by
+ * It will unpack the cluster back one level.
+ *
+ * @param node | Node object: cluster to open.
+ */
+ openCluster : function(node) {
+ var isMovingBeforeClustering = this.moving;
+ if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
+ !(this._sector() == "default" && this.nodeIndices.length == 1)) {
+ // this loads a new sector, loads the nodes and edges and nodeIndices of it.
+ this._addSector(node);
+ var level = 0;
+
+ // we decluster until we reach a decent number of nodes
+ while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
+ this.decreaseClusterLevel();
+ level += 1;
+ }
+
+ }
+ else {
+ this._expandClusterNode(node,false,true);
+
+ // update the index list, dynamic edges and labels
+ this._updateNodeIndexList();
+ this._updateDynamicEdges();
+ this._updateCalculationNodes();
+ this.updateLabels();
+ }
+
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
+ if (this.moving != isMovingBeforeClustering) {
+ this.start();
+ }
+ },
+
+
+ /**
+ * This calls the updateClustes with default arguments
+ */
+ updateClustersDefault : function() {
+ if (this.constants.clustering.enabled == true) {
+ this.updateClusters(0,false,false);
+ }
+ },
+
+
+ /**
+ * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
+ * be clustered with their connected node. This can be repeated as many times as needed.
+ * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
+ */
+ increaseClusterLevel : function() {
+ this.updateClusters(-1,false,true);
+ },
+
+
+ /**
+ * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
+ * be unpacked if they are a cluster. This can be repeated as many times as needed.
+ * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
+ */
+ decreaseClusterLevel : function() {
+ this.updateClusters(1,false,true);
+ },
+
+
+ /**
+ * This is the main clustering function. It clusters and declusters on zoom or forced
+ * This function clusters on zoom, it can be called with a predefined zoom direction
+ * If out, check if we can form clusters, if in, check if we can open clusters.
+ * This function is only called from _zoom()
+ *
+ * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
+ * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
+ * @param {Boolean} force | enabled or disable forcing
+ *
+ */
+ updateClusters : function(zoomDirection,recursive,force,doNotStart) {
+ var isMovingBeforeClustering = this.moving;
+ var amountOfNodes = this.nodeIndices.length;
+
+ // on zoom out collapse the sector if the scale is at the level the sector was made
+ if (this.previousScale > this.scale && zoomDirection == 0) {
+ this._collapseSector();
+ }
+
+ // check if we zoom in or out
+ if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
+ // forming clusters when forced pulls outliers in. When not forced, the edge length of the
+ // outer nodes determines if it is being clustered
+ this._formClusters(force);
+ }
+ else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
+ if (force == true) {
+ // _openClusters checks for each node if the formationScale of the cluster is smaller than
+ // the current scale and if so, declusters. When forced, all clusters are reduced by one step
+ this._openClusters(recursive,force);
+ }
+ else {
+ // if a cluster takes up a set percentage of the active window
+ this._openClustersBySize();
+ }
+ }
+ this._updateNodeIndexList();
+
+ // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
+ if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
+ this._aggregateHubs(force);
+ this._updateNodeIndexList();
+ }
+
+ // we now reduce chains.
+ if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
+ this.handleChains();
+ this._updateNodeIndexList();
+ }
+
+ this.previousScale = this.scale;
+
+ // rest of the update the index list, dynamic edges and labels
+ this._updateDynamicEdges();
+ this.updateLabels();
+
+ // if a cluster was formed, we increase the clusterSession
+ if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
+ this.clusterSession += 1;
+ // if clusters have been made, we normalize the cluster level
+ this.normalizeClusterLevels();
+ }
+
+ if (doNotStart == false || doNotStart === undefined) {
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
+ if (this.moving != isMovingBeforeClustering) {
+ this.start();
+ }
+ }
+
+ this._updateCalculationNodes();
+ },
+
+ /**
+ * This function handles the chains. It is called on every updateClusters().
+ */
+ handleChains : function() {
+ // after clustering we check how many chains there are
+ var chainPercentage = this._getChainFraction();
+ if (chainPercentage > this.constants.clustering.chainThreshold) {
+ this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
+
+ }
+ },
+
+ /**
+ * this functions starts clustering by hubs
+ * The minimum hub threshold is set globally
+ *
+ * @private
+ */
+ _aggregateHubs : function(force) {
+ this._getHubSize();
+ this._formClustersByHub(force,false);
+ },
+
+
+ /**
+ * This function is fired by keypress. It forces hubs to form.
+ *
+ */
+ forceAggregateHubs : function(doNotStart) {
+ var isMovingBeforeClustering = this.moving;
+ var amountOfNodes = this.nodeIndices.length;
+
+ this._aggregateHubs(true);
+
+ // update the index list, dynamic edges and labels
+ this._updateNodeIndexList();
+ this._updateDynamicEdges();
+ this.updateLabels();
+
+ // if a cluster was formed, we increase the clusterSession
+ if (this.nodeIndices.length != amountOfNodes) {
+ this.clusterSession += 1;
+ }
+
+ if (doNotStart == false || doNotStart === undefined) {
+ // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
+ if (this.moving != isMovingBeforeClustering) {
+ this.start();
+ }
+ }
+ },
+
+ /**
+ * If a cluster takes up more than a set percentage of the screen, open the cluster
+ *
+ * @private
+ */
+ _openClustersBySize : function() {
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ if (node.inView() == true) {
+ if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
+ (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
+ this.openCluster(node);
+ }
+ }
+ }
+ }
+ },
+
+
+ /**
+ * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
+ * has to be opened based on the current zoom level.
+ *
+ * @private
+ */
+ _openClusters : function(recursive,force) {
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ this._expandClusterNode(node,recursive,force);
+ this._updateCalculationNodes();
+ }
+ },
+
+ /**
+ * This function checks if a node has to be opened. This is done by checking the zoom level.
+ * If the node contains child nodes, this function is recursively called on the child nodes as well.
+ * This recursive behaviour is optional and can be set by the recursive argument.
+ *
+ * @param {Node} parentNode | to check for cluster and expand
+ * @param {Boolean} recursive | enabled or disable recursive calling
+ * @param {Boolean} force | enabled or disable forcing
+ * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
+ * @private
+ */
+ _expandClusterNode : function(parentNode, recursive, force, openAll) {
+ // first check if node is a cluster
+ if (parentNode.clusterSize > 1) {
+ // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
+ if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
+ openAll = true;
+ }
+ recursive = openAll ? true : recursive;
+
+ // if the last child has been added on a smaller scale than current scale decluster
+ if (parentNode.formationScale < this.scale || force == true) {
+ // we will check if any of the contained child nodes should be removed from the cluster
+ for (var containedNodeId in parentNode.containedNodes) {
+ if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
+ var childNode = parentNode.containedNodes[containedNodeId];
+
+ // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
+ // the largest cluster is the one that comes from outside
+ if (force == true) {
+ if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
+ || openAll) {
+ this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
+ }
+ }
+ else {
+ if (this._nodeInActiveArea(parentNode)) {
+ this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * ONLY CALLED FROM _expandClusterNode
+ *
+ * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
+ * the child node from the parent contained_node object and put it back into the global nodes object.
+ * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
+ *
+ * @param {Node} parentNode | the parent node
+ * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
+ * @param {Boolean} recursive | This will also check if the child needs to be expanded.
+ * With force and recursive both true, the entire cluster is unpacked
+ * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
+ * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
+ * @private
+ */
+ _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
+ var childNode = parentNode.containedNodes[containedNodeId];
+
+ // if child node has been added on smaller scale than current, kick out
+ if (childNode.formationScale < this.scale || force == true) {
+ // unselect all selected items
+ this._unselectAll();
+
+ // put the child node back in the global nodes object
+ this.nodes[containedNodeId] = childNode;
+
+ // release the contained edges from this childNode back into the global edges
+ this._releaseContainedEdges(parentNode,childNode);
+
+ // reconnect rerouted edges to the childNode
+ this._connectEdgeBackToChild(parentNode,childNode);
+
+ // validate all edges in dynamicEdges
+ this._validateEdges(parentNode);
+
+ // undo the changes from the clustering operation on the parent node
+ parentNode.mass -= childNode.mass;
+ parentNode.clusterSize -= childNode.clusterSize;
+ parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
+ parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
+
+ // place the child node near the parent, not at the exact same location to avoid chaos in the system
+ childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
+ childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
+
+ // remove node from the list
+ delete parentNode.containedNodes[containedNodeId];
+
+ // check if there are other childs with this clusterSession in the parent.
+ var othersPresent = false;
+ for (var childNodeId in parentNode.containedNodes) {
+ if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
+ if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
+ othersPresent = true;
+ break;
+ }
+ }
+ }
+ // if there are no others, remove the cluster session from the list
+ if (othersPresent == false) {
+ parentNode.clusterSessions.pop();
+ }
+
+ this._repositionBezierNodes(childNode);
+// this._repositionBezierNodes(parentNode);
+
+ // remove the clusterSession from the child node
+ childNode.clusterSession = 0;
+
+ // recalculate the size of the node on the next time the node is rendered
+ parentNode.clearSizeCache();
+
+ // restart the simulation to reorganise all nodes
+ this.moving = true;
+ }
+
+ // check if a further expansion step is possible if recursivity is enabled
+ if (recursive == true) {
+ this._expandClusterNode(childNode,recursive,force,openAll);
+ }
+ },
+
+
+ /**
+ * position the bezier nodes at the center of the edges
+ *
+ * @param node
+ * @private
+ */
+ _repositionBezierNodes : function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ node.dynamicEdges[i].positionBezierNode();
+ }
+ },
+
+
+ /**
+ * This function checks if any nodes at the end of their trees have edges below a threshold length
+ * This function is called only from updateClusters()
+ * forceLevelCollapse ignores the length of the edge and collapses one level
+ * This means that a node with only one edge will be clustered with its connected node
+ *
+ * @private
+ * @param {Boolean} force
+ */
+ _formClusters : function(force) {
+ if (force == false) {
+ this._formClustersByZoom();
+ }
+ else {
+ this._forceClustersByZoom();
+ }
+ },
+
+
+ /**
+ * This function handles the clustering by zooming out, this is based on a minimum edge distance
+ *
+ * @private
+ */
+ _formClustersByZoom : function() {
+ var dx,dy,length,
+ minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
+
+ // check if any edges are shorter than minLength and start the clustering
+ // the clustering favours the node with the larger mass
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ var edge = this.edges[edgeId];
+ if (edge.connected) {
+ if (edge.toId != edge.fromId) {
+ dx = (edge.to.x - edge.from.x);
+ dy = (edge.to.y - edge.from.y);
+ length = Math.sqrt(dx * dx + dy * dy);
+
+
+ if (length < minLength) {
+ // first check which node is larger
+ var parentNode = edge.from;
+ var childNode = edge.to;
+ if (edge.to.mass > edge.from.mass) {
+ parentNode = edge.to;
+ childNode = edge.from;
+ }
+
+ if (childNode.dynamicEdgesLength == 1) {
+ this._addToCluster(parentNode,childNode,false);
+ }
+ else if (parentNode.dynamicEdgesLength == 1) {
+ this._addToCluster(childNode,parentNode,false);
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * This function forces the graph to cluster all nodes with only one connecting edge to their
+ * connected node.
+ *
+ * @private
+ */
+ _forceClustersByZoom : function() {
+ for (var nodeId in this.nodes) {
+ // another node could have absorbed this child.
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var childNode = this.nodes[nodeId];
+
+ // the edges can be swallowed by another decrease
+ if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
+ var edge = childNode.dynamicEdges[0];
+ var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
+
+ // group to the largest node
+ if (childNode.id != parentNode.id) {
+ if (parentNode.mass > childNode.mass) {
+ this._addToCluster(parentNode,childNode,true);
+ }
+ else {
+ this._addToCluster(childNode,parentNode,true);
+ }
+ }
+ }
+ }
+ }
+ },
+
+
+ /**
+ * To keep the nodes of roughly equal size we normalize the cluster levels.
+ * This function clusters a node to its smallest connected neighbour.
+ *
+ * @param node
+ * @private
+ */
+ _clusterToSmallestNeighbour : function(node) {
+ var smallestNeighbour = -1;
+ var smallestNeighbourNode = null;
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ if (node.dynamicEdges[i] !== undefined) {
+ var neighbour = null;
+ if (node.dynamicEdges[i].fromId != node.id) {
+ neighbour = node.dynamicEdges[i].from;
+ }
+ else if (node.dynamicEdges[i].toId != node.id) {
+ neighbour = node.dynamicEdges[i].to;
+ }
+
+
+ if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
+ smallestNeighbour = neighbour.clusterSessions.length;
+ smallestNeighbourNode = neighbour;
+ }
+ }
+ }
+
+ if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
+ this._addToCluster(neighbour, node, true);
+ }
+ },
+
+
+ /**
+ * This function forms clusters from hubs, it loops over all nodes
+ *
+ * @param {Boolean} force | Disregard zoom level
+ * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
+ * @private
+ */
+ _formClustersByHub : function(force, onlyEqual) {
+ // we loop over all nodes in the list
+ for (var nodeId in this.nodes) {
+ // we check if it is still available since it can be used by the clustering in this loop
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
+ }
+ }
+ },
+
+ /**
+ * This function forms a cluster from a specific preselected hub node
+ *
+ * @param {Node} hubNode | the node we will cluster as a hub
+ * @param {Boolean} force | Disregard zoom level
+ * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
+ * @param {Number} [absorptionSizeOffset] |
+ * @private
+ */
+ _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
+ if (absorptionSizeOffset === undefined) {
+ absorptionSizeOffset = 0;
+ }
+ // we decide if the node is a hub
+ if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
+ (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
+ // initialize variables
+ var dx,dy,length;
+ var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
+ var allowCluster = false;
+
+ // we create a list of edges because the dynamicEdges change over the course of this loop
+ var edgesIdarray = [];
+ var amountOfInitialEdges = hubNode.dynamicEdges.length;
+ for (var j = 0; j < amountOfInitialEdges; j++) {
+ edgesIdarray.push(hubNode.dynamicEdges[j].id);
+ }
+
+ // if the hub clustering is not forces, we check if one of the edges connected
+ // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
+ if (force == false) {
+ allowCluster = false;
+ for (j = 0; j < amountOfInitialEdges; j++) {
+ var edge = this.edges[edgesIdarray[j]];
+ if (edge !== undefined) {
+ if (edge.connected) {
+ if (edge.toId != edge.fromId) {
+ dx = (edge.to.x - edge.from.x);
+ dy = (edge.to.y - edge.from.y);
+ length = Math.sqrt(dx * dx + dy * dy);
+
+ if (length < minLength) {
+ allowCluster = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // start the clustering if allowed
+ if ((!force && allowCluster) || force) {
+ // we loop over all edges INITIALLY connected to this hub
+ for (j = 0; j < amountOfInitialEdges; j++) {
+ edge = this.edges[edgesIdarray[j]];
+ // the edge can be clustered by this function in a previous loop
+ if (edge !== undefined) {
+ var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
+ // we do not want hubs to merge with other hubs nor do we want to cluster itself.
+ if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
+ (childNode.id != hubNode.id)) {
+ this._addToCluster(hubNode,childNode,force);
+ }
+ }
+ }
+ }
+ }
+ },
+
+
+
+ /**
+ * This function adds the child node to the parent node, creating a cluster if it is not already.
+ *
+ * @param {Node} parentNode | this is the node that will house the child node
+ * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
+ * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
+ * @private
+ */
+ _addToCluster : function(parentNode, childNode, force) {
+ // join child node in the parent node
+ parentNode.containedNodes[childNode.id] = childNode;
+
+ // manage all the edges connected to the child and parent nodes
+ for (var i = 0; i < childNode.dynamicEdges.length; i++) {
+ var edge = childNode.dynamicEdges[i];
+ if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
+ this._addToContainedEdges(parentNode,childNode,edge);
+ }
+ else {
+ this._connectEdgeToCluster(parentNode,childNode,edge);
+ }
+ }
+ // a contained node has no dynamic edges.
+ childNode.dynamicEdges = [];
+
+ // remove circular edges from clusters
+ this._containCircularEdgesFromNode(parentNode,childNode);
+
+
+ // remove the childNode from the global nodes object
+ delete this.nodes[childNode.id];
+
+ // update the properties of the child and parent
+ var massBefore = parentNode.mass;
+ childNode.clusterSession = this.clusterSession;
+ parentNode.mass += childNode.mass;
+ parentNode.clusterSize += childNode.clusterSize;
+ parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
+
+ // keep track of the clustersessions so we can open the cluster up as it has been formed.
+ if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
+ parentNode.clusterSessions.push(this.clusterSession);
+ }
+
+ // forced clusters only open from screen size and double tap
+ if (force == true) {
+ // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
+ parentNode.formationScale = 0;
+ }
+ else {
+ parentNode.formationScale = this.scale; // The latest child has been added on this scale
+ }
+
+ // recalculate the size of the node on the next time the node is rendered
+ parentNode.clearSizeCache();
+
+ // set the pop-out scale for the childnode
+ parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
+
+ // nullify the movement velocity of the child, this is to avoid hectic behaviour
+ childNode.clearVelocity();
+
+ // the mass has altered, preservation of energy dictates the velocity to be updated
+ parentNode.updateVelocity(massBefore);
+
+ // restart the simulation to reorganise all nodes
+ this.moving = true;
+ },
+
+
+ /**
+ * This function will apply the changes made to the remainingEdges during the formation of the clusters.
+ * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
+ * It has to be called if a level is collapsed. It is called by _formClusters().
+ * @private
+ */
+ _updateDynamicEdges : function() {
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ node.dynamicEdgesLength = node.dynamicEdges.length;
+
+ // this corrects for multiple edges pointing at the same other node
+ var correction = 0;
+ if (node.dynamicEdgesLength > 1) {
+ for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
+ var edgeToId = node.dynamicEdges[j].toId;
+ var edgeFromId = node.dynamicEdges[j].fromId;
+ for (var k = j+1; k < node.dynamicEdgesLength; k++) {
+ if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
+ (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
+ correction += 1;
+ }
+ }
+ }
+ }
+ node.dynamicEdgesLength -= correction;
+ }
+ },
+
+
+ /**
+ * This adds an edge from the childNode to the contained edges of the parent node
+ *
+ * @param parentNode | Node object
+ * @param childNode | Node object
+ * @param edge | Edge object
+ * @private
+ */
+ _addToContainedEdges : function(parentNode, childNode, edge) {
+ // create an array object if it does not yet exist for this childNode
+ if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
+ parentNode.containedEdges[childNode.id] = []
+ }
+ // add this edge to the list
+ parentNode.containedEdges[childNode.id].push(edge);
+
+ // remove the edge from the global edges object
+ delete this.edges[edge.id];
+
+ // remove the edge from the parent object
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
+ if (parentNode.dynamicEdges[i].id == edge.id) {
+ parentNode.dynamicEdges.splice(i,1);
+ break;
+ }
+ }
+ },
+
+ /**
+ * This function connects an edge that was connected to a child node to the parent node.
+ * It keeps track of which nodes it has been connected to with the originalId array.
+ *
+ * @param {Node} parentNode | Node object
+ * @param {Node} childNode | Node object
+ * @param {Edge} edge | Edge object
+ * @private
+ */
+ _connectEdgeToCluster : function(parentNode, childNode, edge) {
+ // handle circular edges
+ if (edge.toId == edge.fromId) {
+ this._addToContainedEdges(parentNode, childNode, edge);
+ }
+ else {
+ if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
+ edge.originalToId.push(childNode.id);
+ edge.to = parentNode;
+ edge.toId = parentNode.id;
+ }
+ else { // edge connected to other node with the "from" side
+
+ edge.originalFromId.push(childNode.id);
+ edge.from = parentNode;
+ edge.fromId = parentNode.id;
+ }
+
+ this._addToReroutedEdges(parentNode,childNode,edge);
+ }
+ },
+
+
+ /**
+ * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
+ * these edges inside of the cluster.
+ *
+ * @param parentNode
+ * @param childNode
+ * @private
+ */
+ _containCircularEdgesFromNode : function(parentNode, childNode) {
+ // manage all the edges connected to the child and parent nodes
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
+ var edge = parentNode.dynamicEdges[i];
+ // handle circular edges
+ if (edge.toId == edge.fromId) {
+ this._addToContainedEdges(parentNode, childNode, edge);
+ }
+ }
+ },
+
+
+ /**
+ * This adds an edge from the childNode to the rerouted edges of the parent node
+ *
+ * @param parentNode | Node object
+ * @param childNode | Node object
+ * @param edge | Edge object
+ * @private
+ */
+ _addToReroutedEdges : function(parentNode, childNode, edge) {
+ // create an array object if it does not yet exist for this childNode
+ // we store the edge in the rerouted edges so we can restore it when the cluster pops open
+ if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
+ parentNode.reroutedEdges[childNode.id] = [];
+ }
+ parentNode.reroutedEdges[childNode.id].push(edge);
+
+ // this edge becomes part of the dynamicEdges of the cluster node
+ parentNode.dynamicEdges.push(edge);
+ },
+
+
+
+ /**
+ * This function connects an edge that was connected to a cluster node back to the child node.
+ *
+ * @param parentNode | Node object
+ * @param childNode | Node object
+ * @private
+ */
+ _connectEdgeBackToChild : function(parentNode, childNode) {
+ if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
+ for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
+ var edge = parentNode.reroutedEdges[childNode.id][i];
+ if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
+ edge.originalFromId.pop();
+ edge.fromId = childNode.id;
+ edge.from = childNode;
+ }
+ else {
+ edge.originalToId.pop();
+ edge.toId = childNode.id;
+ edge.to = childNode;
+ }
+
+ // append this edge to the list of edges connecting to the childnode
+ childNode.dynamicEdges.push(edge);
+
+ // remove the edge from the parent object
+ for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
+ if (parentNode.dynamicEdges[j].id == edge.id) {
+ parentNode.dynamicEdges.splice(j,1);
+ break;
+ }
+ }
+ }
+ // remove the entry from the rerouted edges
+ delete parentNode.reroutedEdges[childNode.id];
+ }
+ },
+
+
+ /**
+ * When loops are clustered, an edge can be both in the rerouted array and the contained array.
+ * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
+ * parentNode
+ *
+ * @param parentNode | Node object
+ * @private
+ */
+ _validateEdges : function(parentNode) {
+ for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
+ var edge = parentNode.dynamicEdges[i];
+ if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
+ parentNode.dynamicEdges.splice(i,1);
+ }
+ }
+ },
+
+
+ /**
+ * This function released the contained edges back into the global domain and puts them back into the
+ * dynamic edges of both parent and child.
+ *
+ * @param {Node} parentNode |
+ * @param {Node} childNode |
+ * @private
+ */
+ _releaseContainedEdges : function(parentNode, childNode) {
+ for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
+ var edge = parentNode.containedEdges[childNode.id][i];
+
+ // put the edge back in the global edges object
+ this.edges[edge.id] = edge;
+
+ // put the edge back in the dynamic edges of the child and parent
+ childNode.dynamicEdges.push(edge);
+ parentNode.dynamicEdges.push(edge);
+ }
+ // remove the entry from the contained edges
+ delete parentNode.containedEdges[childNode.id];
+
+ },
+
+
+
+
+ // ------------------- UTILITY FUNCTIONS ---------------------------- //
+
+
+ /**
+ * This updates the node labels for all nodes (for debugging purposes)
+ */
+ updateLabels : function() {
+ var nodeId;
+ // update node labels
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ if (node.clusterSize > 1) {
+ node.label = "[".concat(String(node.clusterSize),"]");
+ }
+ }
+ }
+
+ // update node labels
+ for (nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (node.clusterSize == 1) {
+ if (node.originalLabel !== undefined) {
+ node.label = node.originalLabel;
+ }
+ else {
+ node.label = String(node.id);
+ }
+ }
+ }
+ }
+
+// /* Debug Override */
+// for (nodeId in this.nodes) {
+// if (this.nodes.hasOwnProperty(nodeId)) {
+// node = this.nodes[nodeId];
+// node.label = String(node.level);
+// }
+// }
+
+ },
+
+
+ /**
+ * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
+ * if the rest of the nodes are already a few cluster levels in.
+ * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
+ * clustered enough to the clusterToSmallestNeighbours function.
+ */
+ normalizeClusterLevels : function() {
+ var maxLevel = 0;
+ var minLevel = 1e9;
+ var clusterLevel = 0;
+
+ // we loop over all nodes in the list
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ clusterLevel = this.nodes[nodeId].clusterSessions.length;
+ if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
+ if (minLevel > clusterLevel) {minLevel = clusterLevel;}
+ }
+ }
+
+ if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
+ var amountOfNodes = this.nodeIndices.length;
+ var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
+ // we loop over all nodes in the list
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
+ this._clusterToSmallestNeighbour(this.nodes[nodeId]);
+ }
+ }
+ }
+ this._updateNodeIndexList();
+ this._updateDynamicEdges();
+ // if a cluster was formed, we increase the clusterSession
+ if (this.nodeIndices.length != amountOfNodes) {
+ this.clusterSession += 1;
+ }
+ }
+ },
+
+
+
+ /**
+ * This function determines if the cluster we want to decluster is in the active area
+ * this means around the zoom center
+ *
+ * @param {Node} node
+ * @returns {boolean}
+ * @private
+ */
+ _nodeInActiveArea : function(node) {
+ return (
+ Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
+ &&
+ Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
+ )
+ },
+
+
+ /**
+ * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
+ * It puts large clusters away from the center and randomizes the order.
+ *
+ */
+ repositionNodes : function() {
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ if ((node.xFixed == false || node.yFixed == false)) {
+ var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
+ var angle = 2 * Math.PI * Math.random();
+ if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
+ if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
+ this._repositionBezierNodes(node);
+ }
+ }
+ },
+
+
+ /**
+ * We determine how many connections denote an important hub.
+ * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
+ *
+ * @private
+ */
+ _getHubSize : function() {
+ var average = 0;
+ var averageSquared = 0;
+ var hubCounter = 0;
+ var largestHub = 0;
+
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+
+ var node = this.nodes[this.nodeIndices[i]];
+ if (node.dynamicEdgesLength > largestHub) {
+ largestHub = node.dynamicEdgesLength;
+ }
+ average += node.dynamicEdgesLength;
+ averageSquared += Math.pow(node.dynamicEdgesLength,2);
+ hubCounter += 1;
+ }
+ average = average / hubCounter;
+ averageSquared = averageSquared / hubCounter;
+
+ var variance = averageSquared - Math.pow(average,2);
+
+ var standardDeviation = Math.sqrt(variance);
+
+ this.hubThreshold = Math.floor(average + 2*standardDeviation);
+
+ // always have at least one to cluster
+ if (this.hubThreshold > largestHub) {
+ this.hubThreshold = largestHub;
+ }
+
+ // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
+ // console.log("hubThreshold:",this.hubThreshold);
+ },
+
+
+ /**
+ * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
+ * with this amount we can cluster specifically on these chains.
+ *
+ * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
+ * @private
+ */
+ _reduceAmountOfChains : function(fraction) {
+ this.hubThreshold = 2;
+ var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
+ if (reduceAmount > 0) {
+ this._formClusterFromHub(this.nodes[nodeId],true,true,1);
+ reduceAmount -= 1;
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
+ * with this amount we can cluster specifically on these chains.
+ *
+ * @private
+ */
+ _getChainFraction : function() {
+ var chains = 0;
+ var total = 0;
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
+ chains += 1;
+ }
+ total += 1;
+ }
+ }
+ return chains/total;
+ }
+
+};
+
+
+var SelectionMixin = {
+
+ /**
+ * This function can be called from the _doInAllSectors function
+ *
+ * @param object
+ * @param overlappingNodes
+ * @private
+ */
+ _getNodesOverlappingWith : function(object, overlappingNodes) {
+ var nodes = this.nodes;
+ for (var nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ if (nodes[nodeId].isOverlappingWith(object)) {
+ overlappingNodes.push(nodeId);
+ }
+ }
+ }
+ },
+
+ /**
+ * retrieve all nodes overlapping with given object
+ * @param {Object} object An object with parameters left, top, right, bottom
+ * @return {Number[]} An array with id's of the overlapping nodes
+ * @private
+ */
+ _getAllNodesOverlappingWith : function (object) {
+ var overlappingNodes = [];
+ this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
+ return overlappingNodes;
+ },
+
+
+ /**
+ * Return a position object in canvasspace from a single point in screenspace
+ *
+ * @param pointer
+ * @returns {{left: number, top: number, right: number, bottom: number}}
+ * @private
+ */
+ _pointerToPositionObject : function(pointer) {
+ var x = this._canvasToX(pointer.x);
+ var y = this._canvasToY(pointer.y);
+
+ return {left: x,
+ top: y,
+ right: x,
+ bottom: y};
+ },
+
+
+ /**
+ * Get the top node at the a specific point (like a click)
+ *
+ * @param {{x: Number, y: Number}} pointer
+ * @return {Node | null} node
+ * @private
+ */
+ _getNodeAt : function (pointer) {
+ // we first check if this is an navigation controls element
+ var positionObject = this._pointerToPositionObject(pointer);
+ var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
+
+ // if there are overlapping nodes, select the last one, this is the
+ // one which is drawn on top of the others
+ if (overlappingNodes.length > 0) {
+ return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
+ }
+ else {
+ return null;
+ }
+ },
+
+
+ /**
+ * retrieve all edges overlapping with given object, selector is around center
+ * @param {Object} object An object with parameters left, top, right, bottom
+ * @return {Number[]} An array with id's of the overlapping nodes
+ * @private
+ */
+ _getEdgesOverlappingWith : function (object, overlappingEdges) {
+ var edges = this.edges;
+ for (var edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ if (edges[edgeId].isOverlappingWith(object)) {
+ overlappingEdges.push(edgeId);
+ }
+ }
+ }
+ },
+
+
+ /**
+ * retrieve all nodes overlapping with given object
+ * @param {Object} object An object with parameters left, top, right, bottom
+ * @return {Number[]} An array with id's of the overlapping nodes
+ * @private
+ */
+ _getAllEdgesOverlappingWith : function (object) {
+ var overlappingEdges = [];
+ this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
+ return overlappingEdges;
+ },
+
+ /**
+ * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
+ * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
+ *
+ * @param pointer
+ * @returns {null}
+ * @private
+ */
+ _getEdgeAt : function(pointer) {
+ var positionObject = this._pointerToPositionObject(pointer);
+ var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
+
+ if (overlappingEdges.length > 0) {
+ return this.edges[overlappingEdges[overlappingEdges.length - 1]];
+ }
+ else {
+ return null;
+ }
+ },
+
+
+ /**
+ * Add object to the selection array.
+ *
+ * @param obj
+ * @private
+ */
+ _addToSelection : function(obj) {
+ if (obj instanceof Node) {
+ this.selectionObj.nodes[obj.id] = obj;
+ }
+ else {
+ this.selectionObj.edges[obj.id] = obj;
+ }
+
+ },
+
+
+ /**
+ * Remove a single option from selection.
+ *
+ * @param {Object} obj
+ * @private
+ */
+ _removeFromSelection : function(obj) {
+ if (obj instanceof Node) {
+ delete this.selectionObj.nodes[obj.id];
+ }
+ else {
+ delete this.selectionObj.edges[obj.id];
+ }
+ },
+
+
+ /**
+ * Unselect all. The selectionObj is useful for this.
+ *
+ * @param {Boolean} [doNotTrigger] | ignore trigger
+ * @private
+ */
+ _unselectAll : function(doNotTrigger) {
+ if (doNotTrigger === undefined) {
+ doNotTrigger = false;
+ }
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ this.selectionObj.nodes[nodeId].unselect();
+ }
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ this.selectionObj.edges[edgeId].unselect();;
+ }
+ }
+
+ this.selectionObj = {nodes:{},edges:{}};
+
+ if (doNotTrigger == false) {
+ this.emit('select', this.getSelection());
+ }
+ },
+
+ /**
+ * Unselect all clusters. The selectionObj is useful for this.
+ *
+ * @param {Boolean} [doNotTrigger] | ignore trigger
+ * @private
+ */
+ _unselectClusters : function(doNotTrigger) {
+ if (doNotTrigger === undefined) {
+ doNotTrigger = false;
+ }
+
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
+ this.selectionObj.nodes[nodeId].unselect();
+ this._removeFromSelection(this.selectionObj.nodes[nodeId]);
+ }
+ }
+ }
+
+ if (doNotTrigger == false) {
+ this.emit('select', this.getSelection());
+ }
+ },
+
+
+ /**
+ * return the number of selected nodes
+ *
+ * @returns {number}
+ * @private
+ */
+ _getSelectedNodeCount : function() {
+ var count = 0;
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ count += 1;
+ }
+ }
+ return count;
+ },
+
+ /**
+ * return the number of selected nodes
+ *
+ * @returns {number}
+ * @private
+ */
+ _getSelectedNode : function() {
+ for (var nodeId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ return this.selectionObj.nodes[nodeId];
+ }
+ }
+ return null;
+ },
+
+
+ /**
+ * return the number of selected edges
+ *
+ * @returns {number}
+ * @private
+ */
+ _getSelectedEdgeCount : function() {
+ var count = 0;
+ for (var edgeId in this.selectionObj.edges) {
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ count += 1;
+ }
+ }
+ return count;
+ },
+
+
+ /**
+ * return the number of selected objects.
+ *
+ * @returns {number}
+ * @private
+ */
+ _getSelectedObjectCount : function() {
+ var count = 0;
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ count += 1;
+ }
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ count += 1;
+ }
+ }
+ return count;
+ },
+
+ /**
+ * Check if anything is selected
+ *
+ * @returns {boolean}
+ * @private
+ */
+ _selectionIsEmpty : function() {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ return false;
+ }
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+
+ /**
+ * check if one of the selected nodes is a cluster.
+ *
+ * @returns {boolean}
+ * @private
+ */
+ _clusterInSelection : function() {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ /**
+ * select the edges connected to the node that is being selected
+ *
+ * @param {Node} node
+ * @private
+ */
+ _selectConnectedEdges : function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.select();
+ this._addToSelection(edge);
+ }
+ },
+
+
+ /**
+ * unselect the edges connected to the node that is being selected
+ *
+ * @param {Node} node
+ * @private
+ */
+ _unselectConnectedEdges : function(node) {
+ for (var i = 0; i < node.dynamicEdges.length; i++) {
+ var edge = node.dynamicEdges[i];
+ edge.unselect();
+ this._removeFromSelection(edge);
+ }
+ },
+
+
+
+ /**
+ * This is called when someone clicks on a node. either select or deselect it.
+ * If there is an existing selection and we don't want to append to it, clear the existing selection
+ *
+ * @param {Node || Edge} object
+ * @param {Boolean} append
+ * @param {Boolean} [doNotTrigger] | ignore trigger
+ * @private
+ */
+ _selectObject : function(object, append, doNotTrigger) {
+ if (doNotTrigger === undefined) {
+ doNotTrigger = false;
+ }
+
+ if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
+ this._unselectAll(true);
+ }
+
+ if (object.selected == false) {
+ object.select();
+ this._addToSelection(object);
+ if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
+ this._selectConnectedEdges(object);
+ }
+ }
+ else {
+ object.unselect();
+ this._removeFromSelection(object);
+ }
+ if (doNotTrigger == false) {
+ this.emit('select', this.getSelection());
+ }
+ },
+
+
+ /**
+ * handles the selection part of the touch, only for navigation controls elements;
+ * Touch is triggered before tap, also before hold. Hold triggers after a while.
+ * This is the most responsive solution
+ *
+ * @param {Object} pointer
+ * @private
+ */
+ _handleTouch : function(pointer) {
+
+ },
+
+
+ /**
+ * handles the selection part of the tap;
+ *
+ * @param {Object} pointer
+ * @private
+ */
+ _handleTap : function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ this._selectObject(node,false);
+ }
+ else {
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge,false);
+ }
+ else {
+ this._unselectAll();
+ }
+ }
+ this.emit("click", this.getSelection());
+ this._redraw();
+ },
+
+
+ /**
+ * handles the selection part of the double tap and opens a cluster if needed
+ *
+ * @param {Object} pointer
+ * @private
+ */
+ _handleDoubleTap : function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null && node !== undefined) {
+ // we reset the areaCenter here so the opening of the node will occur
+ this.areaCenter = {"x" : this._canvasToX(pointer.x),
+ "y" : this._canvasToY(pointer.y)};
+ this.openCluster(node);
+ }
+ this.emit("doubleClick", this.getSelection());
+ },
+
+
+ /**
+ * Handle the onHold selection part
+ *
+ * @param pointer
+ * @private
+ */
+ _handleOnHold : function(pointer) {
+ var node = this._getNodeAt(pointer);
+ if (node != null) {
+ this._selectObject(node,true);
+ }
+ else {
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge,true);
+ }
+ }
+ this._redraw();
+ },
+
+
+ /**
+ * handle the onRelease event. These functions are here for the navigation controls module.
+ *
+ * @private
+ */
+ _handleOnRelease : function(pointer) {
+
+ },
+
+
+
+ /**
+ *
+ * retrieve the currently selected objects
+ * @return {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+ getSelection : function() {
+ var nodeIds = this.getSelectedNodes();
+ var edgeIds = this.getSelectedEdges();
+ return {nodes:nodeIds, edges:edgeIds};
+ },
+
+ /**
+ *
+ * retrieve the currently selected nodes
+ * @return {String} selection An array with the ids of the
+ * selected nodes.
+ */
+ getSelectedNodes : function() {
+ var idArray = [];
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ idArray.push(nodeId);
+ }
+ }
+ return idArray
+ },
+
+ /**
+ *
+ * retrieve the currently selected edges
+ * @return {Array} selection An array with the ids of the
+ * selected nodes.
+ */
+ getSelectedEdges : function() {
+ var idArray = [];
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ idArray.push(edgeId);
+ }
+ }
+ return idArray;
+ },
+
+
+ /**
+ * select zero or more nodes
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+ setSelection : function(selection) {
+ var i, iMax, id;
+
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
+
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var node = this.nodes[id];
+ if (!node) {
+ throw new RangeError('Node with id "' + id + '" not found');
+ }
+ this._selectObject(node,true,true);
+ }
+ this.redraw();
+ },
+
+
+ /**
+ * Validate the selection: remove ids of nodes which no longer exist
+ * @private
+ */
+ _updateSelection : function () {
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (!this.nodes.hasOwnProperty(nodeId)) {
+ delete this.selectionObj.nodes[nodeId];
+ }
+ }
+ }
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ if (!this.edges.hasOwnProperty(edgeId)) {
+ delete this.selectionObj.edges[edgeId];
+ }
+ }
+ }
+ }
+};
+
+
+
+/**
+ * Created by Alex on 1/22/14.
+ */
+
+var NavigationMixin = {
+
+ _cleanNavigation : function() {
+ // clean up previosu navigation items
+ var wrapper = document.getElementById('graph-navigation_wrapper');
+ if (wrapper != null) {
+ this.containerElement.removeChild(wrapper);
+ }
+ document.onmouseup = null;
+ },
+
+ /**
+ * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
+ * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
+ * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
+ * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
+ *
+ * @private
+ */
+ _loadNavigationElements : function() {
+ this._cleanNavigation();
+
+ this.navigationDivs = {};
+ var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
+ var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
+
+ this.navigationDivs['wrapper'] = document.createElement('div');
+ this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
+ this.navigationDivs['wrapper'].style.position = "absolute";
+ this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
+ this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
+ this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
+
+ for (var i = 0; i < navigationDivs.length; i++) {
+ this.navigationDivs[navigationDivs[i]] = document.createElement('div');
+ this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
+ this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
+ this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
+ this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
+ }
+
+ document.onmouseup = this._stopMovement.bind(this);
+ },
+
+ /**
+ * this stops all movement induced by the navigation buttons
+ *
+ * @private
+ */
+ _stopMovement : function() {
+ this._xStopMoving();
+ this._yStopMoving();
+ this._stopZoom();
+ },
+
+
+ /**
+ * stops the actions performed by page up and down etc.
+ *
+ * @param event
+ * @private
+ */
+ _preventDefault : function(event) {
+ if (event !== undefined) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+ }
+ },
+
+
+ /**
+ * move the screen up
+ * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
+ * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
+ * To avoid this behaviour, we do the translation in the start loop.
+ *
+ * @private
+ */
+ _moveUp : function(event) {
+ this.yIncrement = this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['up'].className += " active";
+ }
+ },
+
+
+ /**
+ * move the screen down
+ * @private
+ */
+ _moveDown : function(event) {
+ this.yIncrement = -this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['down'].className += " active";
+ }
+ },
+
+
+ /**
+ * move the screen left
+ * @private
+ */
+ _moveLeft : function(event) {
+ this.xIncrement = this.constants.keyboard.speed.x;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['left'].className += " active";
+ }
+ },
+
+
+ /**
+ * move the screen right
+ * @private
+ */
+ _moveRight : function(event) {
+ this.xIncrement = -this.constants.keyboard.speed.y;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['right'].className += " active";
+ }
+ },
+
+
+ /**
+ * Zoom in, using the same method as the movement.
+ * @private
+ */
+ _zoomIn : function(event) {
+ this.zoomIncrement = this.constants.keyboard.speed.zoom;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomIn'].className += " active";
+ }
+ },
+
+
+ /**
+ * Zoom out
+ * @private
+ */
+ _zoomOut : function() {
+ this.zoomIncrement = -this.constants.keyboard.speed.zoom;
+ this.start(); // if there is no node movement, the calculation wont be done
+ this._preventDefault(event);
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomOut'].className += " active";
+ }
+ },
+
+
+ /**
+ * Stop zooming and unhighlight the zoom controls
+ * @private
+ */
+ _stopZoom : function() {
+ this.zoomIncrement = 0;
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
+ this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
+ }
+ },
+
+
+ /**
+ * Stop moving in the Y direction and unHighlight the up and down
+ * @private
+ */
+ _yStopMoving : function() {
+ this.yIncrement = 0;
+ if (this.navigationDivs) {
+ this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
+ this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
+ }
+ },
+
+
+ /**
+ * Stop moving in the X direction and unHighlight left and right.
+ * @private
+ */
+ _xStopMoving : function() {
+ this.xIncrement = 0;
+ if (this.navigationDivs) {
+ this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
+ this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
+ }
+ }
+
+
+};
+
+/**
+ * Created by Alex on 2/10/14.
+ */
+
+
+var graphMixinLoaders = {
+
+ /**
+ * Load a mixin into the graph object
+ *
+ * @param {Object} sourceVariable | this object has to contain functions.
+ * @private
+ */
+ _loadMixin: function (sourceVariable) {
+ for (var mixinFunction in sourceVariable) {
+ if (sourceVariable.hasOwnProperty(mixinFunction)) {
+ Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
+ }
+ }
+ },
+
+
+ /**
+ * removes a mixin from the graph object.
+ *
+ * @param {Object} sourceVariable | this object has to contain functions.
+ * @private
+ */
+ _clearMixin: function (sourceVariable) {
+ for (var mixinFunction in sourceVariable) {
+ if (sourceVariable.hasOwnProperty(mixinFunction)) {
+ Graph.prototype[mixinFunction] = undefined;
+ }
+ }
+ },
+
+
+ /**
+ * Mixin the physics system and initialize the parameters required.
+ *
+ * @private
+ */
+ _loadPhysicsSystem: function () {
+ this._loadMixin(physicsMixin);
+ this._loadSelectedForceSolver();
+ if (this.constants.configurePhysics == true) {
+ this._loadPhysicsConfiguration();
+ }
+ },
+
+
+ /**
+ * Mixin the cluster system and initialize the parameters required.
+ *
+ * @private
+ */
+ _loadClusterSystem: function () {
+ this.clusterSession = 0;
+ this.hubThreshold = 5;
+ this._loadMixin(ClusterMixin);
+ },
+
+
+ /**
+ * Mixin the sector system and initialize the parameters required
+ *
+ * @private
+ */
+ _loadSectorSystem: function () {
+ this.sectors = { },
+ this.activeSector = ["default"];
+ this.sectors["active"] = { },
+ this.sectors["active"]["default"] = {"nodes": {},
+ "edges": {},
+ "nodeIndices": [],
+ "formationScale": 1.0,
+ "drawingNode": undefined };
+ this.sectors["frozen"] = {},
+ this.sectors["support"] = {"nodes": {},
+ "edges": {},
+ "nodeIndices": [],
+ "formationScale": 1.0,
+ "drawingNode": undefined };
+
+ this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
+
+ this._loadMixin(SectorMixin);
+ },
+
+
+ /**
+ * Mixin the selection system and initialize the parameters required
+ *
+ * @private
+ */
+ _loadSelectionSystem: function () {
+ this.selectionObj = {nodes: {}, edges: {}};
+
+ this._loadMixin(SelectionMixin);
+ },
+
+
+ /**
+ * Mixin the navigationUI (User Interface) system and initialize the parameters required
+ *
+ * @private
+ */
+ _loadManipulationSystem: function () {
+ // reset global variables -- these are used by the selection of nodes and edges.
+ this.blockConnectingEdgeSelection = false;
+ this.forceAppendSelection = false
+
+ if (this.constants.dataManipulation.enabled == true) {
+ // load the manipulator HTML elements. All styling done in css.
+ if (this.manipulationDiv === undefined) {
+ this.manipulationDiv = document.createElement('div');
+ this.manipulationDiv.className = 'graph-manipulationDiv';
+ this.manipulationDiv.id = 'graph-manipulationDiv';
+ if (this.editMode == true) {
+ this.manipulationDiv.style.display = "block";
+ }
+ else {
+ this.manipulationDiv.style.display = "none";
+ }
+ this.containerElement.insertBefore(this.manipulationDiv, this.frame);
+ }
+
+ if (this.editModeDiv === undefined) {
+ this.editModeDiv = document.createElement('div');
+ this.editModeDiv.className = 'graph-manipulation-editMode';
+ this.editModeDiv.id = 'graph-manipulation-editMode';
+ if (this.editMode == true) {
+ this.editModeDiv.style.display = "none";
+ }
+ else {
+ this.editModeDiv.style.display = "block";
+ }
+ this.containerElement.insertBefore(this.editModeDiv, this.frame);
+ }
+
+ if (this.closeDiv === undefined) {
+ this.closeDiv = document.createElement('div');
+ this.closeDiv.className = 'graph-manipulation-closeDiv';
+ this.closeDiv.id = 'graph-manipulation-closeDiv';
+ this.closeDiv.style.display = this.manipulationDiv.style.display;
+ this.containerElement.insertBefore(this.closeDiv, this.frame);
+ }
+
+ // load the manipulation functions
+ this._loadMixin(manipulationMixin);
+
+ // create the manipulator toolbar
+ this._createManipulatorBar();
+ }
+ else {
+ if (this.manipulationDiv !== undefined) {
+ // removes all the bindings and overloads
+ this._createManipulatorBar();
+ // remove the manipulation divs
+ this.containerElement.removeChild(this.manipulationDiv);
+ this.containerElement.removeChild(this.editModeDiv);
+ this.containerElement.removeChild(this.closeDiv);
+
+ this.manipulationDiv = undefined;
+ this.editModeDiv = undefined;
+ this.closeDiv = undefined;
+ // remove the mixin functions
+ this._clearMixin(manipulationMixin);
+ }
+ }
+ },
+
+
+ /**
+ * Mixin the navigation (User Interface) system and initialize the parameters required
+ *
+ * @private
+ */
+ _loadNavigationControls: function () {
+ this._loadMixin(NavigationMixin);
+
+ // the clean function removes the button divs, this is done to remove the bindings.
+ this._cleanNavigation();
+ if (this.constants.navigation.enabled == true) {
+ this._loadNavigationElements();
+ }
+ },
+
+
+ /**
+ * Mixin the hierarchical layout system.
+ *
+ * @private
+ */
+ _loadHierarchySystem: function () {
+ this._loadMixin(HierarchicalLayoutMixin);
+ }
+
+};
+
+/**
+ * @constructor Graph
+ * Create a graph visualization, displaying nodes and edges.
+ *
+ * @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) {
+
+ this._initializeMixinLoaders();
+
+ // create variables and set default values
+ this.containerElement = container;
+ this.width = '100%';
+ this.height = '100%';
+
+ // render and calculation settings
+ this.renderRefreshRate = 60; // hz (fps)
+ this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
+ this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
+ this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
+ this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
+
+ this.stabilize = true; // stabilize before displaying the graph
+ this.selectable = true;
+ this.initializing = true;
+
+ // these functions are triggered when the dataset is edited
+ this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
+
+ // set constant values
+ this.constants = {
+ nodes: {
+ radiusMin: 5,
+ radiusMax: 20,
+ radius: 5,
+ shape: 'ellipse',
+ image: undefined,
+ widthMin: 16, // px
+ widthMax: 64, // px
+ fixed: false,
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ level: -1,
+ 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: {
+ color:'#848484',
+ highlight:'#848484'
+ },
+ fontColor: '#343434',
+ fontSize: 14, // px
+ fontFace: 'arial',
+ fontFill: 'white',
+ dash: {
+ length: 10,
+ gap: 5,
+ altLength: undefined
+ }
+ },
+ configurePhysics:false,
+ physics: {
+ barnesHut: {
+ enabled: true,
+ theta: 1 / 0.6, // inverted to save time during calculation
+ gravitationalConstant: -2000,
+ centralGravity: 0.3,
+ springLength: 95,
+ springConstant: 0.04,
+ damping: 0.09
+ },
+ repulsion: {
+ centralGravity: 0.1,
+ springLength: 200,
+ springConstant: 0.05,
+ nodeDistance: 100,
+ damping: 0.09
+ },
+ hierarchicalRepulsion: {
+ enabled: false,
+ centralGravity: 0.0,
+ springLength: 100,
+ springConstant: 0.01,
+ nodeDistance: 60,
+ damping: 0.09
+ },
+ damping: null,
+ centralGravity: null,
+ springLength: null,
+ springConstant: null
+ },
+ clustering: { // Per Node in Cluster = PNiC
+ enabled: false, // (Boolean) | global on/off switch for clustering.
+ initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
+ clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes
+ reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
+ chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
+ clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
+ sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
+ screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
+ fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
+ maxFontSize: 1000,
+ forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
+ distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
+ edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
+ nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
+ height: 1, // (px PNiC) | growth of the height per node in cluster.
+ radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
+ maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
+ activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
+ clusterLevelDifference: 2
+ },
+ navigation: {
+ enabled: false
+ },
+ keyboard: {
+ enabled: false,
+ speed: {x: 10, y: 10, zoom: 0.02}
+ },
+ dataManipulation: {
+ enabled: false,
+ initiallyVisible: false
+ },
+ hierarchicalLayout: {
+ enabled:false,
+ levelSeparation: 150,
+ nodeSpacing: 100,
+ direction: "UD" // UD, DU, LR, RL
+ },
+ freezeForStabilization: false,
+ smoothCurves: true,
+ maxVelocity: 10,
+ minVelocity: 0.1, // px/s
+ stabilizationIterations: 1000, // maximum number of iteration to stabilize
+ labels:{
+ add:"Add Node",
+ edit:"Edit",
+ link:"Add Link",
+ del:"Delete selected",
+ editNode:"Edit Node",
+ back:"Back",
+ addDescription:"Click in an empty space to place a new node.",
+ linkDescription:"Click on a node and drag the edge to another node to connect them.",
+ addError:"The function for add does not support two arguments (data,callback).",
+ linkError:"The function for connect does not support two arguments (data,callback).",
+ editError:"The function for edit does not support two arguments (data, callback).",
+ editBoundError:"No edit function has been bound to this button.",
+ deleteError:"The function for delete does not support two arguments (data, callback).",
+ deleteClusterError:"Clusters cannot be deleted."
+ },
+ tooltip: {
+ delay: 300,
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ color: {
+ border: '#666',
+ background: '#FFFFC6'
+ }
+ }
+ };
+ this.editMode = this.constants.dataManipulation.initiallyVisible;
+
+ // Node variables
+ var graph = this;
+ this.groups = new Groups(); // object with groups
+ this.images = new Images(); // object with images
+ this.images.setOnloadCallback(function () {
+ graph._redraw();
+ });
+
+ // keyboard navigation variables
+ this.xIncrement = 0;
+ this.yIncrement = 0;
+ this.zoomIncrement = 0;
+
+ // loading all the mixins:
+ // load the force calculation functions, grouped under the physics system.
+ this._loadPhysicsSystem();
+ // create a frame and canvas
+ this._create();
+ // load the sector system. (mandatory, fully integrated with Graph)
+ this._loadSectorSystem();
+ // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
+ this._loadClusterSystem();
+ // load the selection system. (mandatory, required by Graph)
+ this._loadSelectionSystem();
+ // load the selection system. (mandatory, required by Graph)
+ this._loadHierarchySystem();
+
+ // apply options
+ this.setOptions(options);
+
+ // other vars
+ this.freezeSimulation = false;// freeze the simulation
+ this.cachedFunctions = {};
+
+ // containers for nodes and edges
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
+ this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
+ this.nodes = {}; // object with Node objects
+ this.edges = {}; // object with Edge objects
+
+ // position and scale variables and objects
+ this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
+ this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
+ this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
+ this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
+ this.scale = 1; // defining the global scale variable in the constructor
+ this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
+
+ // datasets or dataviews
+ this.nodesData = null; // A DataSet or DataView
+ this.edgesData = null; // A DataSet or DataView
+
+ // create event listeners used to subscribe on the DataSets of the nodes and edges
+ this.nodesListeners = {
+ 'add': function (event, params) {
+ graph._addNodes(params.items);
+ graph.start();
+ },
+ 'update': function (event, params) {
+ graph._updateNodes(params.items);
+ graph.start();
+ },
+ 'remove': function (event, params) {
+ graph._removeNodes(params.items);
+ graph.start();
+ }
+ };
+ this.edgesListeners = {
+ 'add': function (event, params) {
+ graph._addEdges(params.items);
+ graph.start();
+ },
+ 'update': function (event, params) {
+ graph._updateEdges(params.items);
+ graph.start();
+ },
+ 'remove': function (event, params) {
+ graph._removeEdges(params.items);
+ graph.start();
+ }
+ };
+
+ // properties for the animation
+ this.moving = true;
+ this.timer = undefined; // Scheduling function. Is definded in this.start();
+
+ // load data (the disable start variable will be the same as the enabled clustering)
+ this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
+
+ // hierarchical layout
+ this.initializing = false;
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ this._setupHierarchicalLayout();
+ }
+ else {
+ // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
+ if (this.stabilize == false) {
+ this.zoomExtent(true,this.constants.clustering.enabled);
+ }
+ }
+
+ // if clustering is disabled, the simulation will have started in the setData function
+ if (this.constants.clustering.enabled) {
+ this.startWithClustering();
+ }
+}
+
+// Extend Graph with an Emitter mixin
+Emitter(Graph.prototype);
+
+/**
+ * Get the script path where the vis.js library is located
+ *
+ * @returns {string | null} path Path or null when not found. Path does not
+ * end with a slash.
+ * @private
+ */
+Graph.prototype._getScriptPath = function() {
+ var scripts = document.getElementsByTagName( 'script' );
+
+ // find script named vis.js or vis.min.js
+ for (var i = 0; i < scripts.length; i++) {
+ var src = scripts[i].src;
+ var match = src && /\/?vis(.min)?\.js$/.exec(src);
+ if (match) {
+ // return path without the script name
+ return src.substring(0, src.length - match[0].length);
+ }
+ }
+
+ return null;
+};
+
+
+/**
+ * Find the center position of the graph
+ * @private
+ */
+Graph.prototype._getRange = function() {
+ var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (minX > (node.x)) {minX = node.x;}
+ if (maxX < (node.x)) {maxX = node.x;}
+ if (minY > (node.y)) {minY = node.y;}
+ if (maxY < (node.y)) {maxY = node.y;}
+ }
+ }
+ if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
+ minY = 0, maxY = 0, minX = 0, maxX = 0;
+ }
+ return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
+};
+
+
+/**
+ * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
+ * @returns {{x: number, y: number}}
+ * @private
+ */
+Graph.prototype._findCenter = function(range) {
+ return {x: (0.5 * (range.maxX + range.minX)),
+ y: (0.5 * (range.maxY + range.minY))};
+};
+
+
+/**
+ * center the graph
+ *
+ * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
+ */
+Graph.prototype._centerGraph = function(range) {
+ var center = this._findCenter(range);
+
+ center.x *= this.scale;
+ center.y *= this.scale;
+ center.x -= 0.5 * this.frame.canvas.clientWidth;
+ center.y -= 0.5 * this.frame.canvas.clientHeight;
+
+ this._setTranslation(-center.x,-center.y); // set at 0,0
+};
+
+
+/**
+ * This function zooms out to fit all data on screen based on amount of nodes
+ *
+ * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
+ */
+Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
+ if (initialZoom === undefined) {
+ initialZoom = false;
+ }
+ if (disableStart === undefined) {
+ disableStart = false;
+ }
+
+ var range = this._getRange();
+ var zoomLevel;
+
+ if (initialZoom == true) {
+ var numberOfNodes = this.nodeIndices.length;
+ if (this.constants.smoothCurves == true) {
+ if (this.constants.clustering.enabled == true &&
+ numberOfNodes >= this.constants.clustering.initialMaxNodes) {
+ zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ else {
+ zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ }
+ else {
+ if (this.constants.clustering.enabled == true &&
+ numberOfNodes >= this.constants.clustering.initialMaxNodes) {
+ zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ else {
+ zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ }
+
+ // correct for larger canvasses.
+ var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
+ zoomLevel *= factor;
+ }
+ else {
+ var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
+ var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
+
+ var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
+ var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
+
+ zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
+ }
+
+ if (zoomLevel > 1.0) {
+ zoomLevel = 1.0;
+ }
+
+
+ this._setScale(zoomLevel);
+ this._centerGraph(range);
+ if (disableStart == false) {
+ this.moving = true;
+ this.start();
+ }
+};
+
+
+/**
+ * Update the this.nodeIndices with the most recent node index list
+ * @private
+ */
+Graph.prototype._updateNodeIndexList = function() {
+ this._clearNodeIndexList();
+ for (var idx in this.nodes) {
+ if (this.nodes.hasOwnProperty(idx)) {
+ this.nodeIndices.push(idx);
+ }
+ }
+};
+
+
+/**
+ * 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
+ * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
+ */
+Graph.prototype.setData = function(data, disableStart) {
+ if (disableStart === undefined) {
+ disableStart = false;
+ }
+
+ 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.');
+ }
+
+ // set options
+ this.setOptions(data && data.options);
+
+ // 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._putDataInSector();
+
+ if (!disableStart) {
+ // find a stable position or start animating to a stable position
+ if (this.stabilize) {
+ this._stabilize();
+ }
+ this.start();
+ }
+};
+
+/**
+ * Set options
+ * @param {Object} options
+ */
+Graph.prototype.setOptions = function (options) {
+ if (options) {
+ var prop;
+ // retrieve parameter values
+ if (options.width !== undefined) {this.width = options.width;}
+ if (options.height !== undefined) {this.height = options.height;}
+ if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
+ if (options.selectable !== undefined) {this.selectable = options.selectable;}
+ if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
+ if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
+ if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
+ if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
+
+
+
+ if (options.labels !== undefined) {
+ for (prop in options.labels) {
+ if (options.labels.hasOwnProperty(prop)) {
+ this.constants.labels[prop] = options.labels[prop];
+ }
+ }
+ }
+
+ if (options.onAdd) {
+ this.triggerFunctions.add = options.onAdd;
+ }
+
+ if (options.onEdit) {
+ this.triggerFunctions.edit = options.onEdit;
+ }
+
+ if (options.onConnect) {
+ this.triggerFunctions.connect = options.onConnect;
+ }
+
+ if (options.onDelete) {
+ this.triggerFunctions.del = options.onDelete;
+ }
+
+ if (options.physics) {
+ if (options.physics.barnesHut) {
+ this.constants.physics.barnesHut.enabled = true;
+ for (prop in options.physics.barnesHut) {
+ if (options.physics.barnesHut.hasOwnProperty(prop)) {
+ this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
+ }
+ }
+ }
+
+ if (options.physics.repulsion) {
+ this.constants.physics.barnesHut.enabled = false;
+ for (prop in options.physics.repulsion) {
+ if (options.physics.repulsion.hasOwnProperty(prop)) {
+ this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
+ }
+ }
+ }
+ }
+
+ if (options.hierarchicalLayout) {
+ this.constants.hierarchicalLayout.enabled = true;
+ for (prop in options.hierarchicalLayout) {
+ if (options.hierarchicalLayout.hasOwnProperty(prop)) {
+ this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
+ }
+ }
+ }
+ else if (options.hierarchicalLayout !== undefined) {
+ this.constants.hierarchicalLayout.enabled = false;
+ }
+
+ if (options.clustering) {
+ this.constants.clustering.enabled = true;
+ for (prop in options.clustering) {
+ if (options.clustering.hasOwnProperty(prop)) {
+ this.constants.clustering[prop] = options.clustering[prop];
+ }
+ }
+ }
+ else if (options.clustering !== undefined) {
+ this.constants.clustering.enabled = false;
+ }
+
+ if (options.navigation) {
+ this.constants.navigation.enabled = true;
+ for (prop in options.navigation) {
+ if (options.navigation.hasOwnProperty(prop)) {
+ this.constants.navigation[prop] = options.navigation[prop];
+ }
+ }
+ }
+ else if (options.navigation !== undefined) {
+ this.constants.navigation.enabled = false;
+ }
+
+ if (options.keyboard) {
+ this.constants.keyboard.enabled = true;
+ for (prop in options.keyboard) {
+ if (options.keyboard.hasOwnProperty(prop)) {
+ this.constants.keyboard[prop] = options.keyboard[prop];
+ }
+ }
+ }
+ else if (options.keyboard !== undefined) {
+ this.constants.keyboard.enabled = false;
+ }
+
+ if (options.dataManipulation) {
+ this.constants.dataManipulation.enabled = true;
+ for (prop in options.dataManipulation) {
+ if (options.dataManipulation.hasOwnProperty(prop)) {
+ this.constants.dataManipulation[prop] = options.dataManipulation[prop];
+ }
+ }
+ }
+ else if (options.dataManipulation !== undefined) {
+ this.constants.dataManipulation.enabled = false;
+ }
+
+ // TODO: work out these options and document them
+ if (options.edges) {
+ for (prop in options.edges) {
+ if (options.edges.hasOwnProperty(prop)) {
+ if (typeof options.edges[prop] != "object") {
+ this.constants.edges[prop] = options.edges[prop];
+ }
+ }
+ }
+
+ if (options.edges.color !== undefined) {
+ if (util.isString(options.edges.color)) {
+ this.constants.edges.color = {};
+ this.constants.edges.color.color = options.edges.color;
+ this.constants.edges.color.highlight = options.edges.color;
+ }
+ else {
+ if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
+ if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
+ }
+ }
+
+ if (!options.edges.fontColor) {
+ if (options.edges.color !== undefined) {
+ if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
+ else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
+ }
+ }
+
+ // Added to support dashed lines
+ // 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;
+ }
+ }
+ }
+
+ if (options.nodes) {
+ for (prop in options.nodes) {
+ if (options.nodes.hasOwnProperty(prop)) {
+ this.constants.nodes[prop] = options.nodes[prop];
+ }
+ }
+
+ if (options.nodes.color) {
+ this.constants.nodes.color = util.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);
+ }
+ }
+ }
+
+ if (options.tooltip) {
+ for (prop in options.tooltip) {
+ if (options.tooltip.hasOwnProperty(prop)) {
+ this.constants.tooltip[prop] = options.tooltip[prop];
+ }
+ }
+ if (options.tooltip.color) {
+ this.constants.tooltip.color = util.parseColor(options.tooltip.color);
+ }
+ }
+ }
+
+
+ // (Re)loading the mixins that can be enabled or disabled in the options.
+ // load the force calculation functions, grouped under the physics system.
+ this._loadPhysicsSystem();
+ // load the navigation system.
+ this._loadNavigationControls();
+ // load the data manipulation system
+ this._loadManipulationSystem();
+ // configure the smooth curves
+ this._configureSmoothCurves();
+
+
+ // bind keys. If disabled, this will not do anything;
+ this._createKeyBinds();
+
+ this.setSize(this.width, this.height);
+ this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
+ this._setScale(1);
+ this._redraw();
+};
+
+/**
+ * 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);
+ }
+
+ 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);
+ }
+
+ 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('doubletap', me._onDoubleTap.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('release', me._onRelease.bind(me) );
+ this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
+ this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
+ this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
+
+ // add the frame to the container element
+ this.containerElement.appendChild(this.frame);
+
+};
+
+
+/**
+ * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
+ * @private
+ */
+Graph.prototype._createKeyBinds = function() {
+ var me = this;
+ this.mousetrap = mousetrap;
+
+ this.mousetrap.reset();
+
+ if (this.constants.keyboard.enabled == true) {
+ this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
+ this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
+ this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
+ this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
+ this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
+ this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
+ this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
+ this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
+ this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
+ this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
+ this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
+ this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
+ this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
+ }
+
+ if (this.constants.dataManipulation.enabled == true) {
+ this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
+ this.mousetrap.bind("del",this._deleteSelected.bind(me));
+ }
+};
+
+/**
+ * Get the pointer location from a touch location
+ * @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.center);
+ this.drag.pinched = false;
+ this.pinch.scale = this._getScale();
+
+ this._handleTouch(this.drag.pointer);
+};
+
+/**
+ * handle drag start event
+ * @private
+ */
+Graph.prototype._onDragStart = function () {
+ this._handleDragStart();
+};
+
+
+/**
+ * This function is called by _onDragStart.
+ * It is separated out because we can then overload it for the datamanipulation system.
+ *
+ * @private
+ */
+Graph.prototype._handleDragStart = function() {
+ var drag = this.drag;
+ var node = this._getNodeAt(drag.pointer);
+ // note: drag.pointer is set in _onTouch to get the initial touch location
+
+ drag.dragging = true;
+ drag.selection = [];
+ drag.translation = this._getTranslation();
+ drag.nodeId = null;
+
+ if (node != null) {
+ drag.nodeId = node.id;
+ // select the clicked node if not yet selected
+ if (!node.isSelected()) {
+ this._selectObject(node,false);
+ }
+
+ // create an array with the selected nodes and their original location and status
+ for (var objectId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
+ var object = this.selectionObj.nodes[objectId];
+ var s = {
+ id: object.id,
+ node: object,
+
+ // store original x, y, xFixed and yFixed, make the node temporarily Fixed
+ x: object.x,
+ y: object.y,
+ xFixed: object.xFixed,
+ yFixed: object.yFixed
+ };
+
+ object.xFixed = true;
+ object.yFixed = true;
+
+ drag.selection.push(s);
+ }
+ }
+ }
+};
+
+
+/**
+ * handle drag event
+ * @private
+ */
+Graph.prototype._onDrag = function (event) {
+ this._handleOnDrag(event)
+};
+
+
+/**
+ * This function is called by _onDrag.
+ * It is separated out because we can then overload it for the datamanipulation system.
+ *
+ * @private
+ */
+Graph.prototype._handleOnDrag = function(event) {
+ if (this.drag.pinched) {
+ return;
+ }
+
+ var pointer = this._getPointer(event.gesture.center);
+
+ 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;
+
+ 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 _animationStep if not yet running
+ if (!this.moving) {
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ // move the graph
+ var diffX = pointer.x - this.drag.pointer.x;
+ var diffY = pointer.y - this.drag.pointer.y;
+
+ this._setTranslation(
+ this.drag.translation.x + diffX,
+ this.drag.translation.y + diffY);
+ this._redraw();
+ this.moved = true;
+ }
+};
+
+/**
+ * handle drag start event
+ * @private
+ */
+Graph.prototype._onDragEnd = function () {
+ this.drag.dragging = false;
+ 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;
+ });
+ }
+};
+
+/**
+ * handle tap/click event: select/unselect a node
+ * @private
+ */
+Graph.prototype._onTap = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this.pointerPosition = pointer;
+ this._handleTap(pointer);
+
+};
+
+
+/**
+ * handle doubletap event
+ * @private
+ */
+Graph.prototype._onDoubleTap = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this._handleDoubleTap(pointer);
+};
+
+
+/**
+ * handle long tap event: multi select nodes
+ * @private
+ */
+Graph.prototype._onHold = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this.pointerPosition = pointer;
+ this._handleOnHold(pointer);
+};
+
+/**
+ * handle the release of the screen
+ *
+ * @private
+ */
+Graph.prototype._onRelease = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this._handleOnRelease(pointer);
+};
+
+/**
+ * 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;
+ }
+
+ // TODO: enabled moving while pinching?
+ var scale = this.pinch.scale * event.gesture.scale;
+ this._zoom(scale, pointer)
+};
+
+/**
+ * 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 Position on screen
+ * @return {Number} appliedScale scale is limited within the boundaries
+ * @private
+ */
+Graph.prototype._zoom = function(scale, pointer) {
+ var scaleOld = this._getScale();
+ if (scale < 0.00001) {
+ scale = 0.00001;
+ }
+ if (scale > 10) {
+ scale = 10;
+ }
+// + this.frame.canvas.clientHeight / 2
+ 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;
+
+ this.areaCenter = {"x" : this._canvasToX(pointer.x),
+ "y" : this._canvasToY(pointer.y)};
+
+ this._setScale(scale);
+ this._setTranslation(tx, ty);
+ this.updateClustersDefault();
+ this._redraw();
+
+
+ return scale;
+};
+
+
+/**
+ * Event handler for mouse wheel event, used to zoom the timeline
+ * See http://adomas.org/javascript-mouse-wheel/
+ * 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;
+ }
+
+ // 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) {
+
+ // calculate the new scale
+ var scale = this._getScale();
+ var zoom = delta / 10;
+ if (delta < 0) {
+ zoom = zoom / (1 - zoom);
+ }
+ scale *= (1 + zoom);
+
+ // calculate the pointer location
+ var gesture = util.fakeGesture(this, event);
+ var pointer = this._getPointer(gesture.center);
+
+ // apply the new scale
+ this._zoom(scale, pointer);
+ }
+
+ // Prevent default actions caused by mouse wheel.
+ event.preventDefault();
+};
+
+
+/**
+ * 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 = util.fakeGesture(this, event);
+ var pointer = this._getPointer(gesture.center);
+
+ // check if the previously selected node is still selected
+ if (this.popupNode) {
+ this._checkHidePopup(pointer);
+ }
+
+ // 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 calculationTimer
+ }
+ if (!this.drag.dragging) {
+ this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
+ }
+};
+
+/**
+ * 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)
+ };
+
+ 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;
+ }
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+ }
+
+ if (this.popupNode) {
+ // show popup message window
+ if (this.popupNode != lastPopupNode) {
+ var me = this;
+ if (!me.popup) {
+ me.popup = new Popup(me.frame, me.constants.tooltip);
+ }
+
+ // 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();
+ }
+ }
+ else {
+ if (this.popup) {
+ this.popup.hide();
+ }
+ }
+};
+
+
+/**
+ * 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();
+ }
+ }
+};
+
+
+/**
+ * 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%';
+
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
+
+ if (this.manipulationDiv !== undefined) {
+ this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
+ }
+ if (this.navigationDivs !== undefined) {
+ if (this.navigationDivs['wrapper'] !== undefined) {
+ this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
+ this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
+ }
+ }
+
+ this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
+};
+
+/**
+ * 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;
+
+ 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.off(event, callback);
+ });
+ }
+
+ // remove drawn nodes
+ this.nodes = {};
+
+ if (this.nodesData) {
+ // subscribe to new dataset
+ var me = this;
+ util.forEach(this.nodesListeners, function (callback, event) {
+ me.nodesData.on(event, callback);
+ });
+
+ // draw all new nodes
+ var ids = this.nodesData.getIds();
+ this._addNodes(ids);
+ }
+ this._updateSelection();
+};
+
+/**
+ * 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
+
+ if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
+ var radius = 10 * 0.1*ids.length;
+ var angle = 2 * Math.PI * Math.random();
+ if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
+ if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
+ }
+ this.moving = true;
+ }
+ this._updateNodeIndexList();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+ this._reconnectEdges();
+ this._updateValueRange(this.nodes);
+ this.updateLabels();
+};
+
+/**
+ * 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;
+ }
+ }
+ this.moving = true;
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateNodeIndexList();
+ 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];
+ }
+ this._updateNodeIndexList();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+ 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.off(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.on(event, callback);
+ });
+
+ // draw all new nodes
+ var ids = this.edgesData.getIds();
+ this._addEdges(ids);
+ }
+
+ this._reconnectEdges();
+};
+
+/**
+ * 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();
+ }
+
+ var data = edgesData.get(id, {"showInternalIds" : true});
+ edges[id] = new Edge(data, this, this.constants);
+ }
+
+ this.moving = true;
+ this._updateValueRange(edges);
+ this._createBezierNodes();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+};
+
+/**
+ * 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];
+
+ 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;
+ }
+ }
+
+ this._createBezierNodes();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ 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) {
+ if (edge.via != null) {
+ delete this.sectors['support']['nodes'][edge.via.id];
+ }
+ edge.disconnect();
+ delete edges[id];
+ }
+ }
+
+ this.moving = true;
+ this._updateValueRange(edges);
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
+};
+
+/**
+ * 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 = [];
+ }
+ }
+
+ for (id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ edge.from = null;
+ edge.to = null;
+ edge.connect();
+ }
+ }
+};
+
+/**
+ * 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;
+
+ // 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);
+ }
+ }
+ }
+};
+
+/**
+ * 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
+ * @private
+ */
+Graph.prototype._redraw = function() {
+ var ctx = this.frame.canvas.getContext('2d');
+ // clear the canvas
+ var w = this.frame.canvas.width;
+ var h = this.frame.canvas.height;
+ ctx.clearRect(0, 0, w, h);
+
+ // set scaling and translation
+ ctx.save();
+ ctx.translate(this.translation.x, this.translation.y);
+ ctx.scale(this.scale, this.scale);
+
+ this.canvasTopLeft = {
+ "x": this._canvasToX(0),
+ "y": this._canvasToY(0)
+ };
+ this.canvasBottomRight = {
+ "x": this._canvasToX(this.frame.canvas.clientWidth),
+ "y": this._canvasToY(this.frame.canvas.clientHeight)
+ };
+
+ this._doInAllSectors("_drawAllSectorNodes",ctx);
+ this._doInAllSectors("_drawEdges",ctx);
+ this._doInAllSectors("_drawNodes",ctx,false);
+
+// this._doInSupportSector("_drawNodes",ctx,true);
+// this._drawTree(ctx,"#F00F0F");
+
+ // restore original scaling and translation
+ ctx.restore();
+};
+
+/**
+ * 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
+ };
+ }
+
+ if (offsetX !== undefined) {
+ this.translation.x = offsetX;
+ }
+ if (offsetY !== undefined) {
+ this.translation.y = offsetY;
+ }
+};
+
+/**
+ * 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
+ * @private
+ */
+Graph.prototype._getScale = function() {
+ return this.scale;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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 ;
+};
+
+/**
+ * Redraw all nodes
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Boolean} [alwaysShow]
+ * @private
+ */
+Graph.prototype._drawNodes = function(ctx,alwaysShow) {
+ if (alwaysShow === undefined) {
+ alwaysShow = false;
+ }
+
+ // first draw the unselected nodes
+ var nodes = this.nodes;
+ var selected = [];
+
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
+ if (nodes[id].isSelected()) {
+ selected.push(id);
+ }
+ else {
+ if (nodes[id].inArea() || alwaysShow) {
+ nodes[id].draw(ctx);
+ }
+ }
+ }
+ }
+
+ // draw the selected nodes on top
+ for (var s = 0, sMax = selected.length; s < sMax; s++) {
+ if (nodes[selected[s]].inArea() || alwaysShow) {
+ nodes[selected[s]].draw(ctx);
+ }
+ }
+};
+
+/**
+ * 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];
+ edge.setScale(this.scale);
+ if (edge.connected) {
+ edges[id].draw(ctx);
+ }
+ }
+ }
+};
+
+/**
+ * Find a stable position for all nodes
+ * @private
+ */
+Graph.prototype._stabilize = function() {
+ if (this.constants.freezeForStabilization == true) {
+ this._freezeDefinedNodes();
+ }
+
+ // find stable position
+ var count = 0;
+ while (this.moving && count < this.constants.stabilizationIterations) {
+ this._physicsTick();
+ count++;
+ }
+ this.zoomExtent(false,true);
+ if (this.constants.freezeForStabilization == true) {
+ this._restoreFrozenNodes();
+ }
+ this.emit("stabilized",{iterations:count});
+};
+
+
+Graph.prototype._freezeDefinedNodes = function() {
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].x != null && nodes[id].y != null) {
+ nodes[id].fixedData.x = nodes[id].xFixed;
+ nodes[id].fixedData.y = nodes[id].yFixed;
+ nodes[id].xFixed = true;
+ nodes[id].yFixed = true;
+ }
+ }
+ }
+};
+
+Graph.prototype._restoreFrozenNodes = function() {
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].fixedData.x != null) {
+ nodes[id].xFixed = nodes[id].fixedData.x;
+ nodes[id].yFixed = nodes[id].fixedData.y;
+ }
+ }
+ }
+};
+
+
+/**
+ * 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) {
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+
+/**
+ * /**
+ * Perform one discrete step for all nodes
+ *
+ * @private
+ */
+Graph.prototype._discreteStepNodes = function() {
+ var interval = this.physicsDiscreteStepsize;
+ var nodes = this.nodes;
+ var nodeId;
+ var nodesPresent = false;
+
+ if (this.constants.maxVelocity > 0) {
+ for (nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
+ nodesPresent = true;
+ }
+ }
+ }
+ else {
+ for (nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ nodes[nodeId].discreteStep(interval);
+ nodesPresent = true;
+ }
+ }
+ }
+
+ if (nodesPresent == true) {
+ var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
+ if (vminCorrected > 0.5*this.constants.maxVelocity) {
+ this.moving = true;
+ }
+ else {
+ this.moving = this._isMoving(vminCorrected);
+ }
+ }
+};
+
+
+Graph.prototype._physicsTick = function() {
+ if (!this.freezeSimulation) {
+ if (this.moving) {
+ this._doInAllActiveSectors("_initializeForceCalculation");
+ this._doInAllActiveSectors("_discreteStepNodes");
+ if (this.constants.smoothCurves) {
+ this._doInSupportSector("_discreteStepNodes");
+ }
+ this._findCenter(this._getRange())
+ }
+ }
+};
+
+
+/**
+ * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
+ * It reschedules itself at the beginning of the function
+ *
+ * @private
+ */
+Graph.prototype._animationStep = function() {
+ // reset the timer so a new scheduled animation step can be set
+ this.timer = undefined;
+ // handle the keyboad movement
+ this._handleNavigation();
+
+ // this schedules a new animation step
+ this.start();
+
+ // start the physics simulation
+ var calculationTime = Date.now();
+ var maxSteps = 1;
+ this._physicsTick();
+ var timeRequired = Date.now() - calculationTime;
+ while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
+ this._physicsTick();
+ timeRequired = Date.now() - calculationTime;
+ maxSteps++;
+
+ }
+
+ // start the rendering process
+ var renderTime = Date.now();
+ this._redraw();
+ this.renderTime = Date.now() - renderTime;
+};
+
+if (typeof window !== 'undefined') {
+ window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
+}
+
+/**
+ * Schedule a animation step with the refreshrate interval.
+ */
+Graph.prototype.start = function() {
+ if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
+ if (!this.timer) {
+ var ua = navigator.userAgent.toLowerCase();
+
+ var requiresTimeout = false;
+ if (ua.indexOf('msie 9.0') != -1) { // IE 9
+ requiresTimeout = true;
+ }
+ else if (ua.indexOf('safari') != -1) { // safari
+ if (ua.indexOf('chrome') <= -1) {
+ requiresTimeout = true;
+ }
+ }
+
+ if (requiresTimeout == true) {
+ this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ }
+ else{
+ this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ }
+ }
+ }
+ else {
+ this._redraw();
+ }
+};
+
+
+/**
+ * Move the graph according to the keyboard presses.
+ *
+ * @private
+ */
+Graph.prototype._handleNavigation = function() {
+ if (this.xIncrement != 0 || this.yIncrement != 0) {
+ var translation = this._getTranslation();
+ this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
+ }
+ if (this.zoomIncrement != 0) {
+ var center = {
+ x: this.frame.canvas.clientWidth / 2,
+ y: this.frame.canvas.clientHeight / 2
+ };
+ this._zoom(this.scale*(1 + this.zoomIncrement), center);
+ }
+};
+
+
+/**
+ * Freeze the _animationStep
+ */
+Graph.prototype.toggleFreeze = function() {
+ if (this.freezeSimulation == false) {
+ this.freezeSimulation = true;
+ }
+ else {
+ this.freezeSimulation = false;
+ this.start();
+ }
+};
+
+
+
+Graph.prototype._configureSmoothCurves = function(disableStart) {
+ if (disableStart === undefined) {
+ disableStart = true;
+ }
+
+ if (this.constants.smoothCurves == true) {
+ this._createBezierNodes();
+ }
+ else {
+ // delete the support nodes
+ this.sectors['support']['nodes'] = {};
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ this.edges[edgeId].smooth = false;
+ this.edges[edgeId].via = null;
+ }
+ }
+ }
+ this._updateCalculationNodes();
+ if (!disableStart) {
+ this.moving = true;
+ this.start();
+ }
+};
+
+Graph.prototype._createBezierNodes = function() {
+ if (this.constants.smoothCurves == true) {
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ var edge = this.edges[edgeId];
+ if (edge.via == null) {
+ edge.smooth = true;
+ var nodeId = "edgeId:".concat(edge.id);
+ this.sectors['support']['nodes'][nodeId] = new Node(
+ {id:nodeId,
+ mass:1,
+ shape:'circle',
+ image:"",
+ internalMultiplier:1
+ },{},{},this.constants);
+ edge.via = this.sectors['support']['nodes'][nodeId];
+ edge.via.parentEdgeId = edge.id;
+ edge.positionBezierNode();
+ }
+ }
+ }
+ }
+};
+
+
+Graph.prototype._initializeMixinLoaders = function () {
+ for (var mixinFunction in graphMixinLoaders) {
+ if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
+ Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
+ }
+ }
+};
+
+/**
+ * Load the XY positions of the nodes into the dataset.
+ */
+Graph.prototype.storePosition = function() {
+ var dataArray = [];
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ var allowedToMoveX = !this.nodes.xFixed;
+ var allowedToMoveY = !this.nodes.yFixed;
+ if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
+ dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
+ }
+ }
+ }
+ this.nodesData.update(dataArray);
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * vis.js module exports
+ */
+var vis = {
+ util: util,
+
+ DataSet: DataSet,
+ DataView: DataView,
+ Range: Range,
+ Stack: Stack,
+ TimeStep: TimeStep,
+
+ components: {
+ items: {
+ Item: Item,
+ ItemBox: ItemBox,
+ ItemPoint: ItemPoint,
+ ItemRange: ItemRange
+ },
+
+ Component: Component,
+ Panel: Panel,
+ RootPanel: RootPanel,
+ ItemSet: ItemSet,
+ TimeAxis: TimeAxis
+ },
+
+ 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;
+}
+
+/**
+ * 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;
+}
+
diff --git a/dist/vis.min.css b/dist/vis.min.css
index 7ae07c0c..6b32bc70 100644
--- a/dist/vis.min.css
+++ b/dist/vis.min.css
@@ -1 +1 @@
-.vis.timeline.rootpanel{position:relative;overflow:hidden;border:1px solid #bfbfbf;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .vpanel{position:absolute;overflow:hidden}.vis.timeline .groupset{position:absolute;padding:0;margin:0}.vis.timeline .labels{position:absolute;top:0;left:0;width:100%;height:100%;padding:0;margin:0;border-right:1px solid #bfbfbf;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .labels .label-set{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-top:none;border-bottom:1px solid #bfbfbf}.vis.timeline .labels .label-set .vlabel{position:absolute;left:0;top:0;width:100%;color:#4d4d4d}.vis.timeline.top .groupset .itemset-axis,.vis.timeline.top .labels .label-set .vlabel{border-top:1px solid #bfbfbf;border-bottom:none}.vis.timeline.bottom .groupset .itemset-axis,.vis.timeline.bottom .labels .label-set .vlabel{border-top:none;border-bottom:1px solid #bfbfbf}.vis.timeline .labels .label-set .vlabel .inner{display:inline-block;padding:5px}.vis.timeline .itemset{position:absolute;padding:0;margin:0;overflow:hidden}.vis.timeline .itemset-axis{position:absolute}.vis.timeline .item{position:absolute;color:#1A1A1A;border-color:#97B0F8;background-color:#D5DDF6;display:inline-block;padding:5px}.vis.timeline .item.selected{border-color:#FFC200;background-color:#FFF785;z-index:999}.vis.timeline.editable .item.selected{cursor:move}.vis.timeline .item.point.selected{background-color:#FFF785;z-index:999}.vis.timeline .item.point.selected .dot{border-color:#FFC200}.vis.timeline .item.cluster{background:#97B0F8 url(img/cluster_bg.png);color:#fff}.vis.timeline .item.cluster.point{border-color:#D5DDF6}.vis.timeline .item.box{text-align:center;border-style:solid;border-width:1px;border-radius:5px;-moz-border-radius:5px}.vis.timeline .item.point{background:0 0}.vis.timeline .dot,.vis.timeline .item.dot{padding:0;border:5px solid #97B0F8;position:absolute;border-radius:5px;-moz-border-radius:5px}.vis.timeline .item.range,.vis.timeline .item.rangeoverflow{border-style:solid;border-width:1px;border-radius:2px;-moz-border-radius:2px;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .item.range .content,.vis.timeline .item.rangeoverflow .content{position:relative;display:inline-block}.vis.timeline .item.range .content{overflow:hidden;max-width:100%}.vis.timeline .item.line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis.timeline .item .content{white-space:nowrap;overflow:hidden}.vis.timeline .item .delete{background:url(img/timeline/delete.png) no-repeat top center;position:absolute;width:24px;height:24px;top:0;right:-24px;cursor:pointer}.vis.timeline .item.range .drag-left,.vis.timeline .item.rangeoverflow .drag-left{position:absolute;width:24px;height:100%;top:0;left:-4px;cursor:w-resize;z-index:10000}.vis.timeline .item.range .drag-right,.vis.timeline .item.rangeoverflow .drag-right{position:absolute;width:24px;height:100%;top:0;right:-4px;cursor:e-resize;z-index:10001}.vis.timeline .axis{position:relative}.vis.timeline .axis .text{position:absolute;color:#4d4d4d;padding:3px;white-space:nowrap}.vis.timeline .axis .text.measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis.timeline .axis .grid.vertical{position:absolute;width:0;border-right:1px solid}.vis.timeline .axis .grid.horizontal{position:absolute;left:0;width:100%;height:0;border-bottom:1px solid}.vis.timeline .axis .grid.minor{border-color:#e5e5e5}.vis.timeline .axis .grid.major{border-color:#bfbfbf}.vis.timeline .currenttime{background-color:#FF7F6E;width:2px;z-index:9}.vis.timeline .customtime{background-color:#6E94FF;width:2px;cursor:move;z-index:9}div.graph-manipulationDiv{border-width:0;border-bottom:1px;border-style:solid;border-color:#d6d9d8;background:#fff;background:-moz-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#fff),color-stop(48%,#fcfcfc),color-stop(50%,#fafafa),color-stop(100%,#fcfcfc));background:-webkit-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-o-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-ms-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:linear-gradient(to bottom,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#fcfcfc', GradientType=0);width:600px;height:30px;z-index:10;position:absolute}div.graph-manipulation-editMode{height:30px;z-index:10;position:absolute;margin-top:20px}div.graph-manipulation-closeDiv{height:30px;width:30px;z-index:11;position:absolute;margin-top:3px;margin-left:590px;background-position:0 0;background-repeat:no-repeat;background-image:url(img/graph/cross.png);cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}span.graph-manipulationUI{font-family:verdana;font-size:12px;-moz-border-radius:15px;border-radius:15px;display:inline-block;background-position:0 0;background-repeat:no-repeat;height:24px;margin:-14px 0 0 10px;vertical-align:middle;cursor:pointer;padding:0 8px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}span.graph-manipulationUI:hover{box-shadow:1px 1px 8px rgba(0,0,0,.2)}span.graph-manipulationUI:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}span.graph-manipulationUI.back{background-image:url(img/graph/backIcon.png)}span.graph-manipulationUI.none:hover{box-shadow:1px 1px 8px rgba(0,0,0,0);cursor:default}span.graph-manipulationUI.none:active{box-shadow:1px 1px 8px rgba(0,0,0,0)}span.graph-manipulationUI.none{padding:0}span.graph-manipulationUI.notification{margin:2px;font-weight:700}span.graph-manipulationUI.add{background-image:url(img/graph/addNodeIcon.png)}span.graph-manipulationUI.edit{background-image:url(img/graph/editIcon.png)}span.graph-manipulationUI.edit.editmode{background-color:#fcfcfc;border-style:solid;border-width:1px;border-color:#ccc}span.graph-manipulationUI.connect{background-image:url(img/graph/connectIcon.png)}span.graph-manipulationUI.delete{background-image:url(img/graph/deleteIcon.png)}span.graph-manipulationLabel{margin:0 0 0 23px;line-height:25px}div.graph-seperatorLine{display:inline-block;width:1px;height:20px;background-color:#bdbdbd;margin:5px 7px 0 15px}div.graph-navigation{width:34px;height:34px;z-index:10;-moz-border-radius:17px;border-radius:17px;position:absolute;display:inline-block;background-position:2px 2px;background-repeat:no-repeat;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.graph-navigation:hover{box-shadow:0 0 3px 3px rgba(56,207,21,.3)}div.graph-navigation.active,div.graph-navigation:active{box-shadow:0 0 1px 3px rgba(56,207,21,.95)}div.graph-navigation.up{background-image:url(img/graph/upArrow.png);bottom:50px;left:55px}div.graph-navigation.down{background-image:url(img/graph/downArrow.png);bottom:10px;left:55px}div.graph-navigation.left{background-image:url(img/graph/leftArrow.png);bottom:10px;left:15px}div.graph-navigation.right{background-image:url(img/graph/rightArrow.png);bottom:10px;left:95px}div.graph-navigation.zoomIn{background-image:url(img/graph/plus.png);bottom:10px;right:15px}div.graph-navigation.zoomOut{background-image:url(img/graph/minus.png);bottom:10px;right:55px}div.graph-navigation.zoomExtends{background-image:url(img/graph/zoomExtends.png);bottom:50px;right:15px}
\ No newline at end of file
+.vis.timeline.rootpanel{position:relative;overflow:hidden;border:1px solid #bfbfbf;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .vpanel{position:absolute;overflow:hidden;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .vpanel.side{border-right:1px solid #bfbfbf}.vis.timeline .vpanel.side.hidden{display:none}.vis.timeline .groupset{position:relative}.vis.timeline .labelset{position:relative;width:100%;overflow:hidden;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .labelset .vlabel{position:relative;left:0;top:0;width:100%;color:#4d4d4d;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline.bottom .labelset .vlabel,.vis.timeline.top .groupset .itemset,.vis.timeline.top .vpanel.side-content{border-top:1px solid #bfbfbf;border-bottom:none}.vis.timeline.bottom .groupset .itemset,.vis.timeline.bottom .vpanel.side-content,.vis.timeline.top .labelset .vlabel{border-top:none;border-bottom:1px solid #bfbfbf}.vis.timeline .labelset .vlabel .inner{display:inline-block;padding:5px}.vis.timeline .itemset{position:relative;padding:0;margin:0;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .axis{overflow:visible}.vis.timeline .item{position:absolute;color:#1A1A1A;border-color:#97B0F8;background-color:#D5DDF6;display:inline-block;padding:5px}.vis.timeline .item.selected{border-color:#FFC200;background-color:#FFF785;z-index:999}.vis.timeline.editable .item.selected{cursor:move}.vis.timeline .item.point.selected{background-color:#FFF785;z-index:999}.vis.timeline .item.dot.selected,.vis.timeline .item.point.selected .dot{border-color:#FFC200}.vis.timeline .item.cluster{background:#97B0F8 url(img/cluster_bg.png);color:#fff}.vis.timeline .item.cluster.point{border-color:#D5DDF6}.vis.timeline .item.box{text-align:center;border-style:solid;border-width:1px;border-radius:5px;-moz-border-radius:5px}.vis.timeline .item.point{background:0 0}.vis.timeline .dot,.vis.timeline .item.dot{padding:0;border:5px solid #97B0F8;position:absolute;border-radius:5px;-moz-border-radius:5px}.vis.timeline .item.range,.vis.timeline .item.rangeoverflow{border-style:solid;border-width:1px;border-radius:2px;-moz-border-radius:2px;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .item.range .content,.vis.timeline .item.rangeoverflow .content{position:relative;display:inline-block}.vis.timeline .item.range .content{overflow:hidden;max-width:100%}.vis.timeline .item.line{padding:0;position:absolute;width:0;border-left-width:1px;border-left-style:solid}.vis.timeline .item .content{white-space:nowrap;overflow:hidden}.vis.timeline .item .delete{background:url(img/timeline/delete.png) no-repeat top center;position:absolute;width:24px;height:24px;top:0;right:-24px;cursor:pointer}.vis.timeline .item.range .drag-left,.vis.timeline .item.rangeoverflow .drag-left{position:absolute;width:24px;height:100%;top:0;left:-4px;cursor:w-resize;z-index:10000}.vis.timeline .item.range .drag-right,.vis.timeline .item.rangeoverflow .drag-right{position:absolute;width:24px;height:100%;top:0;right:-4px;cursor:e-resize;z-index:10001}.vis.timeline .timeaxis{position:absolute}.vis.timeline .timeaxis .text{position:absolute;color:#4d4d4d;padding:3px;white-space:nowrap}.vis.timeline .timeaxis .text.measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis.timeline .timeaxis .grid.vertical{position:absolute;width:0;border-right:1px solid}.vis.timeline .timeaxis .grid.horizontal{position:absolute;left:0;width:100%;height:0;border-bottom:1px solid}.vis.timeline .timeaxis .grid.minor{border-color:#e5e5e5}.vis.timeline .timeaxis .grid.major{border-color:#bfbfbf}.vis.timeline .currenttime{background-color:#FF7F6E;width:2px;z-index:9}.vis.timeline .customtime{background-color:#6E94FF;width:2px;cursor:move;z-index:9}div.graph-manipulationDiv{border-width:0;border-bottom:1px;border-style:solid;border-color:#d6d9d8;background:#fff;background:-moz-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#fff),color-stop(48%,#fcfcfc),color-stop(50%,#fafafa),color-stop(100%,#fcfcfc));background:-webkit-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-o-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-ms-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:linear-gradient(to bottom,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#fcfcfc', GradientType=0);width:600px;height:30px;z-index:10;position:absolute}div.graph-manipulation-editMode{height:30px;z-index:10;position:absolute;margin-top:20px}div.graph-manipulation-closeDiv{height:30px;width:30px;z-index:11;position:absolute;margin-top:3px;margin-left:590px;background-position:0 0;background-repeat:no-repeat;background-image:url(img/graph/cross.png);cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}span.graph-manipulationUI{font-family:verdana;font-size:12px;-moz-border-radius:15px;border-radius:15px;display:inline-block;background-position:0 0;background-repeat:no-repeat;height:24px;margin:-14px 0 0 10px;vertical-align:middle;cursor:pointer;padding:0 8px;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}span.graph-manipulationUI:hover{box-shadow:1px 1px 8px rgba(0,0,0,.2)}span.graph-manipulationUI:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}span.graph-manipulationUI.back{background-image:url(img/graph/backIcon.png)}span.graph-manipulationUI.none:hover{box-shadow:1px 1px 8px rgba(0,0,0,0);cursor:default}span.graph-manipulationUI.none:active{box-shadow:1px 1px 8px rgba(0,0,0,0)}span.graph-manipulationUI.none{padding:0}span.graph-manipulationUI.notification{margin:2px;font-weight:700}span.graph-manipulationUI.add{background-image:url(img/graph/addNodeIcon.png)}span.graph-manipulationUI.edit{background-image:url(img/graph/editIcon.png)}span.graph-manipulationUI.edit.editmode{background-color:#fcfcfc;border-style:solid;border-width:1px;border-color:#ccc}span.graph-manipulationUI.connect{background-image:url(img/graph/connectIcon.png)}span.graph-manipulationUI.delete{background-image:url(img/graph/deleteIcon.png)}span.graph-manipulationLabel{margin:0 0 0 23px;line-height:25px}div.graph-seperatorLine{display:inline-block;width:1px;height:20px;background-color:#bdbdbd;margin:5px 7px 0 15px}div.graph-navigation{width:34px;height:34px;z-index:10;-moz-border-radius:17px;border-radius:17px;position:absolute;display:inline-block;background-position:2px 2px;background-repeat:no-repeat;cursor:pointer;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}div.graph-navigation:hover{box-shadow:0 0 3px 3px rgba(56,207,21,.3)}div.graph-navigation.active,div.graph-navigation:active{box-shadow:0 0 1px 3px rgba(56,207,21,.95)}div.graph-navigation.up{background-image:url(img/graph/upArrow.png);bottom:50px;left:55px}div.graph-navigation.down{background-image:url(img/graph/downArrow.png);bottom:10px;left:55px}div.graph-navigation.left{background-image:url(img/graph/leftArrow.png);bottom:10px;left:15px}div.graph-navigation.right{background-image:url(img/graph/rightArrow.png);bottom:10px;left:95px}div.graph-navigation.zoomIn{background-image:url(img/graph/plus.png);bottom:10px;right:15px}div.graph-navigation.zoomOut{background-image:url(img/graph/minus.png);bottom:10px;right:55px}div.graph-navigation.zoomExtends{background-image:url(img/graph/zoomExtends.png);bottom:50px;right:15px}
\ No newline at end of file
diff --git a/dist/vis.min.js b/dist/vis.min.js
index 827b44ec..a7ae2cb0 100644
--- a/dist/vis.min.js
+++ b/dist/vis.min.js
@@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
- * @version 0.7.4-SNAPSHOT
- * @date 2014-04-16
+ * @version 0.7.5-SNAPSHOT
+ * @date 2014-04-22
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -22,12 +22,12 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-!function(t){if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.vis=t()}}(function(){var define,module,exports;return function t(e,i,s){function n(r,a){if(!i[r]){if(!e[r]){var h="function"==typeof require&&require;if(!a&&h)return h(r,!0);if(o)return o(r,!0);throw new Error("Cannot find module '"+r+"'")}var d=i[r]={exports:{}};e[r][0].call(d.exports,function(t){var i=e[r][1][t];return n(i?i:t)},d,d.exports,t,e,i,s)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;ri;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,s,n;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=new Array(r),n=0;r>n;){var a,h;n in o&&(a=o[n],h=t.call(i,a,n,o),s[n]=h),n++}return s}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var s=[],n=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(n,r,o,e)&&s.push(r)}return s}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],s=i.length;return function(n){if("object"!=typeof n&&"function"!=typeof n||null===n)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in n)t.call(n,r)&&o.push(r);if(e)for(var a=0;s>a;a++)t.call(n,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},n=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,n.prototype=new s,n}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},n=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,n.prototype=new s,n});var util={};util.isNumber=function(t){return t instanceof Number||"number"==typeof t},util.isString=function(t){return t instanceof String||"string"==typeof t},util.isDate=function(t){if(t instanceof Date)return!0;if(util.isString(t)){var e=ASPDateRegex.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},util.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},util.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},util.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var s=arguments[e];for(var n in s)s.hasOwnProperty(n)&&void 0!==s[n]&&(t[n]=s[n])}return t},util.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(util.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(moment.isMoment(t))return new Date(t.valueOf());if(util.isString(t))return i=ASPDateRegex.exec(t),i?new Date(Number(i[1])):moment(t).toDate();throw new Error("Cannot convert object of type "+util.getType(t)+" to type Date");case"Moment":if(util.isNumber(t))return moment(t);if(t instanceof Date)return moment(t.valueOf());if(moment.isMoment(t))return moment(t);if(util.isString(t))return i=ASPDateRegex.exec(t),moment(i?Number(i[1]):t);throw new Error("Cannot convert object of type "+util.getType(t)+" to type Date");case"ISODate":if(util.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(moment.isMoment(t))return t.toDate().toISOString();if(util.isString(t))return i=ASPDateRegex.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+util.getType(t)+" to type ISODate");case"ASPDate":if(util.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(util.isString(t)){i=ASPDateRegex.exec(t);var s;return s=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+s+")/"}throw new Error("Cannot convert object of type "+util.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+util.getType(t)+' to type "'+e+'"')}};var ASPDateRegex=/^\/?Date\((\-?\d+)/i;util.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},util.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetLeft,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetLeft,s-=n.scrollLeft,n=n.offsetParent;return s},util.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetTop,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetTop,s-=n.scrollTop,n=n.offsetParent;return s},util.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,s=document.body;return e+(i&&i.scrollTop||s&&s.scrollTop||0)-(i&&i.clientTop||s&&s.clientTop||0)},util.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,s=document.body;return e+(i&&i.scrollLeft||s&&s.scrollLeft||0)-(i&&i.clientLeft||s&&s.clientLeft||0)},util.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},util.removeClassName=function(t,e){var i=t.className.split(" "),s=i.indexOf(e);-1!=s&&(i.splice(s,1),t.className=i.join(" "))},util.forEach=function(t,e){var i,s;if(t instanceof Array)for(i=0,s=t.length;s>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},util.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},util.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},util.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},util.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},util.fakeGesture=function(t,e){var i=null,s=Hammer.event.collectEventData(this,i,e);return isNaN(s.center.pageX)&&(s.center.pageX=e.pageX),isNaN(s.center.pageY)&&(s.center.pageY=e.pageY),s},util.option={},util.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},util.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},util.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},util.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),util.isString(t)?t:util.isNumber(t)?t+"px":e||null},util.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},util.GiveDec=function GiveDec(Hex){return Value="A"==Hex?10:"B"==Hex?11:"C"==Hex?12:"D"==Hex?13:"E"==Hex?14:"F"==Hex?15:eval(Hex)},util.GiveHex=function(t){return Value=10==t?"A":11==t?"B":12==t?"C":13==t?"D":14==t?"E":15==t?"F":""+t},util.parseColor=function(t){var e;if(util.isString(t))if(util.isValidHex(t)){var i=util.hexToHSV(t),s={h:i.h,s:.45*i.s,v:Math.min(1,1.05*i.v)},n={h:i.h,s:Math.min(1,1.25*i.v),v:.6*i.v},o=util.HSVToHex(n.h,n.h,n.v),r=util.HSVToHex(s.h,s.s,s.v);e={background:t,border:o,highlight:{background:r,border:o}}}else e={background:t,border:t,highlight:{background:t,border:t}};else e={},e.background=t.background||"white",e.border=t.border||e.background,util.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border);return e},util.hexToRGB=function(t){t=t.replace("#","").toUpperCase();var e=util.GiveDec(t.substring(0,1)),i=util.GiveDec(t.substring(1,2)),s=util.GiveDec(t.substring(2,3)),n=util.GiveDec(t.substring(3,4)),o=util.GiveDec(t.substring(4,5)),r=util.GiveDec(t.substring(5,6)),a=16*e+i,h=16*s+n,i=16*o+r;return{r:a,g:h,b:i}},util.RGBToHex=function(t,e,i){var s=util.GiveHex(Math.floor(t/16)),n=util.GiveHex(t%16),o=util.GiveHex(Math.floor(e/16)),r=util.GiveHex(e%16),a=util.GiveHex(Math.floor(i/16)),h=util.GiveHex(i%16),d=s+n+o+r+a+h;return"#"+d},util.RGBToHSV=function(t,e,i){t/=255,e/=255,i/=255;
-var s=Math.min(t,Math.min(e,i)),n=Math.max(t,Math.max(e,i));if(s==n)return{h:0,s:0,v:s};var o=t==s?e-i:i==s?t-e:i-t,r=t==s?3:i==s?1:5,a=60*(r-o/(n-s))/360,h=(n-s)/n,d=n;return{h:a,s:h,v:d}},util.HSVToRGB=function(t,e,i){var s,n,o,r=Math.floor(6*t),a=6*t-r,h=i*(1-e),d=i*(1-a*e),c=i*(1-(1-a)*e);switch(r%6){case 0:s=i,n=c,o=h;break;case 1:s=d,n=i,o=h;break;case 2:s=h,n=i,o=c;break;case 3:s=h,n=d,o=i;break;case 4:s=c,n=h,o=i;break;case 5:s=i,n=h,o=d}return{r:Math.floor(255*s),g:Math.floor(255*n),b:Math.floor(255*o)}},util.HSVToHex=function(t,e,i){var s=util.HSVToRGB(t,e,i);return util.RGBToHex(s.r,s.g,s.b)},util.hexToHSV=function(t){var e=util.hexToRGB(t);return util.RGBToHSV(e.r,e.g,e.b)},util.isValidHex=function(t){var e=/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(t);return e},util.copyObject=function(t,e){for(var i in t)t.hasOwnProperty(i)&&("object"==typeof t[i]?(e[i]={},util.copyObject(t[i],e[i])):e[i]=t[i])},DataSet.prototype.on=function(t,e){var i=this.subscribers[t];i||(i=[],this.subscribers[t]=i),i.push({callback:e})},DataSet.prototype.subscribe=DataSet.prototype.on,DataSet.prototype.off=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},DataSet.prototype.unsubscribe=DataSet.prototype.off,DataSet.prototype._trigger=function(t,e,i){if("*"==t)throw new Error("Cannot trigger event *");var s=[];t in this.subscribers&&(s=s.concat(this.subscribers[t])),"*"in this.subscribers&&(s=s.concat(this.subscribers["*"]));for(var n=0;no;o++)i=n._addItem(t[o]),s.push(i);else if(util.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var c={},l=0,u=a.length;u>l;l++){var p=a[l];c[p]=t.getValue(h,l)}i=n._addItem(c),s.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=n._addItem(t),s.push(i)}return s.length&&this._trigger("add",{items:s},e),s},DataSet.prototype.update=function(t,e){var i=[],s=[],n=this,o=n.fieldId,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(util.isDataTable(t))for(var d=this._getColumnNames(t),c=0,l=t.getNumberOfRows();l>c;c++){for(var u={},p=0,g=d.length;g>p;p++){var f=d[p];u[f]=t.getValue(c,p)}r(u)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),s.length&&this._trigger("update",{items:s},e),i.concat(s)},DataSet.prototype.get=function(){var t,e,i,s,n=this,o=this.showInternalIds,r=util.getType(arguments[0]);"String"==r||"Number"==r?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==r?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var a;if(i&&i.type){if(a="DataTable"==i.type?"DataTable":"Array",s&&a!=util.getType(s))throw new Error('Type of parameter "data" ('+util.getType(s)+") does not correspond with specified options.type ("+i.type+")");if("DataTable"==a&&!util.isDataTable(s))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else a=s?"DataTable"==util.getType(s)?"DataTable":"Array":"Array";void 0!=i&&void 0!=i.showInternalIds&&(this.showInternalIds=i.showInternalIds);var h,d,c,l,u=i&&i.convert||this.options.convert,p=i&&i.filter,g=[];if(void 0!=t)h=n._getItem(t,u),p&&!p(h)&&(h=null);else if(void 0!=e)for(c=0,l=e.length;l>c;c++)h=n._getItem(e[c],u),(!p||p(h))&&g.push(h);else for(d in this.data)this.data.hasOwnProperty(d)&&(h=n._getItem(d,u),(!p||p(h))&&g.push(h));if(this.showInternalIds=o,i&&i.order&&void 0==t&&this._sort(g,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)h=this._filterFields(h,f);else for(c=0,l=g.length;l>c;c++)g[c]=this._filterFields(g[c],f)}if("DataTable"==a){var m=this._getColumnNames(s);if(void 0!=t)n._appendRow(s,m,h);else for(c=0,l=g.length;l>c;c++)n._appendRow(s,m,g[c]);return s}if(void 0!=t)return h;if(s){for(c=0,l=g.length;l>c;c++)s.push(g[c]);return s}return g},DataSet.prototype.getIds=function(t){var e,i,s,n,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,c=[];if(a)if(h){o=[];for(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++)c[e]=o[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(n=this._getItem(s,d),a(n)&&c.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++)c[e]=o[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(n=r[s],c.push(n[this.fieldId]));return c},DataSet.prototype.forEach=function(t,e){var i,s,n=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],s=i[this.fieldId],t(i,s);else for(s in r)r.hasOwnProperty(s)&&(i=this._getItem(s,o),(!n||n(i))&&t(i,s))},DataSet.prototype.map=function(t,e){var i,s=e&&e.filter,n=e&&e.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},DataSet.prototype._filterFields=function(t,e){var i={};for(var s in t)t.hasOwnProperty(s)&&-1!=e.indexOf(s)&&(i[s]=t[s]);return i},DataSet.prototype._sort=function(t,e){if(util.isString(e)){var i=e;t.sort(function(t,e){var s=t[i],n=e[i];return s>n?1:n>s?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},DataSet.prototype.remove=function(t,e){var i,s,n,o=[];if(t instanceof Array)for(i=0,s=t.length;s>i;i++)n=this._remove(t[i]),null!=n&&o.push(n);else n=this._remove(t),null!=n&&o.push(n);return o.length&&this._trigger("remove",{items:o},e),o},DataSet.prototype._remove=function(t){if(util.isNumber(t)||util.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},DataSet.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},DataSet.prototype.max=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],r=o[t];null!=r&&(!i||r>s)&&(i=o,s=r)}return i},DataSet.prototype.min=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],r=o[t];null!=r&&(!i||s>r)&&(i=o,s=r)}return i},DataSet.prototype.distinct=function(t){var e=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=util.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},DataSet.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=util.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=util.convert(t[s],n)}return this.data[e]=i,e},DataSet.prototype._getItem=function(t,e){var i,s,n=this.data[t];if(!n)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==r&&s in a&&!this.showInternalIds||(o[i]=util.convert(s,e[i])));else for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==r&&s in a&&!this.showInternalIds||(o[i]=s));return o},DataSet.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=util.convert(t[s],n)}return e},DataSet.prototype.isInternalId=function(t){return t in this.internalIds},DataSet.prototype._getColumnNames=function(t){for(var e=[],i=0,s=t.getNumberOfColumns();s>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},DataSet.prototype._appendRow=function(t,e,i){for(var s=t.addRow(),n=0,o=e.length;o>n;n++){var r=e[n];t.setValue(s,n,i[r])}},DataView.prototype.setData=function(t){var e,i,s;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var n in this.ids)this.ids.hasOwnProperty(n)&&e.push(n);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,s=e.length;s>i;i++)n=e[i],this.ids[n]=!0;this._trigger("add",{items:e}),this.data.on&&this.data.on("*",this.listener)}},DataView.prototype.get=function(){var t,e,i,s=this,n=util.getType(arguments[0]);"String"==n||"Number"==n||"Array"==n?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=util.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return s.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},DataView.prototype.getIds=function(t){var e;if(this.data){var i,s=this.options.filter;i=t&&t.filter?s?function(e){return s(e)&&t.filter(e)}:t.filter:s,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},DataView.prototype._onEvent=function(t,e,i){var s,n,o,r,a=e&&e.items,h=this.data,d=[],c=[],l=[];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]?c.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],l.push(o));break;case"remove":for(s=0,n=a.length;n>s;s++)o=a[s],this.ids[o]&&(delete this.ids[o],l.push(o))}d.length&&this._trigger("add",{items:d},i),c.length&&this._trigger("update",{items:c},i),l.length&&this._trigger("remove",{items:l},i)}},DataView.prototype.on=DataSet.prototype.on,DataView.prototype.off=DataSet.prototype.off,DataView.prototype._trigger=DataSet.prototype._trigger,DataView.prototype.subscribe=DataView.prototype.on,DataView.prototype.unsubscribe=DataView.prototype.off,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+1e3*this.step*60);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+1e3*this.step*60*60);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,s=864e5,n=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),s/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){var e=new Date(t.valueOf());if(this.scale==TimeStep.SCALE.YEAR){var i=e.getFullYear()+Math.round(e.getMonth()/12);e.setFullYear(Math.round(i/this.step)*this.step),e.setMonth(0),e.setDate(0),e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)e.getDate()>15?(e.setDate(1),e.setMonth(e.getMonth()+1)):e.setDate(1),e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:e.setHours(24*Math.round(e.getHours()/24));break;default:e.setHours(12*Math.round(e.getHours()/12))}e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:e.setMinutes(60*Math.round(e.getMinutes()/60));break;default:e.setMinutes(30*Math.round(e.getMinutes()/30))}e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:e.setMinutes(5*Math.round(e.getMinutes()/5)),e.setSeconds(0);break;case 5:e.setSeconds(60*Math.round(e.getSeconds()/60));break;default:e.setSeconds(30*Math.round(e.getSeconds()/30))}e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:e.setSeconds(5*Math.round(e.getSeconds()/5)),e.setMilliseconds(0);break;case 5:e.setMilliseconds(1e3*Math.round(e.getMilliseconds()/1e3));break;default:e.setMilliseconds(500*Math.round(e.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var s=this.step>5?this.step/2:1;e.setMilliseconds(Math.round(e.getMilliseconds()/s)*s)}return e},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return moment(t).format("SSS");case TimeStep.SCALE.SECOND:return moment(t).format("s");case TimeStep.SCALE.MINUTE:return moment(t).format("HH:mm");case TimeStep.SCALE.HOUR:return moment(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return moment(t).format("ddd D");case TimeStep.SCALE.DAY:return moment(t).format("D");case TimeStep.SCALE.MONTH:return moment(t).format("MMM");case TimeStep.SCALE.YEAR:return moment(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return moment(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return moment(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return moment(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return moment(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return moment(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},Stack.prototype.setOptions=function(t){util.extend(this.options,t)},Stack.prototype.update=function(){this._order(),this._stack()},Stack.prototype._order=function(){var t=this.itemset.items;if(!t)throw new Error("Cannot stack items: ItemSet does not contain items");var e=[],i=0;util.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var s=this.options.order||this.defaultOptions.order;if("function"!=typeof s)throw new Error("Option order must be a function");e.sort(s),this.ordered=e},Stack.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)}},Stack.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},Stack.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},Emitter(Range.prototype),Range.prototype.setOptions=function(t){util.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},Range.prototype.subscribe=function(t,e,i,s){function n(t){o._onMouseWheel(t,e,s)}var o=this;if("move"==i)t.on("dragstart",function(t){o._onDragStart(t,e)}),t.on("drag",function(t){o._onDrag(t,e,s)}),t.on("dragend",function(t){o._onDragEnd(t,e)}),t.on("hold",function(){o._onHold()});else{if("zoom"!=i)throw new TypeError('Unknown event "'+i+'". Choose "move" or "zoom".');t.on("mousewheel",n),t.on("DOMMouseScroll",n),t.on("touch",function(t){o._onTouch(t)}),t.on("pinch",function(t){o._onPinch(t,e,s)})}},Range.prototype.setRange=function(t,e){var i=this._applyRange(t,e);if(i){var s={start:this.start,end:this.end};this.emit("rangechange",s),this.emit("rangechanged",s)}},Range.prototype._applyRange=function(t,e){var i,s=null!=t?util.convert(t,"Date").valueOf():this.start,n=null!=e?util.convert(e,"Date").valueOf():this.end,o=null!=this.options.max?util.convert(this.options.max,"Date").valueOf():null,r=null!=this.options.min?util.convert(this.options.min,"Date").valueOf():null;if(isNaN(s)||null===s)throw new Error('Invalid start "'+t+'"');if(isNaN(n)||null===n)throw new Error('Invalid end "'+e+'"');if(s>n&&(n=s),null!==r&&r>s&&(i=r-s,s+=i,n+=i,null!=o&&n>o&&(n=o)),null!==o&&n>o&&(i=n-o,s-=i,n-=i,null!=r&&r>s&&(s=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>n-s&&(this.end-this.start===a?(s=this.start,n=this.end):(i=a-(n-s),s-=i/2,n+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),n-s>h&&(this.end-this.start===h?(s=this.start,n=this.end):(i=n-s-h,s+=i/2,n-=i/2))}var d=this.start!=s||this.end!=n;return this.start=s,this.end=n,d},Range.prototype.getRange=function(){return{start:this.start,end:this.end}},Range.prototype.conversion=function(t){return Range.conversion(this.start,this.end,t)},Range.conversion=function(t,e,i){return 0!=i&&e-t!=0?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var touchParams={};Range.prototype._onDragStart=function(t,e){if(!touchParams.ignore){touchParams.start=this.start,touchParams.end=this.end;var i=e.frame;i&&(i.style.cursor="move")}},Range.prototype._onDrag=function(t,e,i){if(validateDirection(i),!touchParams.ignore){var s="horizontal"==i?t.gesture.deltaX:t.gesture.deltaY,n=touchParams.end-touchParams.start,o="horizontal"==i?e.width:e.height,r=-s/o*n;this._applyRange(touchParams.start+r,touchParams.end+r),this.emit("rangechange",{start:this.start,end:this.end})}},Range.prototype._onDragEnd=function(t,e){touchParams.ignore||(e.frame&&(e.frame.style.cursor="auto"),this.emit("rangechanged",{start:this.start,end:this.end}))},Range.prototype._onMouseWheel=function(t,e,i){validateDirection(i);var s=0;if(t.wheelDelta?s=t.wheelDelta/120:t.detail&&(s=-t.detail/3),s){var n;n=0>s?1-s/5:1/(1+s/5);var o=util.fakeGesture(this,t),r=getPointer(o.center,e.frame),a=this._pointerToDate(e,i,r);this.zoom(n,a)}t.preventDefault()},Range.prototype._onTouch=function(t){touchParams.start=this.start,touchParams.end=this.end,touchParams.ignore=!1,touchParams.center=null;var e=ItemSet.itemFromTarget(t);e&&e.selected&&this.options.editable&&(touchParams.ignore=!0)},Range.prototype._onHold=function(){touchParams.ignore=!0},Range.prototype._onPinch=function(t,e,i){if(touchParams.ignore=!0,t.gesture.touches.length>1){touchParams.center||(touchParams.center=getPointer(t.gesture.center,e.frame));var s=1/t.gesture.scale,n=this._pointerToDate(e,i,touchParams.center),o=getPointer(t.gesture.center,e.frame),r=(this._pointerToDate(e,i,o),parseInt(n+(touchParams.start-n)*s)),a=parseInt(n+(touchParams.end-n)*s);this.setRange(r,a)}},Range.prototype._pointerToDate=function(t,e,i){var s;if("horizontal"==e){var n=t.width;return s=this.conversion(n),i.x/s.scale+s.offset}var o=t.height;return s=this.conversion(o),i.y/s.scale+s.offset},Range.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,s=e+(this.end-e)*t;this.setRange(i,s)},Range.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,s=this.end+e*t;this.start=i,this.end=s},Range.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,s=this.start-i,n=this.end-i;this.setRange(s,n)},Emitter(Controller.prototype),Controller.prototype.add=function(t){if(void 0==t.id)throw new Error("Component has no field id");if(!(t instanceof Component||t instanceof Controller))throw new TypeError("Component must be an instance of prototype Component or Controller");t.setController(this),this.components[t.id]=t},Controller.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]===t))break;e&&(this.components[e].setController(null),delete this.components[e])},Controller.prototype.repaint=function t(){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={};util.forEach(this.components,t),this.emit("repaint"),e&&this.reflow()},Controller.prototype.reflow=function e(){function e(s,n){n in i||(s.depends&&s.depends.forEach(function(t){e(t,t.id)}),s.parent&&e(s.parent,s.parent.id),t=s.reflow()||t,i[n]=!0)}var t=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};util.forEach(this.components,e),this.emit("reflow"),t&&this.repaint()},Component.prototype.setOptions=function(t){t&&(util.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},Component.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},Component.prototype.setController=function(t){this.controller=t||null},Component.prototype.getController=function(){return this.controller},Component.prototype.getContainer=function(){return null},Component.prototype.getFrame=function(){return this.frame},Component.prototype.repaint=function(){return!1},Component.prototype.reflow=function(){return!1},Component.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},Component.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},Component.prototype.requestRepaint=function(){if(!this.controller)throw new Error("Cannot request a repaint: no controller configured");this.controller.emit("request-repaint")},Component.prototype.requestReflow=function(){if(!this.controller)throw new Error("Cannot request a reflow: no controller configured");this.controller.emit("request-reflow")},Panel.prototype=new Component,Panel.prototype.setOptions=Component.prototype.setOptions,Panel.prototype.getContainer=function(){return this.frame},Panel.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.option.asSize,s=this.options,n=this.frame;if(!n){n=document.createElement("div"),n.className="vpanel";var o=s.className;o&&("function"==typeof o?util.addClassName(n,String(o())):util.addClassName(n,String(o))),this.frame=n,t+=1}if(!n.parentNode){if(!this.parent)throw new Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw new Error("Cannot repaint panel: parent has no container element");r.appendChild(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},Panel.prototype.reflow=function(){var t=0,e=util.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},RootPanel.prototype=new Panel,RootPanel.prototype.setOptions=Component.prototype.setOptions,RootPanel.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.option.asSize,s=this.options,n=this.frame;if(n||(n=document.createElement("div"),this.frame=n,this._registerListeners(),t+=1),!n.parentNode){if(!this.container)throw new Error("Cannot repaint root panel: no container attached");this.container.appendChild(n),t+=1}n.className="vis timeline rootpanel "+s.orientation+(s.editable?" editable":"");var o=s.className;return o&&util.addClassName(n,util.option.asString(o)),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._updateWatch(),t>0},RootPanel.prototype.reflow=function(){var t=0,e=util.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},RootPanel.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},RootPanel.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?void(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow()):void t._unwatch()};util.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},RootPanel.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},RootPanel.prototype.setController=function(t){this.controller=t||null,this.controller?this._registerListeners():this._unregisterListeners()},RootPanel.prototype._registerListeners=function(){if(this.frame&&this.controller&&!this.hammer){this.hammer=Hammer(this.frame,{prevent_default:!0});for(var t in this.listeners)this.listeners.hasOwnProperty(t)&&this.hammer.on(t,this.listeners[t])}},RootPanel.prototype._unregisterListeners=function(){if(this.hammer){for(var t in this.listeners)this.listeners.hasOwnProperty(t)&&this.hammer.off(t,this.listeners[t]);this.hammer=null}},TimeAxis.prototype=new Component,TimeAxis.prototype.setOptions=Component.prototype.setOptions,TimeAxis.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},TimeAxis.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},TimeAxis.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},TimeAxis.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.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",!a.parentNode){if(!this.parent)throw new Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw new Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var c=a.nextSibling;d.removeChild(a);var l="bottom"==n&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";
-if(t+=e(a.style,"top",i(s.top,l)),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 u=void 0,p=0;r.hasNext()&&1e3>p;){p++;var g=r.getCurrent(),f=this.toScreen(g),m=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(f,r.getLabelMinor()),m&&this.getOption("showMajorLabels")?(f>0&&(void 0==u&&(u=f),this._repaintMajorText(f,r.getLabelMajor())),this._repaintMajorLine(f)):this._repaintMinorLine(f),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),_=y.length*(o.majorCharWidth||10)+10;(void 0==u||u>_)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),c?d.insertBefore(a,c):d.appendChild(a)}return t>0},TimeAxis.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},TimeAxis.prototype._repaintEnd=function(){util.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},TimeAxis.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var 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"},TimeAxis.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"},TimeAxis.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},TimeAxis.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},TimeAxis.prototype._repaintLine=function(){{var t=this.dom.line,e=this.frame;this.options}this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&t.parentElement&&(e.removeChild(t.line),delete this.dom.line)},TimeAxis.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}},TimeAxis.prototype.reflow=function(){var t=0,e=util.updateProperty,i=this.frame,s=this.range;if(!s)throw new Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var 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 new Error('Unkown orientation "'+this.getOption("orientation")+'"')}var c=n.minorLabelHeight+n.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",c),this._updateConversion();var l=util.convert(s.start,"Number"),u=util.convert(s.end,"Number"),p=this.toTime(5*(n.minorCharWidth||10)).valueOf()-this.toTime(0).valueOf();this.step=new TimeStep(new Date(l),new Date(u),p),t+=e(n.range,"start",l),t+=e(n.range,"end",u),t+=e(n.range,"minimumStep",p.valueOf())}return t>0},TimeAxis.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):Range.conversion(t.start,t.end,this.width)},TimeAxis.prototype.snap=function(t){return this.step.snap(t)},CurrentTime.prototype=new Component,CurrentTime.prototype.setOptions=Component.prototype.setOptions,CurrentTime.prototype.getContainer=function(){return this.frame},CurrentTime.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCurrentTime"))return t&&(i.removeChild(t),delete this.frame),!1;t||(t=document.createElement("div"),t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t),this.frame=t),e.conversion||e._updateConversion();var s=new Date,n=e.toScreen(s);t.style.left=n+"px",t.title="Current time: "+s,void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer);var o=this,r=1/e.conversion.scale/2;return 30>r&&(r=30),this.currentTimeTimer=setTimeout(function(){o.repaint()},r),!1},CustomTime.prototype=new Component,Emitter(CustomTime.prototype),CustomTime.prototype.setOptions=Component.prototype.setOptions,CustomTime.prototype.getContainer=function(){return this.frame},CustomTime.prototype.repaint=function(){var t=this.frame,e=this.parent;if(!e)throw new Error("Cannot repaint bar: no parent attached");var i=e.parent.getContainer();if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCustomTime"))return t&&(i.removeChild(t),delete this.frame),!1;if(!t){t=document.createElement("div"),t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t);var s=document.createElement("div");s.style.position="relative",s.style.top="0px",s.style.left="-10px",s.style.height="100%",s.style.width="20px",t.appendChild(s),this.frame=t,this.hammer=Hammer(t,{prevent_default:!0}),this.hammer.on("dragstart",this._onDragStart.bind(this)),this.hammer.on("drag",this._onDrag.bind(this)),this.hammer.on("dragend",this._onDragEnd.bind(this))}e.conversion||e._updateConversion();var n=e.toScreen(this.customTime);return t.style.left=n+"px",t.title="Time: "+this.customTime,!1},CustomTime.prototype.setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},CustomTime.prototype.getCustomTime=function(){return new Date(this.customTime.valueOf())},CustomTime.prototype._onDragStart=function(t){this.eventParams.customTime=this.customTime,t.stopPropagation(),t.preventDefault()},CustomTime.prototype._onDrag=function(t){var e=t.gesture.deltaX,i=this.parent.toScreen(this.eventParams.customTime)+e,s=this.parent.toTime(i);this.setCustomTime(s),this.controller&&this.controller.emit("timechange",{time:this.customTime}),t.stopPropagation(),t.preventDefault()},CustomTime.prototype._onDragEnd=function(t){this.controller&&this.controller.emit("timechanged",{time:this.customTime}),t.stopPropagation(),t.preventDefault()},ItemSet.prototype=new Panel,ItemSet.types={box:ItemBox,range:ItemRange,rangeoverflow:ItemRangeOverflow,point:ItemPoint},ItemSet.prototype.setOptions=Component.prototype.setOptions,ItemSet.prototype.setController=function(t){var e;if(this.controller)for(e in this.eventListeners)this.eventListeners.hasOwnProperty(e)&&this.controller.off(e,this.eventListeners[e]);if(this.controller=t||null,this.controller)for(e in this.eventListeners)this.eventListeners.hasOwnProperty(e)&&this.controller.on(e,this.eventListeners[e])},function(t){var e=null;Object.defineProperty(t,"controller",{get:function(){return e},set:function(){}})}(this),ItemSet.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},ItemSet.prototype.setSelection=function(t){var e,i,s,n;if(t){if(!Array.isArray(t))throw new TypeError("Array expected");for(e=0,i=this.selection.length;i>e;e++)s=this.selection[e],n=this.items[s],n&&n.unselect();for(this.selection=[],e=0,i=t.length;i>e;e++)s=t[e],n=this.items[s],n&&(this.selection.push(s),n.select());this.controller&&this.requestRepaint()}},ItemSet.prototype.getSelection=function(){return this.selection.concat([])},ItemSet.prototype._deselect=function(t){for(var e=this.selection,i=0,s=e.length;s>i;i++)if(e[i]==t){e.splice(i,1);break}},ItemSet.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.option.asSize,s=this.options,n=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset",r["timeline-itemset"]=this;var a=s.className;a&&util.addClassName(r,util.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var c=document.createElement("div");c.className="itemset-axis",this.dom.axis=c,this.frame=r,t+=1}if(!this.parent)throw new Error("Cannot repaint itemset: no parent attached");var l=this.parent.getContainer();if(!l)throw new Error("Cannot repaint itemset: parent has no container element");r.parentNode||(l.appendChild(r),t+=1),this.dom.axis.parentNode||(l.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(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 u=this,p=this.queue,g=this.itemsData,f=this.items,m={};for(var v in p)if(p.hasOwnProperty(v)){var y=p[v],_=f[v],b=y.action;switch(b){case"add":case"update":var w=g&&g.get(v,m);if(w){var S=w.type||w.start&&w.end&&"range"||s.type||"box",x=ItemSet.types[S];if(_&&(x&&_ instanceof x?(_.data=w,t++):(t+=_.hide(),_=null)),!_){if(!x)throw new TypeError('Unknown item type "'+S+'"');_=new x(u,w,s,o),_.id=y.id,t++}_.repaint(),f[v]=_}delete p[v];break;case"remove":_&&(_.selected&&u._deselect(v),t+=_.hide()),delete f[v],delete p[v];break;default:console.log('Error: unknown action "'+b+'"')}}return util.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},ItemSet.prototype.getForeground=function(){return this.dom.foreground},ItemSet.prototype.getBackground=function(){return this.dom.background},ItemSet.prototype.getAxis=function(){return this.dom.axis},ItemSet.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&"axis"in e.margin?e.margin.axis:this.defaultOptions.margin.axis,s=e.margin&&"item"in e.margin?e.margin.item:this.defaultOptions.margin.item,n=util.updateProperty,o=util.option.asNumber,r=util.option.asSize,a=this.frame;if(a){this._updateConversion(),util.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),c=null!=r(e.height);if(c)h=a.offsetHeight;else{var l=this.stack.ordered;if(l.length){var u=l[0].top,p=l[0].top+l[0].height;util.forEach(l,function(t){u=Math.min(u,t.top),p=Math.max(p,t.top+t.height)}),h=p-u+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},ItemSet.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},ItemSet.prototype.setItems=function(t){var e,i=this,s=this.itemsData;if(t){if(!(t instanceof DataSet||t instanceof DataView))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(s&&(util.forEach(this.listeners,function(t,e){s.unsubscribe(e,t)}),e=s.getIds(),this._onRemove(e)),this.itemsData){var n=this.id;util.forEach(this.listeners,function(t,e){i.itemsData.on(e,t,n)}),e=this.itemsData.getIds(),this._onAdd(e)}},ItemSet.prototype.getItems=function(){return this.itemsData},ItemSet.prototype.removeItem=function(t){var e=this.itemsData.get(t),i=this._myDataSet();e&&this.options.onRemove(e,function(t){t&&i.remove(t)})},ItemSet.prototype._onUpdate=function(t){this._toQueue("update",t)},ItemSet.prototype._onAdd=function(t){this._toQueue("add",t)},ItemSet.prototype._onRemove=function(t){this._toQueue("remove",t)},ItemSet.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]={id:e,action:t}}),this.controller&&this.requestRepaint()},ItemSet.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):Range.conversion(t.start,t.end,this.width)},ItemSet.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},ItemSet.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},ItemSet.prototype._onDragStart=function(t){if(this.options.editable){var e=ItemSet.itemFromTarget(t),i=this;if(e&&e.selected){var s=t.target.dragLeftItem,n=t.target.dragRightItem;this.touchParams.itemProps=s?[{item:s,start:e.data.start.valueOf()}]:n?[{item:n,end:e.data.end.valueOf()}]:this.getSelection().map(function(t){var e=i.items[t],s={item:e};return"start"in e.data&&(s.start=e.data.start.valueOf()),"end"in e.data&&(s.end=e.data.end.valueOf()),s}),t.stopPropagation()}}},ItemSet.prototype._onDrag=function(t){if(this.touchParams.itemProps){var e=this.options.snap||null,i=t.gesture.deltaX,s=i/this.conversion.scale;this.touchParams.itemProps.forEach(function(t){if("start"in t){var i=new Date(t.start+s);t.item.data.start=e?e(i):i}if("end"in t){var n=new Date(t.end+s);t.item.data.end=e?e(n):n}}),this.requestReflow(),t.stopPropagation()}},ItemSet.prototype._onDragEnd=function(t){if(this.touchParams.itemProps){var e=[],i=this,s=this._myDataSet();this.touchParams.itemProps.forEach(function(t){var n=t.item.id,o=i.itemsData.get(n),r=!1;"start"in t.item.data&&(r=t.start!=t.item.data.start.valueOf(),o.start=util.convert(t.item.data.start,s.convert.start)),"end"in t.item.data&&(r=r||t.end!=t.item.data.end.valueOf(),o.end=util.convert(t.item.data.end,s.convert.end)),r&&i.options.onMove(o,function(s){s?e.push(s):("start"in t&&(t.item.data.start=t.start),"end"in t&&(t.item.data.end=t.end),i.requestReflow())})}),this.touchParams.itemProps=null,e.length&&s.update(e),t.stopPropagation()}},ItemSet.itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},ItemSet.itemSetFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-itemset"))return e["timeline-itemset"];e=e.parentNode}return null},ItemSet.prototype._myDataSet=function(){for(var t=this.itemsData;t instanceof DataView;)t=t.data;return t},Item.prototype.select=function(){this.selected=!0,this.visible&&this.repaint()},Item.prototype.unselect=function(){this.selected=!1,this.visible&&this.repaint()},Item.prototype.show=function(){return!1},Item.prototype.hide=function(){return!1},Item.prototype.repaint=function(){return!1},Item.prototype.reflow=function(){return!1},Item.prototype.setOffset=function(t){this.offset=t},Item.prototype._repaintDeleteButton=function(t){if(this.selected&&this.options.editable&&!this.dom.deleteButton){var e=this.parent,i=this.id,s=document.createElement("div");s.className="delete",s.title="Delete this item",Hammer(s,{preventDefault:!0}).on("tap",function(t){e.removeItem(i),t.stopPropagation()}),t.appendChild(s),this.dom.deleteButton=s}else!this.selected&&this.dom.deleteButton&&(this.dom.deleteButton.parentNode&&this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton),this.dom.deleteButton=null)},ItemBox.prototype=new Item(null,null),ItemBox.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");if(!e.box.parentNode){var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");i.appendChild(e.box),t=!0}if(!e.line.parentNode){var s=this.parent.getBackground();if(!s)throw new Error("Cannot repaint time axis: parent has no background container element");s.appendChild(e.line),t=!0}if(!e.dot.parentNode){var n=this.parent.getAxis();if(!s)throw new Error("Cannot repaint time axis: parent has no axis container element");n.appendChild(e.dot),t=!0}if(this._repaintDeleteButton(e.box),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},ItemBox.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},ItemBox.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},ItemBox.prototype.reflow=function(){var t,e,i,s,n,o,r,a,h,d,c,l,u=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(c=this.data,l=this.parent&&this.parent.range,c&&l){var p=l.end-l.start;this.visible=c.start>l.start-p&&c.start0},ItemBox.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot",t.box["timeline-item"]=this)},ItemBox.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"}},ItemPoint.prototype=new Item(null,null),ItemPoint.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}this._repaintDeleteButton(e.point);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},ItemPoint.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},ItemPoint.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},ItemPoint.prototype.reflow=function(){var t,e,i,s,n,o,r,a,h,d,c=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var l=d.end-d.start;this.visible=h.start>d.start-l&&h.start0},ItemPoint.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot),t.point["timeline-item"]=this)},ItemPoint.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},ItemRange.prototype=new Item(null,null),ItemRange.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}this._repaintDeleteButton(e.box),this._repaintDragLeft(),this._repaintDragRight();var s=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=s&&(this.className=s,e.box.className="item range"+s,t=!0)}return t},ItemRange.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},ItemRange.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},ItemRange.prototype.reflow=function(){var t,e,i,s,n,o,r,a,h,d,c,l,u,p,g,f,m=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw new Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start)+this.offset,a=o.toScreen(this.data.end)+this.offset,c=util.updateProperty,l=t.box,u=o.width,g=i.orientation||this.defaultOptions.orientation,s=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,n=i.padding||this.defaultOptions.padding,m+=c(e.content,"width",t.content.offsetWidth),m+=c(this,"height",l.offsetHeight),-u>r&&(r=-u),a>2*u&&(a=2*u),p=0>r?Math.min(-r,a-r-e.content.width-2*n):0,m+=c(e.content,"left",p),"top"==g?(f=s,m+=c(this,"top",f)):(f=o.height-this.height-s,m+=c(this,"top",f)),m+=c(this,"left",r),m+=c(this,"width",Math.max(a-r,1))):m+=1),m>0},ItemRange.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content),t.box["timeline-item"]=this)},ItemRange.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},ItemRange.prototype._repaintDragLeft=function(){if(this.selected&&this.options.editable&&!this.dom.dragLeft){var t=document.createElement("div");t.className="drag-left",t.dragLeftItem=this,Hammer(t,{preventDefault:!0}).on("drag",function(){}),this.dom.box.appendChild(t),this.dom.dragLeft=t}else!this.selected&&this.dom.dragLeft&&(this.dom.dragLeft.parentNode&&this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft),this.dom.dragLeft=null)},ItemRange.prototype._repaintDragRight=function(){if(this.selected&&this.options.editable&&!this.dom.dragRight){var t=document.createElement("div");t.className="drag-right",t.dragRightItem=this,Hammer(t,{preventDefault:!0}).on("drag",function(){}),this.dom.box.appendChild(t),this.dom.dragRight=t}else!this.selected&&this.dom.dragRight&&(this.dom.dragRight.parentNode&&this.dom.dragRight.parentNode.removeChild(this.dom.dragRight),this.dom.dragRight=null)},ItemRangeOverflow.prototype=new ItemRange(null,null),ItemRangeOverflow.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.id);e.content.innerHTML=this.content}t=!0}this._repaintDeleteButton(e.box),this._repaintDragLeft(),this._repaintDragRight();var s=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=s&&(this.className=s,e.box.className="item rangeoverflow"+s,t=!0)}return t},ItemRangeOverflow.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")},Group.prototype=new Component,Group.prototype.setOptions=Component.prototype.setOptions,Group.prototype.getContainer=function(){return this.parent.getContainer()},Group.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 ItemSet(this,null,i),this.itemset.setRange(this.parent.range),this.view=new DataView(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},Group.prototype.setSelection=function(t){this.itemset&&this.itemset.setSelection(t)},Group.prototype.getSelection=function(){return this.itemset?this.itemset.getSelection():[]},Group.prototype.repaint=function(){return!1},Group.prototype.reflow=function(){var t=0,e=util.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},GroupSet.prototype=new Panel,GroupSet.prototype.setOptions=Component.prototype.setOptions,GroupSet.prototype.setRange=function(){},GroupSet.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)}},GroupSet.prototype.getItems=function(){return this.itemsData},GroupSet.prototype.setRange=function(t){this.range=t},GroupSet.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(util.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof DataSet?this.groupsData=t:(this.groupsData=new DataSet({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var s=this.id;util.forEach(this.listeners,function(t,e){i.groupsData.on(e,t,s)}),e=this.groupsData.getIds(),this._onAdd(e)}},GroupSet.prototype.getGroups=function(){return this.groupsData},GroupSet.prototype.setSelection=function(t){var e=[],i=this.groups;for(var s in i)if(i.hasOwnProperty(s)){var n=i[s];n.setSelection(t)}return e},GroupSet.prototype.getSelection=function(){var t=[],e=this.groups;for(var i in e)if(e.hasOwnProperty(i)){var s=e[i];t=t.concat(s.getSelection())}return t},GroupSet.prototype.repaint=function(){var t,e,i,s,n=0,o=util.updateProperty,r=util.option.asSize,a=util.option.asElement,h=this.options,d=this.dom.frame,c=this.dom.labels,l=this.dom.labelSet;if(!this.parent)throw new Error("Cannot repaint groupset: no parent attached");var u=this.parent.getContainer();if(!u)throw new Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",d["timeline-groupset"]=this,this.dom.frame=d;var p=h.className;p&&util.addClassName(d,util.option.asString(p)),n+=1}d.parentNode||(u.appendChild(d),n+=1);var g=a(h.labelContainer);if(!g)throw new Error('Cannot repaint groupset: option "labelContainer" not defined');c||(c=document.createElement("div"),c.className="labels",this.dom.labels=c),l||(l=document.createElement("div"),l.className="label-set",c.appendChild(l),this.dom.labelSet=l),c.parentNode&&c.parentNode==g||(c.parentNode&&c.parentNode.removeChild(c.parentNode),g.appendChild(c)),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")),n+=o(l.style,"height",r(h.height,this.height+"px"));
-var f=this,m=this.queue,v=this.groups,y=this.groupsData,_=Object.keys(m);if(_.length){_.forEach(function(t){var e=m[t],i=v[t];switch(e){case"add":case"update":if(!i){var s=Object.create(f.options);util.extend(s,{height:null,maxHeight:null}),i=new Group(f,t,s),i.setItems(f.itemsData),v[t]=i,f.controller.add(i)}i.data=y.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete v[t],f.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var b=this.groupsData.getIds({order:this.options.groupOrder});for(t=0;t0},GroupSet.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="vlabel";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&&util.addClassName(i,o),e.label=i,i},GroupSet.prototype.getContainer=function(){return this.dom.frame},GroupSet.prototype.getLabelsWidth=function(){return this.props.labels.width},GroupSet.prototype.reflow=function(){var t,e,i=0,s=this.options,n=util.updateProperty,o=util.option.asNumber,r=util.option.asSize,a=this.dom.frame;if(a){var h,d=o(s.maxHeight),c=null!=r(s.height);if(c)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=n(this,"height",h),i+=n(this,"top",a.offsetTop),i+=n(this,"left",a.offsetLeft),i+=n(this,"width",a.offsetWidth)}var l=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var u=e.props&&e.props.label&&e.props.label.width||0;l=Math.max(l,u)}return i+=n(this.props.labels,"width",l),i>0},GroupSet.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},GroupSet.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},GroupSet.prototype._onUpdate=function(t){this._toQueue(t,"update")},GroupSet.prototype._onAdd=function(t){this._toQueue(t,"add")},GroupSet.prototype._onRemove=function(t){this._toQueue(t,"remove")},GroupSet.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},GroupSet.groupFromTarget=function(t){for(var e,i=t.target;i;){if(i.hasOwnProperty("timeline-groupset")){e=i["timeline-groupset"];break}i=i.parentNode}if(e)for(var s in e.groups)if(e.groups.hasOwnProperty(s)){var n=e.groups[s];if(n.itemset&&ItemSet.itemSetFromTarget(t)==n.itemset)return n}return null},Timeline.prototype.on=function(t,e){this.controller.on(t,e)},Timeline.prototype.off=function(t,e){this.controller.off(t,e)},Timeline.prototype.setOptions=function(t){util.extend(this.options,t),this.range.setRange(t.start,t.end),("editable"in t||"selectable"in t)&&this.setSelection(this.options.selectable?this.getSelection():[]);var e=function(t){if(!(this.options[t]instanceof Function)||2!=this.options[t].length)throw new Error("option "+t+" must be a function "+t+"(item, callback)")}.bind(this);["onAdd","onUpdate","onRemove","onMove"].forEach(e),this.controller.reflow(),this.controller.repaint()},Timeline.prototype.setCustomTime=function(t){if(!this.customtime)throw new Error("Cannot get custom time: Custom time bar is not enabled");this.customtime.setCustomTime(t)},Timeline.prototype.getCustomTime=function(){if(!this.customtime)throw new Error("Cannot get custom time: Custom time bar is not enabled");return this.customtime.getCustomTime()},Timeline.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof DataSet&&(e=t):e=null,t instanceof DataSet||(e=new DataSet({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,o=s.max;if(null!=n&&null!=o){var r=o.valueOf()-n.valueOf();0>=r&&(r=864e5),n=new Date(n.valueOf()-.05*r),o=new Date(o.valueOf()+.05*r)}void 0!=this.options.start&&(n=util.convert(this.options.start,"Date")),void 0!=this.options.end&&(o=util.convert(this.options.end,"Date")),(null!=n||null!=o)&&this.range.setRange(n,o)}},Timeline.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?GroupSet:ItemSet;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);util.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(!util.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)}},Timeline.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var s=t.min("start");e=s?s.start.valueOf():null;var n=t.max("start");n&&(i=n.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},Timeline.prototype.setSelection=function(t){this.content&&this.content.setSelection(t)},Timeline.prototype.getSelection=function(){return this.content?this.content.getSelection():[]},Timeline.prototype.setWindow=function(t,e){this.range.setRange(t,e)},Timeline.prototype.getWindow=function(){var t=this.range.getRange();return{start:new Date(t.start),end:new Date(t.end)}},Timeline.prototype._onSelectItem=function(t){if(this.options.selectable){var e=t.gesture.srcEvent&&t.gesture.srcEvent.ctrlKey,i=t.gesture.srcEvent&&t.gesture.srcEvent.shiftKey;if(e||i)return void this._onMultiSelectItem(t);var s=ItemSet.itemFromTarget(t),n=s?[s.id]:[];this.setSelection(n),this.controller.emit("select",{items:this.getSelection()}),t.stopPropagation()}},Timeline.prototype._onAddItem=function(t){if(this.options.selectable&&this.options.editable){var e=this,i=ItemSet.itemFromTarget(t);if(i){var s=e.itemsData.get(i.id);this.options.onUpdate(s,function(t){t&&e.itemsData.update(t)})}else{var n=vis.util.getAbsoluteLeft(this.rootPanel.frame),o=t.gesture.center.pageX-n,r={start:this.timeaxis.snap(this._toTime(o)),content:"new item"},a=util.randomUUID();r[this.itemsData.fieldId]=a;var h=GroupSet.groupFromTarget(t);h&&(r.group=h.groupId),this.options.onAdd(r,function(t){t&&(e.itemsData.add(r),e.controller.once("repaint",function(){e.setSelection([a]),e.controller.emit("select",{items:e.getSelection()})}.bind(e)))})}}},Timeline.prototype._onMultiSelectItem=function(t){if(this.options.selectable){var e,i=ItemSet.itemFromTarget(t);if(i){e=this.getSelection();var s=e.indexOf(i.id);-1==s?e.push(i.id):e.splice(s,1),this.setSelection(e),this.controller.emit("select",{items:this.getSelection()}),t.stopPropagation()}}},Timeline.prototype._toTime=function(t){var e=this.range.conversion(this.content.width);return new Date(t/e.scale+e.offset)},Timeline.prototype._toScreen=function(t){var e=this.range.conversion(this.content.width);return(t.valueOf()-e.offset)*e.scale},function(t){function e(t){return D=t,u()}function i(){C=0,M=D.charAt(0)}function s(){C++,M=D.charAt(C)}function n(){return D.charAt(C+1)}function o(t){return O.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 c(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 l(){for(N=E.NULL,I="";" "==M||" "==M||"\n"==M||"\r"==M;)s();do{var t=!1;if("#"==M){for(var e=C-1;" "==D.charAt(e)||" "==D.charAt(e);)e--;if("\n"==D.charAt(e)||""==D.charAt(e)){for(;""!=M&&"\n"!=M;)s();t=!0}}if("/"==M&&"/"==n()){for(;""!=M&&"\n"!=M;)s();t=!0}if("/"==M&&"*"==n()){for(;""!=M;){if("*"==M&&"/"==n()){s(),s();break}s()}t=!0}for(;" "==M||" "==M||"\n"==M||"\r"==M;)s()}while(t);if(""==M)return void(N=E.DELIMITER);var i=M+n();if(T[i])return N=E.DELIMITER,I=i,s(),void s();if(T[M])return N=E.DELIMITER,I=M,void s();if(o(M)||"-"==M){for(I+=M,s();o(M);)I+=M,s();return"false"==I?I=!1:"true"==I?I=!0:isNaN(Number(I))||(I=Number(I)),void(N=E.IDENTIFIER)}if('"'==M){for(s();""!=M&&('"'!=M||'"'==M&&'"'==n());)I+=M,'"'==M&&s(),s();if('"'!=M)throw b('End of string " expected');return s(),void(N=E.IDENTIFIER)}for(N=E.UNKNOWN;""!=M;)I+=M,s();throw new SyntaxError('Syntax error in part "'+w(I,30)+'"')}function u(){var t={};if(i(),l(),"strict"==I&&(t.strict=!0,l()),("graph"==I||"digraph"==I)&&(t.type=I,l()),N==E.IDENTIFIER&&(t.id=I,l()),"{"!=I)throw b("Angle bracket { expected");if(l(),p(t),"}"!=I)throw b("Angle bracket } expected");if(l(),""!==I)throw b("End of file expected");return l(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==I&&"}"!=I;)g(t),";"==I&&l()}function g(t){var e=f(t);if(e)return void y(t,e);var i=m(t);if(!i){if(N!=E.IDENTIFIER)throw b("Identifier expected");var s=I;if(l(),"="==I){if(l(),N!=E.IDENTIFIER)throw b("Identifier expected");t[s]=I,l()}else v(t,s)}}function f(t){var e=null;if("subgraph"==I&&(e={},e.type="subgraph",l(),N==E.IDENTIFIER&&(e.id=I,l())),"{"==I){if(l(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=I)throw b("Angle bracket } expected");l(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function m(t){return"node"==I?(l(),t.node=_(),"node"):"edge"==I?(l(),t.edge=_(),"edge"):"graph"==I?(l(),t.graph=_(),"graph"):null}function v(t,e){var i={id:e},s=_();s&&(i.attr=s),h(t,i),y(t,e)}function y(t,e){for(;"->"==I||"--"==I;){var i,s=I;l();var n=f(t);if(n)i=n;else{if(N!=E.IDENTIFIER)throw b("Identifier or subgraph expected");i=I,h(t,{id:i}),l()}var o=_(),r=c(t,e,i,s,o);d(t,r),e=i}}function _(){for(var t=null;"["==I;){for(l(),t={};""!==I&&"]"!=I;){if(N!=E.IDENTIFIER)throw b("Attribute name expected");var e=I;if(l(),"="!=I)throw b("Equal sign = expected");if(l(),N!=E.IDENTIFIER)throw b("Attribute value expected");var i=I;a(t,e,i),l(),","==I&&l()}if("]"!=I)throw b("Bracket ] expected");l()}return t}function b(t){return new SyntaxError(t+', got "'+w(I,30)+'" (char '+C+")")}function w(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function S(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function x(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var s=e(t),n={nodes:[],edges:[],options:{}};return s.nodes&&s.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};r(e,t.attr),e.image&&(e.shape="image"),n.nodes.push(e)}),s.edges&&s.edges.forEach(function(t){var e,s;e=t.from instanceof Object?t.from.nodes:{id:t.from},s=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);n.edges.push(e)}),S(e,s,function(e,s){var o=c(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 E={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},T={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},D="",C=0,M="",I="",N=E.NULL,O=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=x}("undefined"!=typeof util?util:exports),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-n*n);this.moveTo(t,e-(r-o)),this.lineTo(t+n,e+o),this.lineTo(t-n,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-n*n);this.moveTo(t,e+(r-o)),this.lineTo(t+n,e-o),this.lineTo(t-n,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var s=0;10>s;s++){var n=s%2===0?1.3*i:.5*i;this.lineTo(t+n*Math.sin(2*s*Math.PI/10),e-n*Math.cos(2*s*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,s,n){var o=Math.PI/180;0>i-2*n&&(n=i/2),0>s-2*n&&(n=s/2),this.beginPath(),this.moveTo(t+n,e),this.lineTo(t+i-n,e),this.arc(t+i-n,e+n,n,270*o,360*o,!1),this.lineTo(t+i,e+s-n),this.arc(t+i-n,e+s-n,n,0,90*o,!1),this.lineTo(t+n,e+s),this.arc(t+n,e+s-n,n,90*o,180*o,!1),this.lineTo(t,e+n),this.arc(t+n,e+n,n,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,s){var n=.5522848,o=i/2*n,r=s/2*n,a=t+i,h=e+s,d=t+i/2,c=e+s/2;this.beginPath(),this.moveTo(t,c),this.bezierCurveTo(t,c-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,c-r,a,c),this.bezierCurveTo(a,c+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,c+r,t,c)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var n=1/3,o=i,r=s*n,a=.5522848,h=o/2*a,d=r/2*a,c=t+o,l=e+r,u=t+o/2,p=e+r/2,g=e+(s-r/2),f=e+s;this.beginPath(),this.moveTo(c,p),this.bezierCurveTo(c,p+d,u+h,l,u,l),this.bezierCurveTo(u-h,l,t,p+d,t,p),this.bezierCurveTo(t,p-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,c,p-d,c,p),this.lineTo(c,g),this.bezierCurveTo(c,g+d,u+h,f,u,f),this.bezierCurveTo(u-h,f,t,g+d,t,g),this.lineTo(t,p)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,s){var n=t-s*Math.cos(i),o=e-s*Math.sin(i),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),c=n+s/3*Math.cos(i-.5*Math.PI),l=o+s/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(c,l),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,s,n){n||(n=[10,5]),0==u&&(u=.001);var o=n.length;this.moveTo(t,e);for(var r=i-t,a=s-e,h=a/r,d=Math.sqrt(r*r+a*a),c=0,l=!0;d>=.1;){var u=n[c++%o];u>d&&(u=d);var p=Math.sqrt(u*u/(1+h*h));0>r&&(p=-p),t+=p,e+=h*p,this[l?"lineTo":"moveTo"](t,e),d-=u,l=!l}}),Node.prototype.resetCluster=function(){this.formationScale=void 0,this.clusterSize=1,this.containedNodes={},this.containedEdges={},this.clusterSessions=[]},Node.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),-1==this.dynamicEdges.indexOf(t)&&this.dynamicEdges.push(t),this.dynamicEdgesLength=this.dynamicEdges.length},Node.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&(this.edges.splice(e,1),this.dynamicEdges.splice(e,1)),this.dynamicEdgesLength=this.dynamicEdges.length},Node.prototype.setProperties=function(t,e){if(t){if(this.originalLabel=void 0,void 0!==t.id&&(this.id=t.id),void 0!==t.label&&(this.label=t.label,this.originalLabel=t.label),void 0!==t.title&&(this.title=t.title),void 0!==t.group&&(this.group=t.group),void 0!==t.x&&(this.x=t.x),void 0!==t.y&&(this.y=t.y),void 0!==t.value&&(this.value=t.value),void 0!==t.level&&(this.level=t.level,this.preassignedLevel=!0),void 0!==t.mass&&(this.mass=t.mass),void 0!==t.horizontalAlignLeft&&(this.horizontalAlignLeft=t.horizontalAlignLeft),void 0!==t.verticalAlignTop&&(this.verticalAlignTop=t.verticalAlignTop),void 0!==t.triggerFunction&&(this.triggerFunction=t.triggerFunction),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var s in i)i.hasOwnProperty(s)&&(this[s]=i[s])}if(void 0!==t.shape&&(this.shape=t.shape),void 0!==t.image&&(this.image=t.image),void 0!==t.radius&&(this.radius=t.radius),void 0!==t.color&&(this.color=util.parseColor(t.color)),void 0!==t.fontColor&&(this.fontColor=t.fontColor),void 0!==t.fontSize&&(this.fontSize=t.fontSize),void 0!==t.fontFace&&(this.fontFace=t.fontFace),void 0!==this.image&&""!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!==t.x&&!t.allowedToMoveX,this.yFixed=this.yFixed||void 0!==t.y&&!t.allowedToMoveY,this.radiusFixed=this.radiusFixed||void 0!==t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},Node.prototype.select=function(){this.selected=!0,this._reset()},Node.prototype.unselect=function(){this.selected=!1,this._reset()},Node.prototype.clearSizeCache=function(){this._reset()},Node.prototype._reset=function(){this.width=void 0,this.height=void 0},Node.prototype.getTitle=function(){return"function"==typeof this.title?this.title():this.title},Node.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var s=this.width/2,n=this.height/2,o=Math.sin(e)*s,r=Math.cos(e)*n;return s*n/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},Node.prototype._setForce=function(t,e){this.fx=t,this.fy=e},Node.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},Node.prototype.discreteStep=function(t){if(!this.xFixed){var e=this.damping*this.vx,i=(this.fx-e)/this.mass;this.vx+=i*t,this.x+=this.vx*t}if(!this.yFixed){var s=this.damping*this.vy,n=(this.fy-s)/this.mass;this.vy+=n*t,this.y+=this.vy*t}},Node.prototype.discreteStepLimited=function(t,e){if(this.xFixed)this.fx=0;else{var i=this.damping*this.vx,s=(this.fx-i)/this.mass;this.vx+=s*t,this.vx=Math.abs(this.vx)>e?this.vx>0?e:-e:this.vx,this.x+=this.vx*t}if(this.yFixed)this.fy=0;else{var n=this.damping*this.vy,o=(this.fy-n)/this.mass;this.vy+=o*t,this.vy=Math.abs(this.vy)>e?this.vy>0?e:-e:this.vy,this.y+=this.vy*t}},Node.prototype.isFixed=function(){return this.xFixed&&this.yFixed},Node.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t},Node.prototype.isSelected=function(){return this.selected},Node.prototype.getValue=function(){return this.value},Node.prototype.getDistance=function(t,e){var i=this.x-t,s=this.y-e;return Math.sqrt(i*i+s*s)},Node.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}this.baseRadiusValue=this.radius},Node.prototype.draw=function(){throw"Draw method not initialized for node"},Node.prototype.resize=function(){throw"Resize method not initialized for node"},Node.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},Node.prototype._resizeImage=function(){if(!this.width||!this.height){var t,e;if(this.value){this.radius=this.baseRadiusValue;var i=this.imageObj.height/this.imageObj.width;void 0!==i?(t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height):(t=0,e=0)}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e,this.growthIndicator=0,this.width>0&&this.height>0&&(this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-t)}},Node.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;if(0!=this.imageObj.width){if(this.clusterSize>1){var i=this.clusterSize>1?10:0;i*=this.graphScaleInv,i=Math.min(.2*this.width,i),t.globalAlpha=.5,t.drawImage(this.imageObj,this.left-i,this.top-i,this.width+2*i,this.height+2*i)}t.globalAlpha=1,t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2}else e=this.y;this._label(t,this.label,this.x,e,void 0,"top")},Node.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.growthIndicator=this.width-(i.width+2*e)}},Node.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.roundRect(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth,this.radius),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=i.width+2*e;this.width=s,this.height=s,this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-s}},Node.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.database(this.x-this.width/2-2*t.lineWidth,this.y-.5*this.height-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=Math.max(i.width,i.height)+2*e;this.radius=s/2,this.width=s,this.height=s,this.radius+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.radius-.5*s}},Node.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.circle(this.x,this.y,this.radius+2*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.width1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.ellipse(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.ellipse(this.left,this.top,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._drawDot=function(t){this._drawShape(t,"circle")},Node.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},Node.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},Node.prototype._drawSquare=function(t){this._drawShape(t,"square")},Node.prototype._drawStar=function(t){this._drawShape(t,"star")},Node.prototype._resizeShape=function(){if(!this.width){this.radius=this.baseRadiusValue;var t=2*this.radius;this.width=t,this.height=t,this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-t}},Node.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var i=2.5,s=2,n=2;switch(e){case"dot":n=2;break;case"square":n=2;break;case"triangle":n=3;break;case"triangleDown":n=3;break;case"star":n=4}t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?s:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t[e](this.x,this.y,this.radius+n*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?s:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.label&&this._label(t,this.label,this.x,this.y+this.height/2,void 0,"top")},Node.prototype._resizeText=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-(i.width+2*e)}},Node.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._label(t,this.label,this.x,this.y)},Node.prototype._label=function(t,e,i,s,n,o){if(e&&this.fontSize*this.graphScale>this.fontDrawThreshold){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=n||"center",t.textBaseline=o||"middle";for(var r=e.split("\n"),a=r.length,h=this.fontSize+4,d=s+(1-a)/2*h,c=0;a>c;c++)t.fillText(r[c],i,d),d+=h}},Node.prototype.getTextSize=function(t){if(void 0!==this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,s=0,n=0,o=e.length;o>n;n++)s=Math.max(s,t.measureText(e[n]).width);return{width:s,height:i}}return{width:0,height:0}},Node.prototype.inArea=function(){return void 0!==this.width?this.x+this.width*this.graphScaleInv>=this.canvasTopLeft.x&&this.x-this.width*this.graphScaleInv=this.canvasTopLeft.y&&this.y-this.height*this.graphScaleInv=this.canvasTopLeft.x&&this.x=this.canvasTopLeft.y&&this.yh}return!1},Edge.prototype._drawLine=function(t){if(t.strokeStyle=1==this.selected?this.color.highlight:this.color.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var e;if(this.label){if(1==this.smooth){var i=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),s=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));e={x:i,y:s}}else e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}}else{var n,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(n=a.x+a.width/2,o=a.y-r):(n=a.x+r,o=a.y-a.height/2),this._circle(t,n,o,r),e=this._pointOnCircle(n,o,r,.5),this._label(t,this.label,e.x,e.y)}},Edge.prototype._getLineWidth=function(){return 1==this.selected?Math.min(2*this.width,this.widthMax)*this.graphScaleInv:this.width*this.graphScaleInv},Edge.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,this.to.x,this.to.y):t.lineTo(this.to.x,this.to.y),t.stroke()},Edge.prototype._circle=function(t,e,i,s){t.beginPath(),t.arc(e,i,s,0,2*Math.PI,!1),t.stroke()},Edge.prototype._label=function(t,e,i,s){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontFill;var n=t.measureText(e).width,o=this.fontSize,r=i-n/2,a=s-o/2;t.fillRect(r,a,n,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},Edge.prototype._drawDashLine=function(t){if(t.strokeStyle=1==this.selected?this.color.highlight:this.color.color,t.lineWidth=this._getLineWidth(),void 0!==t.mozDash||void 0!==t.setLineDash){t.beginPath(),t.moveTo(this.from.x,this.from.y);var e=[0];e=void 0!==this.dash.length&&void 0!==this.dash.gap?[this.dash.length,this.dash.gap]:[5,5],"undefined"!=typeof t.setLineDash?(t.setLineDash(e),t.lineDashOffset=0):(t.mozDash=e,t.mozDashOffset=0),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,this.to.x,this.to.y):t.lineTo(this.to.x,this.to.y),t.stroke(),"undefined"!=typeof t.setLineDash?(t.setLineDash([0]),t.lineDashOffset=0):(t.mozDash=[0],t.mozDashOffset=0)}else t.beginPath(),t.lineCap="round",void 0!==this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!==this.dash.length&&void 0!==this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke();if(this.label){var i;if(1==this.smooth){var s=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),n=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));i={x:s,y:n}}else i=this._pointOnLine(.5);this._label(t,this.label,i.x,i.y)}},Edge.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},Edge.prototype._pointOnCircle=function(t,e,i,s){var n=2*(s-3/8)*Math.PI;return{x:t+i*Math.cos(n),y:e-i*Math.sin(n)}},Edge.prototype._drawArrowCenter=function(t){var e;if(1==this.selected?(t.strokeStyle=this.color.highlight,t.fillStyle=this.color.highlight):(t.strokeStyle=this.color.color,t.fillStyle=this.color.color),t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),s=10+5*this.width;if(1==this.smooth){var n=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),o=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));e={x:n,y:o}}else e=this._pointOnLine(.5);t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&this._label(t,this.label,e.x,e.y)}else{var r,a,h=.25*Math.max(100,this.length),d=this.from;d.width||d.resize(t),d.width>d.height?(r=d.x+.5*d.width,a=d.y-h):(r=d.x+h,a=d.y-.5*d.height),this._circle(t,r,a,h);var i=.2*Math.PI,s=10+5*this.width;e=this._pointOnCircle(r,a,h,.5),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(r,a,h,.5),this._label(t,this.label,e.x,e.y))}},Edge.prototype._drawArrow=function(t){1==this.selected?(t.strokeStyle=this.color.highlight,t.fillStyle=this.color.highlight):(t.strokeStyle=this.color.color,t.fillStyle=this.color.color),t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var s=this.to.x-this.from.x,n=this.to.y-this.from.y,o=Math.sqrt(s*s+n*n),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y;1==this.smooth&&(e=Math.atan2(this.to.y-this.via.y,this.to.x-this.via.x),s=this.to.x-this.via.x,n=this.to.y-this.via.y,o=Math.sqrt(s*s+n*n));var c,l,u=this.to.distanceToBorder(t,e),p=(o-u)/o;if(1==this.smooth?(c=(1-p)*this.via.x+p*this.to.x,l=(1-p)*this.via.y+p*this.to.y):(c=(1-p)*this.from.x+p*this.to.x,l=(1-p)*this.from.y+p*this.to.y),t.beginPath(),t.moveTo(h,d),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,c,l):t.lineTo(c,l),t.stroke(),i=10+5*this.width,t.arrow(c,l,e,i),t.fill(),t.stroke(),this.label){var g;if(1==this.smooth){var f=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),m=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));g={x:f,y:m}}else g=this._pointOnLine(.5);this._label(t,this.label,g.x,g.y)}}else{var v,y,_,b=this.from,w=.25*Math.max(100,this.length);b.width||b.resize(t),b.width>b.height?(v=b.x+.5*b.width,y=b.y-w,_={x:v,y:b.y,angle:.9*Math.PI}):(v=b.x+w,y=b.y-.5*b.height,_={x:b.x,y:y,angle:.6*Math.PI}),t.beginPath(),t.arc(v,y,w,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(_.x,_.y,_.angle,i),t.fill(),t.stroke(),this.label&&(g=this._pointOnCircle(v,y,w,.5),this._label(t,this.label,g.x,g.y))}},Edge.prototype._getDistanceToEdge=function(t,e,i,s,n,o){if(1==this.smooth){var r,a,h,d,c,l,u=1e9;for(r=0;10>r;r++)a=.1*r,h=Math.pow(1-a,2)*t+2*a*(1-a)*this.via.x+Math.pow(a,2)*i,d=Math.pow(1-a,2)*e+2*a*(1-a)*this.via.y+Math.pow(a,2)*s,c=Math.abs(n-h),l=Math.abs(o-d),u=Math.min(u,Math.sqrt(c*c+l*l));return u}var p=i-t,g=s-e,f=p*p+g*g,m=((n-t)*p+(o-e)*g)/f;m>1?m=1:0>m&&(m=0);var h=t+m*p,d=e+m*g,c=h-n,l=d-o;return Math.sqrt(c*c+l*l)},Edge.prototype.setScale=function(t){this.graphScaleInv=1/t},Edge.prototype.select=function(){this.selected=!0},Edge.prototype.unselect=function(){this.selected=!1},Edge.prototype.positionBezierNode=function(){null!==this.via&&(this.via.x=.5*(this.from.x+this.to.x),this.via.y=.5*(this.from.y+this.to.y))},Popup.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},Popup.prototype.setText=function(t){this.frame.innerHTML=t},Popup.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,s=this.frame.parentNode.clientHeight,n=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>s&&(o=s-e-this.padding),on&&(r=n-i-this.padding),rthis.constants.clustering.clusterThreshold&&1==this.constants.clustering.enabled&&this.clusterToFit(this.constants.clustering.reduceToNodes,!1),this._calculateForces())},_calculateForces:function(){this._calculateGravitationalForces(),this._calculateNodeForces(),1==this.constants.smoothCurves?this._calculateSpringForcesWithSupport():this._calculateSpringForces()},_updateCalculationNodes:function(){if(1==this.constants.smoothCurves){this.calculationNodes={},this.calculationNodeIndices=[];for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&(this.calculationNodes[t]=this.nodes[t]);var e=this.sectors.support.nodes;for(var i in e)e.hasOwnProperty(i)&&(this.edges.hasOwnProperty(e[i].parentEdgeId)?this.calculationNodes[i]=e[i]:e[i]._setForce(0,0));for(var s in this.calculationNodes)this.calculationNodes.hasOwnProperty(s)&&this.calculationNodeIndices.push(s)}else this.calculationNodes=this.nodes,this.calculationNodeIndices=this.nodeIndices},_calculateGravitationalForces:function(){var t,e,i,s,n,o=this.calculationNodes,r=this.constants.physics.centralGravity,a=0;for(n=0;n
Simulation Mode:
Barnes Hut
Repulsion
Hierarchical
Barnes Hut
gravitationalConstant
0
-20000
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
Repulsion
nodeDistance
0
300
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
Hierarchical
nodeDistance
0
300
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
direction
1
4
levelSeparation
1
500
nodeSpacing
1
500
Options:
',this.containerElement.parentElement.insertBefore(this.physicsConfiguration,this.containerElement),this.optionsDiv=document.createElement("div"),this.optionsDiv.style.fontSize="14px",this.optionsDiv.style.fontFamily="verdana",this.containerElement.parentElement.insertBefore(this.optionsDiv,this.containerElement);var e;e=document.getElementById("graph_BH_gc"),e.onchange=showValueOfRange.bind(this,"graph_BH_gc",-1,"physics_barnesHut_gravitationalConstant"),e=document.getElementById("graph_BH_cg"),e.onchange=showValueOfRange.bind(this,"graph_BH_cg",1,"physics_centralGravity"),e=document.getElementById("graph_BH_sc"),e.onchange=showValueOfRange.bind(this,"graph_BH_sc",1,"physics_springConstant"),e=document.getElementById("graph_BH_sl"),e.onchange=showValueOfRange.bind(this,"graph_BH_sl",1,"physics_springLength"),e=document.getElementById("graph_BH_damp"),e.onchange=showValueOfRange.bind(this,"graph_BH_damp",1,"physics_damping"),e=document.getElementById("graph_R_nd"),e.onchange=showValueOfRange.bind(this,"graph_R_nd",1,"physics_repulsion_nodeDistance"),e=document.getElementById("graph_R_cg"),e.onchange=showValueOfRange.bind(this,"graph_R_cg",1,"physics_centralGravity"),e=document.getElementById("graph_R_sc"),e.onchange=showValueOfRange.bind(this,"graph_R_sc",1,"physics_springConstant"),e=document.getElementById("graph_R_sl"),e.onchange=showValueOfRange.bind(this,"graph_R_sl",1,"physics_springLength"),e=document.getElementById("graph_R_damp"),e.onchange=showValueOfRange.bind(this,"graph_R_damp",1,"physics_damping"),e=document.getElementById("graph_H_nd"),e.onchange=showValueOfRange.bind(this,"graph_H_nd",1,"physics_hierarchicalRepulsion_nodeDistance"),e=document.getElementById("graph_H_cg"),e.onchange=showValueOfRange.bind(this,"graph_H_cg",1,"physics_centralGravity"),e=document.getElementById("graph_H_sc"),e.onchange=showValueOfRange.bind(this,"graph_H_sc",1,"physics_springConstant"),e=document.getElementById("graph_H_sl"),e.onchange=showValueOfRange.bind(this,"graph_H_sl",1,"physics_springLength"),e=document.getElementById("graph_H_damp"),e.onchange=showValueOfRange.bind(this,"graph_H_damp",1,"physics_damping"),e=document.getElementById("graph_H_direction"),e.onchange=showValueOfRange.bind(this,"graph_H_direction",t,"hierarchicalLayout_direction"),e=document.getElementById("graph_H_levsep"),e.onchange=showValueOfRange.bind(this,"graph_H_levsep",1,"hierarchicalLayout_levelSeparation"),e=document.getElementById("graph_H_nspac"),e.onchange=showValueOfRange.bind(this,"graph_H_nspac",1,"hierarchicalLayout_nodeSpacing");var i=document.getElementById("graph_physicsMethod1"),s=document.getElementById("graph_physicsMethod2"),n=document.getElementById("graph_physicsMethod3");s.checked=!0,this.constants.physics.barnesHut.enabled&&(i.checked=!0),this.constants.hierarchicalLayout.enabled&&(n.checked=!0);var o=document.getElementById("graph_toggleSmooth"),r=document.getElementById("graph_repositionNodes"),a=document.getElementById("graph_generateOptions");o.onclick=graphToggleSmoothCurves.bind(this),r.onclick=graphRepositionNodes.bind(this),a.onclick=graphGenerateOptions.bind(this),o.style.background=1==this.constants.smoothCurves?"#A4FF56":"#FF8532",switchConfigurations.apply(this),i.onchange=switchConfigurations.bind(this),s.onchange=switchConfigurations.bind(this),n.onchange=switchConfigurations.bind(this)}},_overWriteGraphConstants:function(t,e){var i=t.split("_");1==i.length?this.constants[i[0]]=e:2==i.length?this.constants[i[0]][i[1]]=e:3==i.length&&(this.constants[i[0]][i[1]][i[2]]=e)}},hierarchalRepulsionMixin={_calculateNodeForces:function(){var t,e,i,s,n,o,r,a,h,d,c=this.calculationNodes,l=this.calculationNodeIndices,u=5,p=.5*-u,g=this.constants.physics.hierarchicalRepulsion.nodeDistance,f=g;for(h=0;hi&&(o=m*i+u,0==i?i=.01:o/=i,s=t*o,n=e*o,r.fx-=s,r.fy-=n,a.fx+=s,a.fy+=n)}}},barnesHutMixin={_calculateNodeForces:function(){if(0!=this.constants.physics.barnesHut.gravitationalConstant){var t,e=this.calculationNodes,i=this.calculationNodeIndices,s=i.length;this._formBarnesHutTree(e,i);for(var n=this.barnesHutTree,o=0;s>o;o++)t=e[i[o]],this._getForceContribution(n.root.children.NW,t),this._getForceContribution(n.root.children.NE,t),this._getForceContribution(n.root.children.SW,t),this._getForceContribution(n.root.children.SE,t)}},_getForceContribution:function(t,e){if(t.childrenCount>0){var i,s,n;if(i=t.centerOfMass.x-e.x,s=t.centerOfMass.y-e.y,n=Math.sqrt(i*i+s*s),n*t.calcSize>this.constants.physics.barnesHut.theta){0==n&&(n=.1*Math.random(),i=n);var o=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(n*n*n),r=i*o,a=s*o;e.fx+=r,e.fy+=a}else if(4==t.childrenCount)this._getForceContribution(t.children.NW,e),this._getForceContribution(t.children.NE,e),this._getForceContribution(t.children.SW,e),this._getForceContribution(t.children.SE,e);else if(t.children.data.id!=e.id){0==n&&(n=.5*Math.random(),i=n);var o=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(n*n*n),r=i*o,a=s*o;e.fx+=r,e.fy+=a}}},_formBarnesHutTree:function(t,e){for(var i,s=e.length,n=Number.MAX_VALUE,o=Number.MAX_VALUE,r=-Number.MAX_VALUE,a=-Number.MAX_VALUE,h=0;s>h;h++){var d=t[e[h]].x,c=t[e[h]].y;n>d&&(n=d),d>r&&(r=d),o>c&&(o=c),c>a&&(a=c)}var l=Math.abs(r-n)-Math.abs(a-o);l>0?(o-=.5*l,a+=.5*l):(n+=.5*l,r-=.5*l);var u=1e-5,p=Math.max(u,Math.abs(r-n)),g=.5*p,f=.5*(n+r),m=.5*(o+a),v={root:{centerOfMass:{x:0,y:0},mass:0,range:{minX:f-g,maxX:f+g,minY:m-g,maxY:m+g},size:p,calcSize:1/p,children:{data:null},maxWidth:0,level:0,childrenCount:4}};for(this._splitBranch(v.root),h=0;s>h;h++)i=t[e[h]],this._placeInTree(v.root,i);this.barnesHutTree=v},_updateBranchMass:function(t,e){var i=t.mass+e.mass,s=1/i;t.centerOfMass.x=t.centerOfMass.x*t.mass+e.x*e.mass,t.centerOfMass.x*=s,t.centerOfMass.y=t.centerOfMass.y*t.mass+e.y*e.mass,t.centerOfMass.y*=s,t.mass=i;var n=Math.max(Math.max(e.height,e.radius),e.width);t.maxWidth=t.maxWidthe.x?t.children.NW.range.maxY>e.y?this._placeInRegion(t,e,"NW"):this._placeInRegion(t,e,"SW"):t.children.NW.range.maxY>e.y?this._placeInRegion(t,e,"NE"):this._placeInRegion(t,e,"SE")},_placeInRegion:function(t,e,i){switch(t.children[i].childrenCount){case 0:t.children[i].children.data=e,t.children[i].childrenCount=1,this._updateBranchMass(t.children[i],e);break;case 1:t.children[i].children.data.x==e.x&&t.children[i].children.data.y==e.y?(e.x+=Math.random(),e.y+=Math.random()):(this._splitBranch(t.children[i]),this._placeInTree(t.children[i],e));break;case 4:this._placeInTree(t.children[i],e)}},_splitBranch:function(t){var e=null;1==t.childrenCount&&(e=t.children.data,t.mass=0,t.centerOfMass.x=0,t.centerOfMass.y=0),t.childrenCount=4,t.children.data=null,this._insertRegion(t,"NW"),this._insertRegion(t,"NE"),this._insertRegion(t,"SW"),this._insertRegion(t,"SE"),null!=e&&this._placeInTree(t,e)},_insertRegion:function(t,e){var i,s,n,o,r=.5*t.size;switch(e){case"NW":i=t.range.minX,s=t.range.minX+r,n=t.range.minY,o=t.range.minY+r;break;case"NE":i=t.range.minX+r,s=t.range.maxX,n=t.range.minY,o=t.range.minY+r;break;case"SW":i=t.range.minX,s=t.range.minX+r,n=t.range.minY+r,o=t.range.maxY;break;case"SE":i=t.range.minX+r,s=t.range.maxX,n=t.range.minY+r,o=t.range.maxY}t.children[e]={centerOfMass:{x:0,y:0},mass:0,range:{minX:i,maxX:s,minY:n,maxY:o},size:.5*t.size,calcSize:2*t.calcSize,children:{data:null},maxWidth:0,level:t.level+1,childrenCount:0}},_drawTree:function(t,e){void 0!==this.barnesHutTree&&(t.lineWidth=1,this._drawBranch(this.barnesHutTree.root,t,e))},_drawBranch:function(t,e,i){void 0===i&&(i="#FF0000"),4==t.childrenCount&&(this._drawBranch(t.children.NW,e),this._drawBranch(t.children.NE,e),this._drawBranch(t.children.SE,e),this._drawBranch(t.children.SW,e)),e.strokeStyle=i,e.beginPath(),e.moveTo(t.range.minX,t.range.minY),e.lineTo(t.range.maxX,t.range.minY),e.stroke(),e.beginPath(),e.moveTo(t.range.maxX,t.range.minY),e.lineTo(t.range.maxX,t.range.maxY),e.stroke(),e.beginPath(),e.moveTo(t.range.maxX,t.range.maxY),e.lineTo(t.range.minX,t.range.maxY),e.stroke(),e.beginPath(),e.moveTo(t.range.minX,t.range.maxY),e.lineTo(t.range.minX,t.range.minY),e.stroke()}},repulsionMixin={_calculateNodeForces:function(){var t,e,i,s,n,o,r,a,h,d,c,l=this.calculationNodes,u=this.calculationNodeIndices,p=-2/3,g=4/3,f=this.constants.physics.repulsion.nodeDistance,m=f;for(d=0;di&&(r=.5*m>i?1:v*i+g,r*=0==o?1:1+o*this.constants.clustering.forceAmplification,r/=i,s=t*r,n=e*r,a.fx-=s,a.fy-=n,h.fx+=s,h.fy+=n)}}},HierarchicalLayoutMixin={_resetLevels:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];0==e.preassignedLevel&&(e.level=-1)}},_setupHierarchicalLayout:function(){if(1==this.constants.hierarchicalLayout.enabled){"RL"==this.constants.hierarchicalLayout.direction||"DU"==this.constants.hierarchicalLayout.direction?this.constants.hierarchicalLayout.levelSeparation*=-1:this.constants.hierarchicalLayout.levelSeparation=Math.abs(this.constants.hierarchicalLayout.levelSeparation);var t,e,i=0,s=!1,n=!1;for(e in this.nodes)this.nodes.hasOwnProperty(e)&&(t=this.nodes[e],-1!=t.level?s=!0:n=!0,is&&(o.xFixed=!1,o.x=i[o.level].minPos,r=!0):o.yFixed&&o.level>s&&(o.yFixed=!1,o.y=i[o.level].minPos,r=!0),1==r&&(i[o.level].minPos+=i[o.level].nodeSpacing,o.edges.length>1&&this._placeBranchNodes(o.edges,o.id,i,o.level))}},_setLevel:function(t,e,i){for(var s=0;st)&&(n.level=t,e.length>1&&this._setLevel(t+1,n.edges,n.id))}},_restoreNodes:function(){for(nodeId in this.nodes)this.nodes.hasOwnProperty(nodeId)&&(this.nodes[nodeId].xFixed=!1,this.nodes[nodeId].yFixed=!1)}},manipulationMixin={_clearManipulatorBar:function(){for(;this.manipulationDiv.hasChildNodes();)this.manipulationDiv.removeChild(this.manipulationDiv.firstChild)},_restoreOverloadedFunctions:function(){for(var t in this.cachedFunctions)this.cachedFunctions.hasOwnProperty(t)&&(this[t]=this.cachedFunctions[t])},_toggleEditMode:function(){this.editMode=!this.editMode;var t=document.getElementById("graph-manipulationDiv"),e=document.getElementById("graph-manipulation-closeDiv"),i=document.getElementById("graph-manipulation-editMode");1==this.editMode?(t.style.display="block",e.style.display="block",i.style.display="none",e.onclick=this._toggleEditMode.bind(this)):(t.style.display="none",e.style.display="none",i.style.display="block",e.onclick=null),this._createManipulatorBar()},_createManipulatorBar:function(){if(this.boundFunction&&this.off("select",this.boundFunction),this._restoreOverloadedFunctions(),this.freezeSimulation=!1,this.blockConnectingEdgeSelection=!1,this.forceAppendSelection=!1,1==this.editMode){for(;this.manipulationDiv.hasChildNodes();)this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);this.manipulationDiv.innerHTML=""+this.constants.labels.add+""+this.constants.labels.link+"",1==this._getSelectedNodeCount()&&this.triggerFunctions.edit&&(this.manipulationDiv.innerHTML+=""+this.constants.labels.editNode+""),0==this._selectionIsEmpty()&&(this.manipulationDiv.innerHTML+=""+this.constants.labels["delete"]+"");var t=document.getElementById("graph-manipulate-addNode");t.onclick=this._createAddNodeToolbar.bind(this);var e=document.getElementById("graph-manipulate-connectNode");if(e.onclick=this._createAddEdgeToolbar.bind(this),1==this._getSelectedNodeCount()&&this.triggerFunctions.edit){var i=document.getElementById("graph-manipulate-editNode");i.onclick=this._editNode.bind(this)}if(0==this._selectionIsEmpty()){var s=document.getElementById("graph-manipulate-delete");s.onclick=this._deleteSelected.bind(this)}var n=document.getElementById("graph-manipulation-closeDiv");n.onclick=this._toggleEditMode.bind(this),this.boundFunction=this._createManipulatorBar.bind(this),this.on("select",this.boundFunction)}else{this.editModeDiv.innerHTML=""+this.constants.labels.edit+"";var o=document.getElementById("graph-manipulate-editModeButton");o.onclick=this._toggleEditMode.bind(this)}},_createAddNodeToolbar:function(){this._clearManipulatorBar(),this.boundFunction&&this.off("select",this.boundFunction),this.manipulationDiv.innerHTML=""+this.constants.labels.back+" "+this.constants.labels.addDescription+"";var t=document.getElementById("graph-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._addNode.bind(this),this.on("select",this.boundFunction)},_createAddEdgeToolbar:function(){this._clearManipulatorBar(),this._unselectAll(!0),this.freezeSimulation=!0,this.boundFunction&&this.off("select",this.boundFunction),this._unselectAll(),this.forceAppendSelection=!1,this.blockConnectingEdgeSelection=!0,this.manipulationDiv.innerHTML=""+this.constants.labels.back+" "+this.constants.labels.linkDescription+"";var t=document.getElementById("graph-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._handleConnect.bind(this),this.on("select",this.boundFunction),this.cachedFunctions._handleTouch=this._handleTouch,this.cachedFunctions._handleOnRelease=this._handleOnRelease,this._handleTouch=this._handleConnect,this._handleOnRelease=this._finishConnect,this._redraw()},_handleConnect:function(t){if(0==this._getSelectedNodeCount()){var e=this._getNodeAt(t);null!=e&&(e.clusterSize>1?alert("Cannot create edges to a cluster."):(this._selectObject(e,!1),this.sectors.support.nodes.targetNode=new Node({id:"targetNode"},{},{},this.constants),this.sectors.support.nodes.targetNode.x=e.x,this.sectors.support.nodes.targetNode.y=e.y,this.sectors.support.nodes.targetViaNode=new Node({id:"targetViaNode"},{},{},this.constants),this.sectors.support.nodes.targetViaNode.x=e.x,this.sectors.support.nodes.targetViaNode.y=e.y,this.sectors.support.nodes.targetViaNode.parentEdgeId="connectionEdge",this.edges.connectionEdge=new Edge({id:"connectionEdge",from:e.id,to:this.sectors.support.nodes.targetNode.id},this,this.constants),this.edges.connectionEdge.from=e,this.edges.connectionEdge.connected=!0,this.edges.connectionEdge.smooth=!0,this.edges.connectionEdge.selected=!0,this.edges.connectionEdge.to=this.sectors.support.nodes.targetNode,this.edges.connectionEdge.via=this.sectors.support.nodes.targetViaNode,this.cachedFunctions._handleOnDrag=this._handleOnDrag,this._handleOnDrag=function(t){var e=this._getPointer(t.gesture.center);this.sectors.support.nodes.targetNode.x=this._canvasToX(e.x),this.sectors.support.nodes.targetNode.y=this._canvasToY(e.y),this.sectors.support.nodes.targetViaNode.x=.5*(this._canvasToX(e.x)+this.edges.connectionEdge.from.x),this.sectors.support.nodes.targetViaNode.y=this._canvasToY(e.y)},this.moving=!0,this.start()))}},_finishConnect:function(t){if(1==this._getSelectedNodeCount()){this._handleOnDrag=this.cachedFunctions._handleOnDrag,delete this.cachedFunctions._handleOnDrag;var e=this.edges.connectionEdge.fromId;delete this.edges.connectionEdge,delete this.sectors.support.nodes.targetNode,delete this.sectors.support.nodes.targetViaNode;var i=this._getNodeAt(t);null!=i&&(i.clusterSize>1?alert("Cannot create edges to a cluster."):(this._createEdge(e,i.id),this._createManipulatorBar())),this._unselectAll()}},_addNode:function(){if(this._selectionIsEmpty()&&1==this.editMode){var t=this._pointerToPositionObject(this.pointerPosition),e={id:util.randomUUID(),x:t.left,y:t.top,label:"new",allowedToMoveX:!0,allowedToMoveY:!0};if(this.triggerFunctions.add)if(2==this.triggerFunctions.add.length){var i=this;this.triggerFunctions.add(e,function(t){i.nodesData.add(t),i._createManipulatorBar(),i.moving=!0,i.start()})}else alert(this.constants.labels.addError),this._createManipulatorBar(),this.moving=!0,this.start();else this.nodesData.add(e),this._createManipulatorBar(),this.moving=!0,this.start()}},_createEdge:function(t,e){if(1==this.editMode){var i={from:t,to:e};if(this.triggerFunctions.connect)if(2==this.triggerFunctions.connect.length){var s=this;this.triggerFunctions.connect(i,function(t){s.edgesData.add(t),s.moving=!0,s.start()})}else alert(this.constants.labels.linkError),this.moving=!0,this.start();else this.edgesData.add(i),this.moving=!0,this.start()}},_editNode:function(){if(this.triggerFunctions.edit&&1==this.editMode){var t=this._getSelectedNode(),e={id:t.id,label:t.label,group:t.group,shape:t.shape,color:{background:t.color.background,border:t.color.border,highlight:{background:t.color.highlight.background,border:t.color.highlight.border}}};if(2==this.triggerFunctions.edit.length){var i=this;this.triggerFunctions.edit(e,function(t){i.nodesData.update(t),i._createManipulatorBar(),i.moving=!0,i.start()})}else alert(this.constants.labels.editError)}else alert(this.constants.labels.editBoundError)},_deleteSelected:function(){if(!this._selectionIsEmpty()&&1==this.editMode)if(this._clusterInSelection())alert(this.constants.labels.deleteClusterError);else{var t=this.getSelectedNodes(),e=this.getSelectedEdges();if(this.triggerFunctions.delete){var i=this,s={nodes:t,edges:e};(this.triggerFunctions.delete.length=2)?this.triggerFunctions.delete(s,function(t){i.edgesData.remove(t.edges),i.nodesData.remove(t.nodes),this._unselectAll(),i.moving=!0,i.start()}):alert(this.constants.labels.deleteError)}else this.edgesData.remove(e),this.nodesData.remove(t),this._unselectAll(),this.moving=!0,this.start()}}},SectorMixin={_putDataInSector:function(){this.sectors.active[this._sector()].nodes=this.nodes,this.sectors.active[this._sector()].edges=this.edges,this.sectors.active[this._sector()].nodeIndices=this.nodeIndices},_switchToSector:function(t,e){void 0===e||"active"==e?this._switchToActiveSector(t):this._switchToFrozenSector(t)},_switchToActiveSector:function(t){this.nodeIndices=this.sectors.active[t].nodeIndices,this.nodes=this.sectors.active[t].nodes,this.edges=this.sectors.active[t].edges},_switchToSupportSector:function(){this.nodeIndices=this.sectors.support.nodeIndices,this.nodes=this.sectors.support.nodes,this.edges=this.sectors.support.edges},_switchToFrozenSector:function(t){this.nodeIndices=this.sectors.frozen[t].nodeIndices,this.nodes=this.sectors.frozen[t].nodes,this.edges=this.sectors.frozen[t].edges},_loadLatestSector:function(){this._switchToSector(this._sector())},_sector:function(){return this.activeSector[this.activeSector.length-1]},_previousSector:function(){if(this.activeSector.length>1)return this.activeSector[this.activeSector.length-2];throw new TypeError("there are not enough sectors in the this.activeSector array.")},_setActiveSector:function(t){this.activeSector.push(t)},_forgetLastSector:function(){this.activeSector.pop()},_createNewSector:function(t){this.sectors.active[t]={nodes:{},edges:{},nodeIndices:[],formationScale:this.scale,drawingNode:void 0},this.sectors.active[t].drawingNode=new Node({id:t,color:{background:"#eaefef",border:"495c5e"}},{},{},this.constants),this.sectors.active[t].drawingNode.clusterSize=2},_deleteActiveSector:function(t){delete this.sectors.active[t]},_deleteFrozenSector:function(t){delete this.sectors.frozen[t]},_freezeSector:function(t){this.sectors.frozen[t]=this.sectors.active[t],this._deleteActiveSector(t)},_activateSector:function(t){this.sectors.active[t]=this.sectors.frozen[t],this._deleteFrozenSector(t)},_mergeThisWithFrozen:function(t){for(var e in this.nodes)this.nodes.hasOwnProperty(e)&&(this.sectors.frozen[t].nodes[e]=this.nodes[e]);for(var i in this.edges)this.edges.hasOwnProperty(i)&&(this.sectors.frozen[t].edges[i]=this.edges[i]);for(var s=0;s1?this[t](s[0],s[1]):this[t](e)}this._loadLatestSector()},_doInSupportSector:function(t,e){if(void 0===e)this._switchToSupportSector(),this[t]();else{this._switchToSupportSector();var i=Array.prototype.splice.call(arguments,1);i.length>1?this[t](i[0],i[1]):this[t](e)}this._loadLatestSector()},_doInAllFrozenSectors:function(t,e){if(void 0===e)for(var i in this.sectors.frozen)this.sectors.frozen.hasOwnProperty(i)&&(this._switchToFrozenSector(i),this[t]());else for(var i in this.sectors.frozen)if(this.sectors.frozen.hasOwnProperty(i)){this._switchToFrozenSector(i);var s=Array.prototype.splice.call(arguments,1);s.length>1?this[t](s[0],s[1]):this[t](e)}this._loadLatestSector()},_doInAllSectors:function(t,e){var i=Array.prototype.splice.call(arguments,1);void 0===e?(this._doInAllActiveSectors(t),this._doInAllFrozenSectors(t)):i.length>1?(this._doInAllActiveSectors(t,i[0],i[1]),this._doInAllFrozenSectors(t,i[0],i[1])):(this._doInAllActiveSectors(t,e),this._doInAllFrozenSectors(t,e))},_clearNodeIndexList:function(){var t=this._sector();this.sectors.active[t].nodeIndices=[],this.nodeIndices=this.sectors.active[t].nodeIndices},_drawSectorNodes:function(t,e){var i,s=1e9,n=-1e9,o=1e9,r=-1e9;for(var a in this.sectors[e])if(this.sectors[e].hasOwnProperty(a)&&void 0!==this.sectors[e][a].drawingNode){this._switchToSector(a,e),s=1e9,n=-1e9,o=1e9,r=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),o>i.x-.5*i.width&&(o=i.x-.5*i.width),ri.y-.5*i.height&&(s=i.y-.5*i.height),nt&&s>n;)n%3==0?(this.forceAggregateHubs(!0),this.normalizeClusterLevels()):this.increaseClusterLevel(),i=this.nodeIndices.length,n+=1;n>0&&1==e&&this.repositionNodes(),this._updateCalculationNodes()},openCluster:function(t){var e=this.moving;if(t.clusterSize>this.constants.clustering.sectorThreshold&&this._nodeInActiveArea(t)&&("default"!=this._sector()||1!=this.nodeIndices.length)){this._addSector(t);for(var i=0;this.nodeIndices.lengthi;)this.decreaseClusterLevel(),i+=1}else this._expandClusterNode(t,!1,!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this._updateCalculationNodes(),this.updateLabels();this.moving!=e&&this.start()},updateClustersDefault:function(){1==this.constants.clustering.enabled&&this.updateClusters(0,!1,!1)},increaseClusterLevel:function(){this.updateClusters(-1,!1,!0)},decreaseClusterLevel:function(){this.updateClusters(1,!1,!0)},updateClusters:function(t,e,i,s){var n=this.moving,o=this.nodeIndices.length;this.previousScale>this.scale&&0==t&&this._collapseSector(),this.previousScale>this.scale||-1==t?this._formClusters(i):(this.previousScalethis.scale||-1==t)&&(this._aggregateHubs(i),this._updateNodeIndexList()),(this.previousScale>this.scale||-1==t)&&(this.handleChains(),this._updateNodeIndexList()),this.previousScale=this.scale,this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.lengththis.constants.clustering.chainThreshold&&this._reduceAmountOfChains(1-this.constants.clustering.chainThreshold/t)},_aggregateHubs:function(t){this._getHubSize(),this._formClustersByHub(t,!1)},forceAggregateHubs:function(t){var e=this.moving,i=this.nodeIndices.length;this._aggregateHubs(!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.length!=i&&(this.clusterSession+=1),(0==t||void 0===t)&&this.moving!=e&&this.start()},_openClustersBySize:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];1==e.inView()&&(e.width*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientWidth||e.height*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientHeight)&&this.openCluster(e)}},_openClusters:function(t,e){for(var i=0;i1&&(t.clusterSizei)){var r=o.from,a=o.to;o.to.mass>o.from.mass&&(r=o.to,a=o.from),1==a.dynamicEdgesLength?this._addToCluster(r,a,!1):1==r.dynamicEdgesLength&&this._addToCluster(a,r,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],s=i.toId==e.id?this.nodes[i.fromId]:this.nodes[i.toId];e.id!=s.id&&(s.mass>e.mass?this._addToCluster(s,e,!0):this._addToCluster(e,s,!0))}}},_clusterToSmallestNeighbour:function(t){for(var e=-1,i=null,s=0;sn.clusterSessions.length&&(e=n.clusterSessions.length,i=n)}null!=n&&void 0!==this.nodes[n.id]&&this._addToCluster(n,t,!0)},_formClustersByHub:function(t,e){for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&this._formClusterFromHub(this.nodes[i],t,e)},_formClusterFromHub:function(t,e,i,s){if(void 0===s&&(s=0),t.dynamicEdgesLength>=this.hubThreshold&&0==i||t.dynamicEdgesLength==this.hubThreshold&&1==i){for(var n,o,r,a=this.constants.clustering.clusterEdgeThreshold/this.scale,h=!1,d=[],c=t.dynamicEdges.length,l=0;c>l;l++)d.push(t.dynamicEdges[l].id);if(0==e)for(h=!1,l=0;c>l;l++){var u=this.edges[d[l]];if(void 0!==u&&u.connected&&u.toId!=u.fromId&&(n=u.to.x-u.from.x,o=u.to.y-u.from.y,r=Math.sqrt(n*n+o*o),a>r)){h=!0;break}}if(!e&&h||e)for(l=0;c>l;l++)if(u=this.edges[d[l]],void 0!==u){var p=this.nodes[u.fromId==t.id?u.toId:u.fromId];p.dynamicEdges.length<=this.hubThreshold+s&&p.id!=t.id&&this._addToCluster(t,p,e)}}},_addToCluster:function(t,e,i){t.containedNodes[e.id]=e;for(var s=0;s1)for(var s=0;s1&&(e.label="[".concat(String(e.clusterSize),"]"))}for(t in this.nodes)this.nodes.hasOwnProperty(t)&&(e=this.nodes[t],1==e.clusterSize&&(e.label=void 0!==e.originalLabel?e.originalLabel:String(e.id)))},normalizeClusterLevels:function(){var t=0,e=1e9,i=0;for(var s in this.nodes)this.nodes.hasOwnProperty(s)&&(i=this.nodes[s].clusterSessions.length,i>t&&(t=i),e>i&&(e=i));if(t-e>this.constants.clustering.clusterLevelDifference){var n=this.nodeIndices.length,o=t-this.constants.clustering.clusterLevelDifference;for(var s in this.nodes)this.nodes.hasOwnProperty(s)&&this.nodes[s].clusterSessions.lengths&&(s=o.dynamicEdgesLength),t+=o.dynamicEdgesLength,e+=Math.pow(o.dynamicEdgesLength,2),i+=1}t/=i,e/=i;var r=e-Math.pow(t,2),a=Math.sqrt(r);this.hubThreshold=Math.floor(t+2*a),this.hubThreshold>s&&(this.hubThreshold=s)},_reduceAmountOfChains:function(t){this.hubThreshold=2;var e=Math.floor(this.nodeIndices.length*t);for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&e>0&&(this._formClusterFromHub(this.nodes[i],!0,!0,1),e-=1)},_getChainFraction:function(){var t=0,e=0;for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&(2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&(t+=1),e+=1);return t/e}},SelectionMixin={_getNodesOverlappingWith:function(t,e){var i=this.nodes;for(var s in i)i.hasOwnProperty(s)&&i[s].isOverlappingWith(t)&&e.push(s)},_getAllNodesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getNodesOverlappingWith",t,e),e},_pointerToPositionObject:function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y);return{left:e,top:i,right:e,bottom:i}},_getNodeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllNodesOverlappingWith(e);return i.length>0?this.nodes[i[i.length-1]]:null},_getEdgesOverlappingWith:function(t,e){var i=this.edges;for(var s in i)i.hasOwnProperty(s)&&i[s].isOverlappingWith(t)&&e.push(s)},_getAllEdgesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getEdgesOverlappingWith",t,e),e},_getEdgeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllEdgesOverlappingWith(e);return i.length>0?this.edges[i[i.length-1]]:null},_addToSelection:function(t){t instanceof Node?this.selectionObj.nodes[t.id]=t:this.selectionObj.edges[t.id]=t},_removeFromSelection:function(t){t instanceof Node?delete this.selectionObj.nodes[t.id]:delete this.selectionObj.edges[t.id]},_unselectAll:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&this.selectionObj.nodes[e].unselect();for(var i in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(i)&&this.selectionObj.edges[i].unselect();this.selectionObj={nodes:{},edges:{}},0==t&&this.emit("select",this.getSelection())},_unselectClusters:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&this.selectionObj.nodes[e].clusterSize>1&&(this.selectionObj.nodes[e].unselect(),this._removeFromSelection(this.selectionObj.nodes[e]));0==t&&this.emit("select",this.getSelection())},_getSelectedNodeCount:function(){var t=0;for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&(t+=1);return t},_getSelectedNode:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t))return this.selectionObj.nodes[t];return null},_getSelectedEdgeCount:function(){var t=0;for(var e in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(e)&&(t+=1);return t},_getSelectedObjectCount:function(){var t=0;for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&(t+=1);for(var i in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(i)&&(t+=1);return t},_selectionIsEmpty:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t))return!1;for(var e in this.selectionObj.edges)if(this.selectionObj.edges.hasOwnProperty(e))return!1;return!0},_clusterInSelection:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t)&&this.selectionObj.nodes[t].clusterSize>1)return!0;return!1},_selectConnectedEdges:function(t){for(var e=0;ee;e++){s=t[e];var n=this.nodes[s];if(!n)throw new RangeError('Node with id "'+s+'" not found');this._selectObject(n,!0,!0)}this.redraw()},_updateSelection:function(){for(var t in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(t)&&(this.nodes.hasOwnProperty(t)||delete this.selectionObj.nodes[t]);for(var e in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(e)&&(this.edges.hasOwnProperty(e)||delete this.selectionObj.edges[e])}},NavigationMixin={_cleanNavigation:function(){var t=document.getElementById("graph-navigation_wrapper");null!=t&&this.containerElement.removeChild(t),document.onmouseup=null},_loadNavigationElements:function(){this._cleanNavigation(),this.navigationDivs={};var t=["up","down","left","right","zoomIn","zoomOut","zoomExtends"],e=["_moveUp","_moveDown","_moveLeft","_moveRight","_zoomIn","_zoomOut","zoomExtent"];this.navigationDivs.wrapper=document.createElement("div"),this.navigationDivs.wrapper.id="graph-navigation_wrapper",this.navigationDivs.wrapper.style.position="absolute",this.navigationDivs.wrapper.style.width=this.frame.canvas.clientWidth+"px",this.navigationDivs.wrapper.style.height=this.frame.canvas.clientHeight+"px",this.containerElement.insertBefore(this.navigationDivs.wrapper,this.frame);for(var i=0;it.x&&(s=t.x),nt.y&&(e=t.y),i=this.constants.clustering.initialMaxNodes?49.07548/(n+142.05338)+91444e-8:12.662/(n+7.4147)+.0964822:1==this.constants.clustering.enabled&&n>=this.constants.clustering.initialMaxNodes?77.5271985/(n+187.266146)+476710517e-13:30.5062972/(n+19.93597763)+.08413486;var o=Math.min(this.frame.canvas.clientWidth/600,this.frame.canvas.clientHeight/600);i*=o}else{var r=1.1*(Math.abs(s.minX)+Math.abs(s.maxX)),a=1.1*(Math.abs(s.minY)+Math.abs(s.maxY)),h=this.frame.canvas.clientWidth/r,d=this.frame.canvas.clientHeight/a;i=d>=h?h:d}i>1&&(i=1),this._setScale(i),this._centerGraph(s),0==e&&(this.moving=!0,this.start())},Graph.prototype._updateNodeIndexList=function(){this._clearNodeIndexList();for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodeIndices.push(t)},Graph.prototype.setData=function(t,e){if(void 0===e&&(e=!1),t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var i=vis.util.DOTToGraph(t.dot);return void this.setData(i)}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this._putDataInSector(),e||(this.stabilize&&this._stabilize(),this.start())},Graph.prototype.setOptions=function(t){if(t){var e;if(void 0!==t.width&&(this.width=t.width),void 0!==t.height&&(this.height=t.height),void 0!==t.stabilize&&(this.stabilize=t.stabilize),void 0!==t.selectable&&(this.selectable=t.selectable),void 0!==t.smoothCurves&&(this.constants.smoothCurves=t.smoothCurves),void 0!==t.freezeForStabilization&&(this.constants.freezeForStabilization=t.freezeForStabilization),void 0!==t.configurePhysics&&(this.constants.configurePhysics=t.configurePhysics),void 0!==t.stabilizationIterations&&(this.constants.stabilizationIterations=t.stabilizationIterations),void 0!==t.labels)for(e in t.labels)t.labels.hasOwnProperty(e)&&(this.constants.labels[e]=t.labels[e]);if(t.onAdd&&(this.triggerFunctions.add=t.onAdd),t.onEdit&&(this.triggerFunctions.edit=t.onEdit),t.onConnect&&(this.triggerFunctions.connect=t.onConnect),t.onDelete&&(this.triggerFunctions.delete=t.onDelete),t.physics){if(t.physics.barnesHut){this.constants.physics.barnesHut.enabled=!0;for(e in t.physics.barnesHut)t.physics.barnesHut.hasOwnProperty(e)&&(this.constants.physics.barnesHut[e]=t.physics.barnesHut[e])}if(t.physics.repulsion){this.constants.physics.barnesHut.enabled=!1;for(e in t.physics.repulsion)t.physics.repulsion.hasOwnProperty(e)&&(this.constants.physics.repulsion[e]=t.physics.repulsion[e])}}if(t.hierarchicalLayout){this.constants.hierarchicalLayout.enabled=!0;for(e in t.hierarchicalLayout)t.hierarchicalLayout.hasOwnProperty(e)&&(this.constants.hierarchicalLayout[e]=t.hierarchicalLayout[e])}else void 0!==t.hierarchicalLayout&&(this.constants.hierarchicalLayout.enabled=!1);if(t.clustering){this.constants.clustering.enabled=!0;for(e in t.clustering)t.clustering.hasOwnProperty(e)&&(this.constants.clustering[e]=t.clustering[e])}else void 0!==t.clustering&&(this.constants.clustering.enabled=!1);if(t.navigation){this.constants.navigation.enabled=!0;for(e in t.navigation)t.navigation.hasOwnProperty(e)&&(this.constants.navigation[e]=t.navigation[e])}else void 0!==t.navigation&&(this.constants.navigation.enabled=!1);if(t.keyboard){this.constants.keyboard.enabled=!0;for(e in t.keyboard)t.keyboard.hasOwnProperty(e)&&(this.constants.keyboard[e]=t.keyboard[e])}else void 0!==t.keyboard&&(this.constants.keyboard.enabled=!1);if(t.dataManipulation){this.constants.dataManipulation.enabled=!0;for(e in t.dataManipulation)t.dataManipulation.hasOwnProperty(e)&&(this.constants.dataManipulation[e]=t.dataManipulation[e])}else void 0!==t.dataManipulation&&(this.constants.dataManipulation.enabled=!1);if(t.edges){for(e in t.edges)t.edges.hasOwnProperty(e)&&"object"!=typeof t.edges[e]&&(this.constants.edges[e]=t.edges[e]);void 0!==t.edges.color&&(util.isString(t.edges.color)?(this.constants.edges.color={},this.constants.edges.color.color=t.edges.color,this.constants.edges.color.highlight=t.edges.color):(void 0!==t.edges.color.color&&(this.constants.edges.color.color=t.edges.color.color),void 0!==t.edges.color.highlight&&(this.constants.edges.color.highlight=t.edges.color.highlight))),t.edges.fontColor||void 0!==t.edges.color&&(util.isString(t.edges.color)?this.constants.edges.fontColor=t.edges.color:void 0!==t.edges.color.color&&(this.constants.edges.fontColor=t.edges.color.color)),t.edges.dash&&(void 0!==t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!==t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!==t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=util.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var s=t.groups[i];this.groups.add(i,s)}if(t.tooltip){for(e in t.tooltip)t.tooltip.hasOwnProperty(e)&&(this.constants.tooltip[e]=t.tooltip[e]);t.tooltip.color&&(this.constants.tooltip.color=util.parseColor(t.tooltip.color))}}this._loadPhysicsSystem(),this._loadNavigationControls(),this._loadManipulationSystem(),this._configureSmoothCurves(),this._createKeyBinds(),this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1),this._redraw()},Graph.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.style.zIndex="1",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=Hammer(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("doubletap",e._onDoubleTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("release",e._onRelease.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("DOMMouseScroll",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},Graph.prototype._createKeyBinds=function(){var t=this;this.mousetrap=mousetrap,this.mousetrap.reset(),1==this.constants.keyboard.enabled&&(this.mousetrap.bind("up",this._moveUp.bind(t),"keydown"),this.mousetrap.bind("up",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("down",this._moveDown.bind(t),"keydown"),this.mousetrap.bind("down",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("left",this._moveLeft.bind(t),"keydown"),this.mousetrap.bind("left",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("right",this._moveRight.bind(t),"keydown"),this.mousetrap.bind("right",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("=",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("=",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("-",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("-",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("[",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("[",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("]",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("]",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pageup",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("pageup",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pagedown",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("pagedown",this._stopZoom.bind(t),"keyup")),1==this.constants.dataManipulation.enabled&&(this.mousetrap.bind("escape",this._createManipulatorBar.bind(t)),this.mousetrap.bind("del",this._deleteSelected.bind(t)))},Graph.prototype._getPointer=function(t){return{x:t.pageX-vis.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-vis.util.getAbsoluteTop(this.frame.canvas)}},Graph.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.center),this.drag.pinched=!1,this.pinch.scale=this._getScale(),this._handleTouch(this.drag.pointer)},Graph.prototype._onDragStart=function(){this._handleDragStart()},Graph.prototype._handleDragStart=function(){var t=this.drag,e=this._getNodeAt(t.pointer);if(t.dragging=!0,t.selection=[],t.translation=this._getTranslation(),t.nodeId=null,null!=e){t.nodeId=e.id,e.isSelected()||this._selectObject(e,!1);for(var i in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(i)){var s=this.selectionObj.nodes[i],n={id:s.id,node:s,x:s.x,y:s.y,xFixed:s.xFixed,yFixed:s.yFixed};s.xFixed=!0,s.yFixed=!0,t.selection.push(n)}}},Graph.prototype._onDrag=function(t){this._handleOnDrag(t)},Graph.prototype._handleOnDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.center),i=this,s=this.drag,n=s.selection;if(n&&n.length){var o=e.x-s.pointer.x,r=e.y-s.pointer.y;n.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+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}}},Graph.prototype._onDragEnd=function(){this.drag.dragging=!1;var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},Graph.prototype._onTap=function(t){console.log(t);var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleTap(e)},Graph.prototype._onDoubleTap=function(t){var e=this._getPointer(t.gesture.center);this._handleDoubleTap(e)},Graph.prototype._onHold=function(t){var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleOnHold(e)},Graph.prototype._onRelease=function(t){var e=this._getPointer(t.gesture.center);this._handleOnRelease(e)},Graph.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},Graph.prototype._zoom=function(t,e){var i=this._getScale();1e-5>t&&(t=1e-5),t>10&&(t=10);var s=this._getTranslation(),n=t/i,o=(1-n)*e.x+s.x*n,r=(1-n)*e.y+s.y*n;return this.areaCenter={x:this._canvasToX(e.x),y:this._canvasToY(e.y)},this._setScale(t),this._setTranslation(o,r),this.updateClustersDefault(),this._redraw(),t},Graph.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){var i=this._getScale(),s=e/10;0>e&&(s/=1-s),i*=1+s;var n=util.fakeGesture(this,t),o=this._getPointer(n.center);this._zoom(i,o)}t.preventDefault()},Graph.prototype._onMouseMoveTitle=function(t){var e=util.fakeGesture(this,t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var s=this,n=function(){s._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.drag.dragging||(this.popupTimer=setTimeout(n,this.constants.tooltip.delay))},Graph.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},s=this.popupNode;if(void 0==this.popupNode){var n=this.nodes;for(e in n)if(n.hasOwnProperty(e)){var o=n[e];if(void 0!==o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0===this.popupNode){var 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!=s){var h=this;h.popup||(h.popup=new Popup(h.frame,h.constants.tooltip)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},Graph.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},Graph.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight,void 0!==this.manipulationDiv&&(this.manipulationDiv.style.width=this.frame.canvas.clientWidth+"px"),void 0!==this.navigationDivs&&void 0!==this.navigationDivs.wrapper&&(this.navigationDivs.wrapper.style.width=this.frame.canvas.clientWidth+"px",this.navigationDivs.wrapper.style.height=this.frame.canvas.clientHeight+"px"),this.emit("resize",{width:this.frame.canvas.width,height:this.frame.canvas.height})},Graph.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof DataSet||t instanceof DataView)this.nodesData=t;else if(t instanceof Array)this.nodesData=new DataSet,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new DataSet}if(e&&util.forEach(this.nodesListeners,function(t,i){e.off(i,t)}),this.nodes={},this.nodesData){var i=this;util.forEach(this.nodesListeners,function(t,e){i.nodesData.on(e,t)});var s=this.nodesData.getIds();this._addNodes(s)}this._updateSelection()},Graph.prototype._addNodes=function(t){for(var e,i=0,s=t.length;s>i;i++){e=t[i];var n=this.nodesData.get(e),o=new Node(n,this.images,this.groups,this.constants);if(this.nodes[e]=o,!(0!=o.xFixed&&0!=o.yFixed||null!==o.x&&null!==o.y)){var r=1*t.length,a=2*Math.PI*Math.random();0==o.xFixed&&(o.x=r*Math.cos(a)),0==o.yFixed&&(o.y=r*Math.sin(a))}this.moving=!0}this._updateNodeIndexList(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes(),this._reconnectEdges(),this._updateValueRange(this.nodes),this.updateLabels()},Graph.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,s=0,n=t.length;n>s;s++){var o=t[s],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new Node(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(e)},Graph.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,s=t.length;s>i;i++){var n=t[i];delete e[n]}this._updateNodeIndexList(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes(),this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},Graph.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof DataSet||t instanceof DataView)this.edgesData=t;else if(t instanceof Array)this.edgesData=new DataSet,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new DataSet}if(e&&util.forEach(this.edgesListeners,function(t,i){e.off(i,t)}),this.edges={},this.edgesData){var i=this;util.forEach(this.edgesListeners,function(t,e){i.edgesData.on(e,t)});var s=this.edgesData.getIds();this._addEdges(s)}this._reconnectEdges()},Graph.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],r=e[o];r&&r.disconnect();var a=i.get(o,{showInternalIds:!0});e[o]=new Edge(a,this,this.constants)}this.moving=!0,this._updateValueRange(e),this._createBezierNodes(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes()},Graph.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new Edge(r,this,this.constants),this.edges[o]=a)}this._createBezierNodes(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this.moving=!0,this._updateValueRange(e)},Graph.prototype._removeEdges=function(t){for(var e=this.edges,i=0,s=t.length;s>i;i++){var n=t[i],o=e[n];o&&(null!=o.via&&delete this.sectors.support.nodes[o.via.id],o.disconnect(),delete e[n])}this.moving=!0,this._updateValueRange(e),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes()},Graph.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var s=i[t];s.from=null,s.to=null,s.connect()}},Graph.prototype._updateValueRange=function(t){var e,i=void 0,s=void 0;for(e in t)if(t.hasOwnProperty(e)){var n=t[e].getValue();void 0!==n&&(i=void 0===i?n:Math.min(n,i),s=void 0===s?n:Math.max(n,s))}if(void 0!==i&&void 0!==s)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,s)},Graph.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},Graph.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this.canvasTopLeft={x:this._canvasToX(0),y:this._canvasToY(0)},this.canvasBottomRight={x:this._canvasToX(this.frame.canvas.clientWidth),y:this._canvasToY(this.frame.canvas.clientHeight)},this._doInAllSectors("_drawAllSectorNodes",t),this._doInAllSectors("_drawEdges",t),this._doInAllSectors("_drawNodes",t,!1),t.restore()},Graph.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},Graph.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},Graph.prototype._setScale=function(t){this.scale=t},Graph.prototype._getScale=function(){return this.scale},Graph.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},Graph.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},Graph.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},Graph.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},Graph.prototype._drawNodes=function(t,e){void 0===e&&(e=!1);var i=this.nodes,s=[];for(var n in i)i.hasOwnProperty(n)&&(i[n].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight),i[n].isSelected()?s.push(n):(i[n].inArea()||e)&&i[n].draw(t));for(var o=0,r=s.length;r>o;o++)(i[s[o]].inArea()||e)&&i[s[o]].draw(t)},Graph.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var s=e[i];s.setScale(this.scale),s.connected&&e[i].draw(t)}},Graph.prototype._stabilize=function(){1==this.constants.freezeForStabilization&&this._freezeDefinedNodes();for(var t=0;this.moving&&t0)for(t in i)i.hasOwnProperty(t)&&(i[t].discreteStepLimited(e,this.constants.maxVelocity),s=!0);else for(t in i)i.hasOwnProperty(t)&&(i[t].discreteStep(e),s=!0);if(1==s){var n=this.constants.minVelocity/Math.max(this.scale,.05);this.moving=n>.5*this.constants.maxVelocity?!0:this._isMoving(n)}},Graph.prototype._physicsTick=function(){this.freezeSimulation||this.moving&&(this._doInAllActiveSectors("_initializeForceCalculation"),this._doInAllActiveSectors("_discreteStepNodes"),this.constants.smoothCurves&&this._doInSupportSector("_discreteStepNodes"),this._findCenter(this._getRange()))},Graph.prototype._animationStep=function(){this.timer=void 0,this._handleNavigation(),this.start();var t=Date.now(),e=1;this._physicsTick();for(var i=Date.now()-t;is;++s)i[s].apply(this,e)}return this},i.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks[t]||[]},i.prototype.hasListeners=function(t){return!!this.listeners(t).length}},{}],3:[function(t,e){!function(t,i){"use strict";function s(){if(!n.READY){n.event.determineEventTypes();for(var t in n.gestures)n.gestures.hasOwnProperty(t)&&n.detection.register(n.gestures[t]);n.event.onTouch(n.DOCUMENT,n.EVENT_MOVE,n.detection.detect),n.event.onTouch(n.DOCUMENT,n.EVENT_END,n.detection.detect),n.READY=!0}}var n=function(t,e){return new n.Instance(t,e||{})};n.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},n.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,n.HAS_TOUCHEVENTS="ontouchstart"in t,n.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,n.NO_MOUSEEVENTS=n.HAS_TOUCHEVENTS&&navigator.userAgent.match(n.MOBILE_REGEX),n.EVENT_TYPES={},n.DIRECTION_DOWN="down",n.DIRECTION_LEFT="left",n.DIRECTION_UP="up",n.DIRECTION_RIGHT="right",n.POINTER_MOUSE="mouse",n.POINTER_TOUCH="touch",n.POINTER_PEN="pen",n.EVENT_START="start",n.EVENT_MOVE="move",n.EVENT_END="end",n.DOCUMENT=document,n.plugins={},n.READY=!1,n.Instance=function(t,e){var i=this;return s(),this.element=t,this.enabled=!0,this.options=n.utils.extend(n.utils.extend({},n.defaults),e||{}),this.options.stop_browser_behavior&&n.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),n.event.onTouch(t,n.EVENT_START,function(t){i.enabled&&n.detection.startDetect(i,t)}),this},n.Instance.prototype={on:function(t,e){for(var i=t.split(" "),s=0;s0&&e==n.EVENT_END?e=n.EVENT_MOVE:c||(e=n.EVENT_END),c||null===o?o=h:h=o,i.call(n.detection,s.collectEventData(t,e,h)),n.HAS_POINTEREVENTS&&e==n.EVENT_END&&(c=n.PointerEvent.updatePointer(e,h))),c||(o=null,r=!1,a=!1,n.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=n.HAS_POINTEREVENTS?n.PointerEvent.getEvents():n.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],n.EVENT_TYPES[n.EVENT_START]=t[0],n.EVENT_TYPES[n.EVENT_MOVE]=t[1],n.EVENT_TYPES[n.EVENT_END]=t[2]},getTouchList:function(t){return n.HAS_POINTEREVENTS?n.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var s=this.getTouchList(i,e),o=n.POINTER_TOUCH;return(i.type.match(/mouse/)||n.PointerEvent.matchType(n.POINTER_MOUSE,i))&&(o=n.POINTER_MOUSE),{center:n.utils.getCenter(s),timeStamp:(new Date).getTime(),target:i.target,touches:s,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return n.detection.stopDetect()}}}},n.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==n.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[n.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==n.POINTER_MOUSE,i[n.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==n.POINTER_TOUCH,i[n.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==n.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},n.utils={extend:function(t,e,s){for(var n in e)t[n]!==i&&s||(t[n]=e[n]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],s=0,n=t.length;n>s;s++)e.push(t[s].pageX),i.push(t[s].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,s=e.pageX-t.pageX;return 180*Math.atan2(i,s)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),s=Math.abs(t.pageY-e.pageY);return i>=s?t.pageX-e.pageX>0?n.DIRECTION_LEFT:n.DIRECTION_RIGHT:t.pageY-e.pageY>0?n.DIRECTION_UP:n.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,s=e.pageY-t.pageY;return Math.sqrt(i*i+s*s)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==n.DIRECTION_UP||t==n.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,s=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var n=0;ni;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==n.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=n.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,s=t.touches.length;s>i;i++)e.touches.push(n.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=n.utils.getVelocity(o,r,a);return n.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:n.utils.getDistance(e.center,t.center),angle:n.utils.getAngle(e.center,t.center),direction:n.utils.getDirection(e.center,t.center),scale:n.utils.getScale(e.touches,t.touches),rotation:n.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),n.utils.extend(n.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},n.gestures=n.gestures||{},n.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case n.EVENT_START:clearTimeout(this.timer),n.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==n.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case n.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case n.EVENT_END:clearTimeout(this.timer)}}},n.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==n.EVENT_END){var i=n.detection.previous,s=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},n.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(n.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),void(this.triggered=!1);if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case n.EVENT_START:this.triggered=!1;break;case n.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case n.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},n.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==n.POINTER_MOUSE?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==n.EVENT_START&&e.trigger(this.name,t)))}},n.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==n.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=n:(t.Hammer=n,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return n}))}(this)},{}],4:[function(t,e){(function(i){function s(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function n(t,e){return function(i){return u(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){E(t),d(this,t)}function h(t){var e=y(t),i=e.year||0,s=e.month||0,n=e.week||0,o=e.day||0,r=e.hour||0,a=e.minute||0,h=e.second||0,d=e.millisecond||0;this._milliseconds=+d+1e3*h+6e4*a+36e5*r,this._days=+o+7*n,this._months=+s+12*i,this._data={},this._bubble()}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function c(t){var e,i={};for(e in t)t.hasOwnProperty(e)&&ye.hasOwnProperty(e)&&(i[e]=t[e]);return i}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e,i){for(var s=""+Math.abs(t),n=t>=0;s.lengths;s++)(i&&t[s]!==e[s]||!i&&b(t[s])!==b(e[s]))&&r++;return r+o}function v(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=qe[t]||Xe[e]||e}return t}function y(t){var e,i,s={};for(i in t)t.hasOwnProperty(i)&&(e=v(i),e&&(s[e]=t[i]));return s}function _(t){var e,s;if(0===t.indexOf("week"))e=7,s="day";else{if(0!==t.indexOf("month"))return;e=12,s="month"}oe[t]=function(n,o){var r,a,h=oe.fn._lang[t],d=[];if("number"==typeof n&&(o=n,n=i),a=function(t){var e=oe().utc().set(s,t);return h.call(oe.fn._lang,e,n||"")},null!=o)return a(o);for(r=0;e>r;r++)d.push(a(r));return d}}function b(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function w(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function S(t){return x(t)?366:365}function x(t){return t%4===0&&t%100!==0||t%400===0}function E(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[le]<0||t._a[le]>11?le:t._a[ue]<1||t._a[ue]>w(t._a[ce],t._a[le])?ue:t._a[pe]<0||t._a[pe]>23?pe:t._a[ge]<0||t._a[ge]>59?ge:t._a[fe]<0||t._a[fe]>59?fe:t._a[me]<0||t._a[me]>999?me:-1,t._pf._overflowDayOfYear&&(ce>e||e>ue)&&(e=ue),t._pf.overflow=e)}function T(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function D(t){return t?t.toLowerCase().replace("_","-"):t}function C(t,e){return e._isUTC?oe(t).zone(e._offset||0):oe(t).local()}function M(t,e){return e.abbr=t,ve[t]||(ve[t]=new r),ve[t].set(e),ve[t]}function I(t){delete ve[t]}function N(e){var i,s,n,o,r=0,a=function(e){if(!ve[e]&&_e)try{t("./lang/"+e)}catch(i){}return ve[e]};if(!e)return oe.fn._lang;if(!g(e)){if(s=a(e))return s;e=[e]}for(;r0;){if(s=a(o.slice(0,i).join("-")))return s;if(n&&n.length>=i&&m(o,n,!0)>=i-1)break;i--}r++}return oe.fn._lang}function O(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function L(t){var e,i,s=t.match(xe);for(e=0,i=s.length;i>e;e++)s[e]=Qe[s[e]]?Qe[s[e]]:O(s[e]);return function(n){var o="";for(e=0;i>e;e++)o+=s[e]instanceof Function?s[e].call(n,t):s[e];return o}}function k(t,e){return t.isValid()?(e=P(e,t.lang()),Ze[e]||(Ze[e]=L(e)),Ze[e](t)):t.lang().invalidDate()}function P(t,e){function i(t){return e.longDateFormat(t)||t}var s=5;for(Ee.lastIndex=0;s>=0&&Ee.test(t);)t=t.replace(Ee,i),Ee.lastIndex=0,s-=1;return t}function A(t,e){var i,s=e._strict;switch(t){case"DDDD":return ze;case"YYYY":case"GGGG":case"gggg":return s?Fe:Ce;case"Y":case"G":case"g":return He;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return s?Re:Me;case"S":if(s)return Pe;case"SS":if(s)return Ae;case"SSS":if(s)return ze;case"DDD":return De;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ne;case"a":case"A":return N(e._l)._meridiemParse;case"X":return ke;case"Z":case"ZZ":return Oe;case"T":return Le;case"SSSS":return Ie;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return s?Ae:Te;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Te;default:return i=new RegExp(B(W(t.replace("\\","")),"i"))}}function z(t){t=t||"";var e=t.match(Oe)||[],i=e[e.length-1]||[],s=(i+"").match(je)||["-",0,0],n=+(60*s[1])+b(s[2]);return"+"===s[0]?-n:n}function F(t,e,i){var s,n=i._a;switch(t){case"M":case"MM":null!=e&&(n[le]=b(e)-1);break;case"MMM":case"MMMM":s=N(i._l).monthsParse(e),null!=s?n[le]=s:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(n[ue]=b(e));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=b(e));break;case"YY":n[ce]=b(e)+(b(e)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":n[ce]=b(e);break;case"a":case"A":i._isPm=N(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":n[pe]=b(e);break;case"m":case"mm":n[ge]=b(e);break;case"s":case"ss":n[fe]=b(e);break;case"S":case"SS":case"SSS":case"SSSS":n[me]=b(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=z(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function R(t){var e,i,s,n,o,r,a,h,d,c,l=[];if(!t._d){for(s=Y(t),t._w&&null==t._a[ue]&&null==t._a[le]&&(o=function(e){var i=parseInt(e,10);return e?e.length<3?i>68?1900+i:2e3+i:i:null==t._a[ce]?oe().weekYear():t._a[ce]},r=t._w,null!=r.GG||null!=r.W||null!=r.E?a=J(o(r.GG),r.W||1,r.E,4,1):(h=N(t._l),d=null!=r.d?Z(r.d,h):null!=r.e?parseInt(r.e,10)+h._week.dow:0,c=parseInt(r.w,10)||1,null!=r.d&&dS(n)&&(t._pf._overflowDayOfYear=!0),i=X(n,0,t._dayOfYear),t._a[le]=i.getUTCMonth(),t._a[ue]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=l[e]=s[e];for(;7>e;e++)t._a[e]=l[e]=null==t._a[e]?2===e?1:0:t._a[e];l[pe]+=b((t._tzm||0)/60),l[ge]+=b((t._tzm||0)%60),t._d=(t._useUTC?X:q).apply(null,l)}}function H(t){var e;t._d||(e=y(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],R(t))}function Y(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function G(t){t._a=[],t._pf.empty=!0;var e,i,s,n,o,r=N(t._l),a=""+t._i,h=a.length,d=0;for(s=P(t._f,r).match(xe)||[],e=0;e0&&t._pf.unusedInput.push(o),a=a.slice(a.indexOf(i)+i.length),d+=i.length),Qe[n]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(n),F(n,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(n);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[pe]<12&&(t._a[pe]+=12),t._isPm===!1&&12===t._a[pe]&&(t._a[pe]=0),R(t),E(t)}function W(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,s,n){return e||i||s||n})}function B(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function j(t){var e,i,n,o,r;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(o=0;or)&&(n=r,i=e));d(t,i||e)}function V(t){var e,i,s=t._i,n=Ye.exec(s);if(n){for(t._pf.iso=!0,e=0,i=We.length;i>e;e++)if(We[e][1].exec(s)){t._f=We[e][0]+(n[6]||" ");break}for(e=0,i=Be.length;i>e;e++)if(Be[e][1].exec(s)){t._f+=Be[e][0];break}s.match(Oe)&&(t._f+="Z"),G(t)}else t._d=new Date(s)}function U(t){var e=t._i,s=be.exec(e);e===i?t._d=new Date:s?t._d=new Date(+s[1]):"string"==typeof e?V(t):g(e)?(t._a=e.slice(0),R(t)):f(e)?t._d=new Date(+e):"object"==typeof e?H(t):t._d=new Date(e)}function q(t,e,i,s,n,o,r){var a=new Date(t,e,i,s,n,o,r);return 1970>t&&a.setFullYear(t),a}function X(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function Z(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function K(t,e,i,s,n){return n.relativeTime(e||1,!!i,t,s)}function $(t,e,i){var s=de(Math.abs(t)/1e3),n=de(s/60),o=de(n/60),r=de(o/24),a=de(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",de(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,K.apply({},h)}function Q(t,e,i){var s,n=i-e,o=i-t.day();return o>n&&(o-=7),n-7>o&&(o+=7),s=oe(t).add("d",o),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function J(t,e,i,s,n){var o,r,a=X(t,0,1).getUTCDay();return i=null!=i?i:n,o=n-a+(a>s?7:0)-(n>a?7:0),r=7*(e-1)+(i-n)+o+1,{year:r>0?t:t-1,dayOfYear:r>0?r:S(t-1)+r}}function te(t){var e=t._i,i=t._f;return null===e?oe.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=N().preparse(e)),oe.isMoment(e)?(t=c(e),t._d=new Date(+e._d)):i?g(i)?j(t):G(t):U(t),new a(t))}function ee(t,e){oe.fn[t]=oe.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),oe.updateOffset(this),this):this._d["get"+i+e]()}}function ie(t){oe.duration.fn[t]=function(){return this._data[t]}}function se(t,e){oe.duration.fn["as"+t]=function(){return+this/e}}function ne(t){var e=!1,i=oe;"undefined"==typeof ender&&(t?(he.moment=function(){return!e&&console&&console.warn&&(e=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),i.apply(null,arguments)},d(he.moment,i)):he.moment=oe)}for(var oe,re,ae="2.5.1",he=this,de=Math.round,ce=0,le=1,ue=2,pe=3,ge=4,fe=5,me=6,ve={},ye={_isAMomentObject:null,_i:null,_f:null,_l:null,_strict:null,_isUTC:null,_offset:null,_pf:null,_lang:null},_e="undefined"!=typeof e&&e.exports&&"undefined"!=typeof t,be=/^\/?Date\((\-?\d+)/i,we=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Se=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,xe=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,Ee=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Te=/\d\d?/,De=/\d{1,3}/,Ce=/\d{1,4}/,Me=/[+\-]?\d{1,6}/,Ie=/\d+/,Ne=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Oe=/Z|[\+\-]\d\d:?\d\d/gi,Le=/T/i,ke=/[\+\-]?\d+(\.\d{1,3})?/,Pe=/\d/,Ae=/\d\d/,ze=/\d{3}/,Fe=/\d{4}/,Re=/[+-]?\d{6}/,He=/[+-]?\d+/,Ye=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ge="YYYY-MM-DDTHH:mm:ssZ",We=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],Be=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],je=/([\+\-]|\d\d)/gi,Ve="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Ue={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},qe={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Xe={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Ze={},Ke="DDD w W M D d".split(" "),$e="M D H h m s w W".split(" "),Qe={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 u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},YYYYYY:function(){var t=this.year(),e=t>=0?"+":"-";return e+u(Math.abs(t),6)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return u(this.weekYear(),4)},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return u(this.isoWeekYear(),4)},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return b(this.milliseconds()/100)},SS:function(){return u(b(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},SSSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(b(t/60),2)+":"+u(b(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(b(t/60),2)+u(b(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},Je=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Ke.length;)re=Ke.pop(),Qe[re+"o"]=o(Qe[re],re);for(;$e.length;)re=$e.pop(),Qe[re+re]=n(Qe[re],2);for(Qe.DDDD=n(Qe.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,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=oe.utc([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=oe([2e3,1]).day(e),s="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,s){var n=this._relativeTime[i];return"function"==typeof n?n(t,e,i,s):n.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return Q(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),oe=function(t,e,n,o){var r;return"boolean"==typeof n&&(o=n,n=i),r={},r._isAMomentObject=!0,r._i=t,r._f=e,r._l=n,r._strict=o,r._isUTC=!1,r._pf=s(),te(r)},oe.utc=function(t,e,n,o){var r;return"boolean"==typeof n&&(o=n,n=i),r={},r._isAMomentObject=!0,r._useUTC=!0,r._isUTC=!0,r._l=n,r._i=t,r._f=e,r._strict=o,r._pf=s(),te(r).utc()},oe.unix=function(t){return oe(1e3*t)},oe.duration=function(t,e){var i,s,n,o=t,r=null;return oe.isDuration(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(r=we.exec(t))?(i="-"===r[1]?-1:1,o={y:0,d:b(r[ue])*i,h:b(r[pe])*i,m:b(r[ge])*i,s:b(r[fe])*i,ms:b(r[me])*i}):(r=Se.exec(t))&&(i="-"===r[1]?-1:1,n=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},o={y:n(r[2]),M:n(r[3]),d:n(r[4]),h:n(r[5]),m:n(r[6]),s:n(r[7]),w:n(r[8])}),s=new h(o),oe.isDuration(t)&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},oe.version=ae,oe.defaultFormat=Ge,oe.updateOffset=function(){},oe.lang=function(t,e){var i;return t?(e?M(D(t),e):null===e?(I(t),t="en"):ve[t]||N(t),i=oe.duration.fn._lang=oe.fn._lang=N(t),i._abbr):oe.fn._lang._abbr},oe.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),N(t)},oe.isMoment=function(t){return t instanceof a||null!=t&&t.hasOwnProperty("_isAMomentObject")},oe.isDuration=function(t){return t instanceof h
-},re=Je.length-1;re>=0;--re)_(Je[re]);for(oe.normalizeUnits=function(t){return v(t)},oe.invalid=function(t){var e=oe.utc(0/0);return null!=t?d(e._pf,t):e._pf.userInvalidated=!0,e},oe.parseZone=function(t){return oe(t).parseZone()},d(oe.fn=a.prototype,{clone:function(){return oe(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=oe(this).utc();return 00:!1},parsingFlags:function(){return d({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=k(this,t||oe.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?oe.duration(+e,t):oe.duration(t,e),p(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?oe.duration(+e,t):oe.duration(t,e),p(this,i,-1),this},diff:function(t,e,i){var s,n,o=C(t,this),r=6e4*(this.zone()-o.zone());return e=v(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+o.daysInMonth()),n=12*(this.year()-o.year())+(this.month()-o.month()),n+=(this-oe(this).startOf("month")-(o-oe(o).startOf("month")))/s,n-=6e4*(this.zone()-oe(this).startOf("month").zone()-(o.zone()-oe(o).startOf("month").zone()))/s,"year"===e&&(n/=12)):(s=this-o,n="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-r)/864e5:"week"===e?(s-r)/6048e5:s),i?n:l(n)},from:function(t,e){return oe.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(oe(),t)},calendar:function(){var t=C(oe(),this).startOf("day"),e=this.diff(t,"days",!0),i=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(this.lang().calendar(i,this))},isLeapYear:function(){return x(this.year())},isDST:function(){return this.zone()+oe(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+oe(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+C(t,this).startOf(e)},min:function(t){return t=oe.apply(null,arguments),this>t?this:t},max:function(t){return t=oe.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=z(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&p(this,oe.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?oe(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return w(this.year(),this.month())},dayOfYear:function(t){var e=de((oe(this).startOf("day")-oe(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(t){var e=Q(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=Q(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=Q(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=v(t),this[t]()},set:function(t,e){return t=v(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===i?this._lang:(this._lang=N(t),this)}}),re=0;re-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function o(t,e){return t.sort().join(",")===e.sort().join(",")}function r(t){t=t||{};var e,i=!1;for(e in C)t[e]?i=!0:C[e]=0;i||(I=!1)}function a(t,e,i,s,n){var r,a,h=[];if(!T[t])return[];for("keyup"==i&&u(t)&&(e=[t]),r=0;r95&&112>t||w.hasOwnProperty(t)&&(_[w[t]]=t)}return _}function f(t,e,i){return i||(i=g()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function m(t,e,i,n){C[t]=0,n||(n=f(e[0],[]));var o,a=function(){I=n,++C[t],p()},h=function(t){d(i,t),"keyup"!==n&&(M=s(t)),setTimeout(r,10)};for(o=0;o1)return m(t,d,e,i);for(h="+"===t?["+"]:t.split("+"),o=0;o":".","?":"/","|":"\\"},E={option:"alt",command:"meta","return":"enter",escape:"esc"},T={},D={},C={},M=!1,I=!1,N=1;20>N;++N)w[111+N]="f"+N;for(N=0;9>=N;++N)w[N+96]=N;i(document,"keypress",l),i(document,"keydown",l),i(document,"keyup",l);var O={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),D[t+":"+i]=e,this},unbind:function(t,e){return D[t+":"+e]&&(delete D[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return D[t+":"+e](),this},reset:function(){return T={},D={},this}};e.exports=O},{}]},{},[1])(1)});
\ No newline at end of file
+!function(t){if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.vis=t()}}(function(){var define,module,exports;return function t(e,i,s){function n(a,r){if(!i[a]){if(!e[a]){var h="function"==typeof require&&require;if(!r&&h)return h(a,!0);if(o)return o(a,!0);throw new Error("Cannot find module '"+a+"'")}var d=i[a]={exports:{}};e[a][0].call(d.exports,function(t){var i=e[a][1][t];return n(i?i:t)},d,d.exports,t,e,i,s)}return i[a].exports}for(var o="function"==typeof require&&require,a=0;ai;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,s,n;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),a=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=new Array(a),n=0;a>n;){var r,h;n in o&&(r=o[n],h=t.call(i,r,n,o),s[n]=h),n++}return s}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var s=[],n=arguments[1],o=0;i>o;o++)if(o in e){var a=e[o];t.call(n,a,o,e)&&s.push(a)}return s}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],s=i.length;return function(n){if("object"!=typeof n&&"function"!=typeof n||null===n)throw new TypeError("Object.keys called on non-object");var o=[];for(var a in n)t.call(n,a)&&o.push(a);if(e)for(var r=0;s>r;r++)t.call(n,i[r])&&o.push(i[r]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},n=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,n.prototype=new s,n}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},n=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,n.prototype=new s,n});var util={};util.isNumber=function(t){return t instanceof Number||"number"==typeof t},util.isString=function(t){return t instanceof String||"string"==typeof t},util.isDate=function(t){if(t instanceof Date)return!0;if(util.isString(t)){var e=ASPDateRegex.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},util.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},util.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},util.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var s=arguments[e];for(var n in s)s.hasOwnProperty(n)&&void 0!==s[n]&&(t[n]=s[n])}return t},util.equalArray=function(t,e){if(t.length!=e.length)return!1;for(var i=1,s=t.length;s>i;i++)if(t[i]!=e[i])return!1;return!0},util.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(util.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(moment.isMoment(t))return new Date(t.valueOf());if(util.isString(t))return i=ASPDateRegex.exec(t),i?new Date(Number(i[1])):moment(t).toDate();throw new Error("Cannot convert object of type "+util.getType(t)+" to type Date");case"Moment":if(util.isNumber(t))return moment(t);if(t instanceof Date)return moment(t.valueOf());if(moment.isMoment(t))return moment(t);if(util.isString(t))return i=ASPDateRegex.exec(t),moment(i?Number(i[1]):t);throw new Error("Cannot convert object of type "+util.getType(t)+" to type Date");case"ISODate":if(util.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(moment.isMoment(t))return t.toDate().toISOString();if(util.isString(t))return i=ASPDateRegex.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+util.getType(t)+" to type ISODate");case"ASPDate":if(util.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(util.isString(t)){i=ASPDateRegex.exec(t);var s;return s=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+s+")/"}throw new Error("Cannot convert object of type "+util.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+util.getType(t)+' to type "'+e+'"')}};var ASPDateRegex=/^\/?Date\((\-?\d+)/i;util.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},util.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetLeft,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetLeft,s-=n.scrollLeft,n=n.offsetParent;return s},util.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetTop,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetTop,s-=n.scrollTop,n=n.offsetParent;return s},util.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,s=document.body;return e+(i&&i.scrollTop||s&&s.scrollTop||0)-(i&&i.clientTop||s&&s.clientTop||0)},util.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,s=document.body;return e+(i&&i.scrollLeft||s&&s.scrollLeft||0)-(i&&i.clientLeft||s&&s.clientLeft||0)},util.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},util.removeClassName=function(t,e){var i=t.className.split(" "),s=i.indexOf(e);-1!=s&&(i.splice(s,1),t.className=i.join(" "))},util.forEach=function(t,e){var i,s;if(t instanceof Array)for(i=0,s=t.length;s>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},util.toArray=function(t){var e=[];for(var i in t)t.hasOwnProperty(i)&&e.push(t[i]);return e},util.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},util.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},util.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},util.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},util.fakeGesture=function(t,e){var i=null,s=Hammer.event.collectEventData(this,i,e);return isNaN(s.center.pageX)&&(s.center.pageX=e.pageX),isNaN(s.center.pageY)&&(s.center.pageY=e.pageY),s},util.option={},util.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},util.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null
+},util.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},util.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),util.isString(t)?t:util.isNumber(t)?t+"px":e||null},util.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},util.GiveDec=function GiveDec(Hex){var Value;return Value="A"==Hex?10:"B"==Hex?11:"C"==Hex?12:"D"==Hex?13:"E"==Hex?14:"F"==Hex?15:eval(Hex)},util.GiveHex=function(t){var e;return e=10==t?"A":11==t?"B":12==t?"C":13==t?"D":14==t?"E":15==t?"F":""+t},util.parseColor=function(t){var e;if(util.isString(t))if(util.isValidHex(t)){var i=util.hexToHSV(t),s={h:i.h,s:.45*i.s,v:Math.min(1,1.05*i.v)},n={h:i.h,s:Math.min(1,1.25*i.v),v:.6*i.v},o=util.HSVToHex(n.h,n.h,n.v),a=util.HSVToHex(s.h,s.s,s.v);e={background:t,border:o,highlight:{background:a,border:o}}}else e={background:t,border:t,highlight:{background:t,border:t}};else e={},e.background=t.background||"white",e.border=t.border||e.background,util.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border);return e},util.hexToRGB=function(t){t=t.replace("#","").toUpperCase();var e=util.GiveDec(t.substring(0,1)),i=util.GiveDec(t.substring(1,2)),s=util.GiveDec(t.substring(2,3)),n=util.GiveDec(t.substring(3,4)),o=util.GiveDec(t.substring(4,5)),a=util.GiveDec(t.substring(5,6)),r=16*e+i,h=16*s+n,i=16*o+a;return{r:r,g:h,b:i}},util.RGBToHex=function(t,e,i){var s=util.GiveHex(Math.floor(t/16)),n=util.GiveHex(t%16),o=util.GiveHex(Math.floor(e/16)),a=util.GiveHex(e%16),r=util.GiveHex(Math.floor(i/16)),h=util.GiveHex(i%16),d=s+n+o+a+r+h;return"#"+d},util.RGBToHSV=function(t,e,i){t/=255,e/=255,i/=255;var s=Math.min(t,Math.min(e,i)),n=Math.max(t,Math.max(e,i));if(s==n)return{h:0,s:0,v:s};var o=t==s?e-i:i==s?t-e:i-t,a=t==s?3:i==s?1:5,r=60*(a-o/(n-s))/360,h=(n-s)/n,d=n;return{h:r,s:h,v:d}},util.HSVToRGB=function(t,e,i){var s,n,o,a=Math.floor(6*t),r=6*t-a,h=i*(1-e),d=i*(1-r*e),c=i*(1-(1-r)*e);switch(a%6){case 0:s=i,n=c,o=h;break;case 1:s=d,n=i,o=h;break;case 2:s=h,n=i,o=c;break;case 3:s=h,n=d,o=i;break;case 4:s=c,n=h,o=i;break;case 5:s=i,n=h,o=d}return{r:Math.floor(255*s),g:Math.floor(255*n),b:Math.floor(255*o)}},util.HSVToHex=function(t,e,i){var s=util.HSVToRGB(t,e,i);return util.RGBToHex(s.r,s.g,s.b)},util.hexToHSV=function(t){var e=util.hexToRGB(t);return util.RGBToHSV(e.r,e.g,e.b)},util.isValidHex=function(t){var e=/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(t);return e},util.copyObject=function(t,e){for(var i in t)t.hasOwnProperty(i)&&("object"==typeof t[i]?(e[i]={},util.copyObject(t[i],e[i])):e[i]=t[i])},DataSet.prototype.on=function(t,e){var i=this.subscribers[t];i||(i=[],this.subscribers[t]=i),i.push({callback:e})},DataSet.prototype.subscribe=DataSet.prototype.on,DataSet.prototype.off=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},DataSet.prototype.unsubscribe=DataSet.prototype.off,DataSet.prototype._trigger=function(t,e,i){if("*"==t)throw new Error("Cannot trigger event *");var s=[];t in this.subscribers&&(s=s.concat(this.subscribers[t])),"*"in this.subscribers&&(s=s.concat(this.subscribers["*"]));for(var n=0;no;o++)i=n._addItem(t[o]),s.push(i);else if(util.isDataTable(t))for(var r=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var c={},l=0,u=r.length;u>l;l++){var p=r[l];c[p]=t.getValue(h,l)}i=n._addItem(c),s.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=n._addItem(t),s.push(i)}return s.length&&this._trigger("add",{items:s},e),s},DataSet.prototype.update=function(t,e){var i=[],s=[],n=this,o=n.fieldId,a=function(t){var e=t[o];n.data[e]?(e=n._updateItem(t),s.push(e)):(e=n._addItem(t),i.push(e))};if(t instanceof Array)for(var r=0,h=t.length;h>r;r++)a(t[r]);else if(util.isDataTable(t))for(var d=this._getColumnNames(t),c=0,l=t.getNumberOfRows();l>c;c++){for(var u={},p=0,g=d.length;g>p;p++){var m=d[p];u[m]=t.getValue(c,p)}a(u)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");a(t)}return i.length&&this._trigger("add",{items:i},e),s.length&&this._trigger("update",{items:s},e),i.concat(s)},DataSet.prototype.get=function(){var t,e,i,s,n=this,o=this.showInternalIds,a=util.getType(arguments[0]);"String"==a||"Number"==a?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==a?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",s&&r!=util.getType(s))throw new Error('Type of parameter "data" ('+util.getType(s)+") does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!util.isDataTable(s))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=s&&"DataTable"==util.getType(s)?"DataTable":"Array";void 0!=i&&void 0!=i.showInternalIds&&(this.showInternalIds=i.showInternalIds);var h,d,c,l,u=i&&i.convert||this.options.convert,p=i&&i.filter,g=[];if(void 0!=t)h=n._getItem(t,u),p&&!p(h)&&(h=null);else if(void 0!=e)for(c=0,l=e.length;l>c;c++)h=n._getItem(e[c],u),(!p||p(h))&&g.push(h);else for(d in this.data)this.data.hasOwnProperty(d)&&(h=n._getItem(d,u),(!p||p(h))&&g.push(h));if(this.showInternalIds=o,i&&i.order&&void 0==t&&this._sort(g,i.order),i&&i.fields){var m=i.fields;if(void 0!=t)h=this._filterFields(h,m);else for(c=0,l=g.length;l>c;c++)g[c]=this._filterFields(g[c],m)}if("DataTable"==r){var f=this._getColumnNames(s);if(void 0!=t)n._appendRow(s,f,h);else for(c=0,l=g.length;l>c;c++)n._appendRow(s,f,g[c]);return s}if(void 0!=t)return h;if(s){for(c=0,l=g.length;l>c;c++)s.push(g[c]);return s}return g},DataSet.prototype.getIds=function(t){var e,i,s,n,o,a=this.data,r=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,c=[];if(r)if(h){o=[];for(s in a)a.hasOwnProperty(s)&&(n=this._getItem(s,d),r(n)&&o.push(n));for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(s in a)a.hasOwnProperty(s)&&(n=this._getItem(s,d),r(n)&&c.push(n[this.fieldId]));else if(h){o=[];for(s in a)a.hasOwnProperty(s)&&o.push(a[s]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(s in a)a.hasOwnProperty(s)&&(n=a[s],c.push(n[this.fieldId]));return c},DataSet.prototype.forEach=function(t,e){var i,s,n=e&&e.filter,o=e&&e.convert||this.options.convert,a=this.data;if(e&&e.order)for(var r=this.get(e),h=0,d=r.length;d>h;h++)i=r[h],s=i[this.fieldId],t(i,s);else for(s in a)a.hasOwnProperty(s)&&(i=this._getItem(s,o),(!n||n(i))&&t(i,s))},DataSet.prototype.map=function(t,e){var i,s=e&&e.filter,n=e&&e.convert||this.options.convert,o=[],a=this.data;for(var r in a)a.hasOwnProperty(r)&&(i=this._getItem(r,n),(!s||s(i))&&o.push(t(i,r)));return e&&e.order&&this._sort(o,e.order),o},DataSet.prototype._filterFields=function(t,e){var i={};for(var s in t)t.hasOwnProperty(s)&&-1!=e.indexOf(s)&&(i[s]=t[s]);return i},DataSet.prototype._sort=function(t,e){if(util.isString(e)){var i=e;t.sort(function(t,e){var s=t[i],n=e[i];return s>n?1:n>s?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},DataSet.prototype.remove=function(t,e){var i,s,n,o=[];if(t instanceof Array)for(i=0,s=t.length;s>i;i++)n=this._remove(t[i]),null!=n&&o.push(n);else n=this._remove(t),null!=n&&o.push(n);return o.length&&this._trigger("remove",{items:o},e),o},DataSet.prototype._remove=function(t){if(util.isNumber(t)||util.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},DataSet.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},DataSet.prototype.max=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],a=o[t];null!=a&&(!i||a>s)&&(i=o,s=a)}return i},DataSet.prototype.min=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],a=o[t];null!=a&&(!i||s>a)&&(i=o,s=a)}return i},DataSet.prototype.distinct=function(t){var e=this.data,i=[],s=this.options.convert[t],n=0;for(var o in e)if(e.hasOwnProperty(o)){for(var a=e[o],r=util.convert(a[t],s),h=!1,d=0;n>d;d++)if(i[d]==r){h=!0;break}h||(i[n]=r,n++)}return i},DataSet.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=util.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=util.convert(t[s],n)}return this.data[e]=i,e},DataSet.prototype._getItem=function(t,e){var i,s,n=this.data[t];if(!n)return null;var o={},a=this.fieldId,r=this.internalIds;if(e)for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==a&&s in r&&!this.showInternalIds||(o[i]=util.convert(s,e[i])));else for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==a&&s in r&&!this.showInternalIds||(o[i]=s));return o},DataSet.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=util.convert(t[s],n)}return e},DataSet.prototype.isInternalId=function(t){return t in this.internalIds},DataSet.prototype._getColumnNames=function(t){for(var e=[],i=0,s=t.getNumberOfColumns();s>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},DataSet.prototype._appendRow=function(t,e,i){for(var s=t.addRow(),n=0,o=e.length;o>n;n++){var a=e[n];t.setValue(s,n,i[a])}},DataView.prototype.setData=function(t){var e,i,s;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var n in this.ids)this.ids.hasOwnProperty(n)&&e.push(n);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,s=e.length;s>i;i++)n=e[i],this.ids[n]=!0;this._trigger("add",{items:e}),this.data.on&&this.data.on("*",this.listener)}},DataView.prototype.get=function(){var t,e,i,s=this,n=util.getType(arguments[0]);"String"==n||"Number"==n||"Array"==n?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=util.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return s.options.filter(t)&&e.filter(t)});var a=[];return void 0!=t&&a.push(t),a.push(o),a.push(i),this.data&&this.data.get.apply(this.data,a)},DataView.prototype.getIds=function(t){var e;if(this.data){var i,s=this.options.filter;i=t&&t.filter?s?function(e){return s(e)&&t.filter(e)}:t.filter:s,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},DataView.prototype._onEvent=function(t,e,i){var s,n,o,a,r=e&&e.items,h=this.data,d=[],c=[],l=[];if(r&&h){switch(t){case"add":for(s=0,n=r.length;n>s;s++)o=r[s],a=this.get(o),a&&(this.ids[o]=!0,d.push(o));break;case"update":for(s=0,n=r.length;n>s;s++)o=r[s],a=this.get(o),a?this.ids[o]?c.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],l.push(o));break;case"remove":for(s=0,n=r.length;n>s;s++)o=r[s],this.ids[o]&&(delete this.ids[o],l.push(o))}d.length&&this._trigger("add",{items:d},i),c.length&&this._trigger("update",{items:c},i),l.length&&this._trigger("remove",{items:l},i)}},DataView.prototype.on=DataSet.prototype.on,DataView.prototype.off=DataSet.prototype.off,DataView.prototype._trigger=DataSet.prototype._trigger,DataView.prototype.subscribe=DataView.prototype.on,DataView.prototype.unsubscribe=DataView.prototype.off,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+1e3*this.step*60);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+1e3*this.step*60*60);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,s=864e5,n=36e5,o=6e4,a=1e3,r=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),s/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){var e=new Date(t.valueOf());if(this.scale==TimeStep.SCALE.YEAR){var i=e.getFullYear()+Math.round(e.getMonth()/12);e.setFullYear(Math.round(i/this.step)*this.step),e.setMonth(0),e.setDate(0),e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)e.getDate()>15?(e.setDate(1),e.setMonth(e.getMonth()+1)):e.setDate(1),e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:e.setHours(24*Math.round(e.getHours()/24));break;default:e.setHours(12*Math.round(e.getHours()/12))}e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:e.setMinutes(60*Math.round(e.getMinutes()/60));break;default:e.setMinutes(30*Math.round(e.getMinutes()/30))}e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:e.setMinutes(5*Math.round(e.getMinutes()/5)),e.setSeconds(0);break;case 5:e.setSeconds(60*Math.round(e.getSeconds()/60));break;default:e.setSeconds(30*Math.round(e.getSeconds()/30))}e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:e.setSeconds(5*Math.round(e.getSeconds()/5)),e.setMilliseconds(0);break;case 5:e.setMilliseconds(1e3*Math.round(e.getMilliseconds()/1e3));break;default:e.setMilliseconds(500*Math.round(e.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var s=this.step>5?this.step/2:1;e.setMilliseconds(Math.round(e.getMilliseconds()/s)*s)}return e},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return moment(t).format("SSS");case TimeStep.SCALE.SECOND:return moment(t).format("s");case TimeStep.SCALE.MINUTE:return moment(t).format("HH:mm");case TimeStep.SCALE.HOUR:return moment(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return moment(t).format("ddd D");case TimeStep.SCALE.DAY:return moment(t).format("D");case TimeStep.SCALE.MONTH:return moment(t).format("MMM");case TimeStep.SCALE.YEAR:return moment(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return moment(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return moment(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return moment(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return moment(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return moment(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},Stack.prototype.setOptions=function(t){util.extend(this.options,t)},Stack.prototype.order=function t(e){var t=this.options.order||this.defaultOptions.order;if("function"!=typeof t)throw new Error("Option order must be a function");e.sort(t)},Stack.prototype.orderByStart=function(t){t.sort(function(t,e){return t.data.start-e.data.start})},Stack.prototype.orderByEnd=function(t){t.sort(function(t,e){var i="end"in t.data?t.data.end:t.data.start,s="end"in e.data?e.data.end:e.data.start;return i-s})},Stack.prototype.stack=function(t,e){var i,s,n,o,a=this.options;if(n=a.margin&&void 0!==a.margin.item?a.margin.item:this.defaultOptions.margin.item,o=a.margin&&void 0!==a.margin.axis?a.margin.axis:this.defaultOptions.margin.axis,e)for(i=0,s=t.length;s>i;i++)t[i].top=null;for(i=0,s=t.length;s>i;i++){var r=t[i];if(null===r.top){r.top=o;do{for(var h=null,d=0,c=t.length;c>d;d++){var l=t[d];if(null!==l.top&&l!==r&&this.collision(r,l,n)){h=l;break}}null!=h&&(r.top=h.top+h.height+n)}while(h)}}},Stack.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},Emitter(Range.prototype),Range.prototype.setOptions=function(t){util.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},Range.prototype.setRange=function(t,e){var i=this._applyRange(t,e);if(i){var s={start:new Date(this.start),end:new Date(this.end)};this.emit("rangechange",s),this.emit("rangechanged",s)}},Range.prototype._applyRange=function(t,e){var i,s=null!=t?util.convert(t,"Date").valueOf():this.start,n=null!=e?util.convert(e,"Date").valueOf():this.end,o=null!=this.options.max?util.convert(this.options.max,"Date").valueOf():null,a=null!=this.options.min?util.convert(this.options.min,"Date").valueOf():null;if(isNaN(s)||null===s)throw new Error('Invalid start "'+t+'"');if(isNaN(n)||null===n)throw new Error('Invalid end "'+e+'"');if(s>n&&(n=s),null!==a&&a>s&&(i=a-s,s+=i,n+=i,null!=o&&n>o&&(n=o)),null!==o&&n>o&&(i=n-o,s-=i,n-=i,null!=a&&a>s&&(s=a)),null!==this.options.zoomMin){var r=parseFloat(this.options.zoomMin);0>r&&(r=0),r>n-s&&(this.end-this.start===r?(s=this.start,n=this.end):(i=r-(n-s),s-=i/2,n+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),n-s>h&&(this.end-this.start===h?(s=this.start,n=this.end):(i=n-s-h,s+=i/2,n-=i/2))}var d=this.start!=s||this.end!=n;return this.start=s,this.end=n,d},Range.prototype.getRange=function(){return{start:this.start,end:this.end}},Range.prototype.conversion=function(t){return Range.conversion(this.start,this.end,t)},Range.conversion=function(t,e,i){return 0!=i&&e-t!=0?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var touchParams={};Range.prototype._onDragStart=function(){if(!touchParams.ignore){touchParams.start=this.start,touchParams.end=this.end;var t=this.parent.frame;t&&(t.style.cursor="move")}},Range.prototype._onDrag=function(t){var e=this.options.direction;if(validateDirection(e),!touchParams.ignore){var i="horizontal"==e?t.gesture.deltaX:t.gesture.deltaY,s=touchParams.end-touchParams.start,n="horizontal"==e?this.parent.width:this.parent.height,o=-i/n*s;this._applyRange(touchParams.start+o,touchParams.end+o),this.emit("rangechange",{start:new Date(this.start),end:new Date(this.end)})}},Range.prototype._onDragEnd=function(){touchParams.ignore||(this.parent.frame&&(this.parent.frame.style.cursor="auto"),this.emit("rangechanged",{start:new Date(this.start),end:new Date(this.end)}))},Range.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){var i;i=0>e?1-e/5:1/(1+e/5);var s=util.fakeGesture(this,t),n=getPointer(s.center,this.parent.frame),o=this._pointerToDate(n);this.zoom(i,o)}t.preventDefault()},Range.prototype._onTouch=function(t){touchParams.start=this.start,touchParams.end=this.end,touchParams.ignore=!1,touchParams.center=null;var e=ItemSet.itemFromTarget(t);e&&e.selected&&this.options.editable&&(touchParams.ignore=!0)},Range.prototype._onHold=function(){touchParams.ignore=!0},Range.prototype._onPinch=function(t){this.options.direction;if(touchParams.ignore=!0,t.gesture.touches.length>1){touchParams.center||(touchParams.center=getPointer(t.gesture.center,this.parent.frame));var e=1/t.gesture.scale,i=this._pointerToDate(touchParams.center),s=getPointer(t.gesture.center,this.parent.frame),n=(this._pointerToDate(this.parent,s),parseInt(i+(touchParams.start-i)*e)),o=parseInt(i+(touchParams.end-i)*e);this.setRange(n,o)}},Range.prototype._pointerToDate=function(t){var e,i=this.options.direction;if(validateDirection(i),"horizontal"==i){var s=this.parent.width;return e=this.conversion(s),t.x/e.scale+e.offset}var n=this.parent.height;return e=this.conversion(n),t.y/e.scale+e.offset},Range.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,s=e+(this.end-e)*t;this.setRange(i,s)},Range.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,s=this.end+e*t;this.start=i,this.end=s},Range.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,s=this.start-i,n=this.end-i;this.setRange(s,n)},Emitter(Component.prototype),Component.prototype.setOptions=function(t){t&&(util.extend(this.options,t),this.repaint())},Component.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},Component.prototype.getFrame=function(){return null},Component.prototype.repaint=function(){return!1},Component.prototype._isResized=function(){var t=this._previousWidth!==this.width||this._previousHeight!==this.height;return this._previousWidth=this.width,this._previousHeight=this.height,t},Panel.prototype=new Component,Panel.prototype.setOptions=Component.prototype.setOptions,Panel.prototype.getFrame=function(){return this.frame},Panel.prototype.appendChild=function(t){this.childs.push(t),t.parent=this;var e=t.getFrame();e&&(e.parentNode&&e.parentNode.removeChild(e),this.frame.appendChild(e))},Panel.prototype.insertBefore=function(t,e){var i=this.childs.indexOf(e);if(-1!=i){this.childs.splice(i,0,t),t.parent=this;var s=t.getFrame();if(s){s.parentNode&&s.parentNode.removeChild(s);var n=e.getFrame();n?this.frame.insertBefore(s,n):this.frame.appendChild(s)}}},Panel.prototype.removeChild=function(t){var e=this.childs.indexOf(t);if(-1!=e){this.childs.splice(e,1),t.parent=null;var i=t.getFrame();i&&i.parentNode&&this.frame.removeChild(i)}},Panel.prototype.hasChild=function(t){var e=this.childs.indexOf(t);return-1!=e},Panel.prototype.repaint=function(){var t=util.option.asString,e=this.options,i=this.getFrame();i.className="vpanel"+(e.className?" "+t(e.className):"");var s=this._repaintChilds();return this._updateSize(),this._isResized()||s},Panel.prototype._repaintChilds=function(){for(var t=!1,e=0,i=this.childs.length;i>e;e++)t=this.childs[e].repaint()||t;return t},Panel.prototype._updateSize=function(){this.frame.style.top=util.option.asSize(this.options.top),this.frame.style.bottom=util.option.asSize(this.options.bottom),this.frame.style.left=util.option.asSize(this.options.left),this.frame.style.right=util.option.asSize(this.options.right),this.frame.style.width=util.option.asSize(this.options.width,"100%"),this.frame.style.height=util.option.asSize(this.options.height,""),this.top=this.frame.offsetTop,this.left=this.frame.offsetLeft,this.width=this.frame.offsetWidth,this.height=this.frame.offsetHeight},RootPanel.prototype=new Panel,RootPanel.prototype._create=function(){this.frame=document.createElement("div"),this.hammer=Hammer(this.frame,{prevent_default:!0}),this.listeners={};var t=this,e=["touch","pinch","tap","doubletap","hold","dragstart","drag","dragend","mousewheel","DOMMouseScroll"];e.forEach(function(e){var i=function(){var i=[e].concat(Array.prototype.slice.call(arguments,0));t.emit.apply(t,i)};t.hammer.on(e,i),t.listeners[e]=i})},RootPanel.prototype.setOptions=function(t){t&&(util.extend(this.options,t),this.repaint(),this._initWatch())},RootPanel.prototype.getFrame=function(){return this.frame},RootPanel.prototype.repaint=function(){var t=this.options,e="vis timeline rootpanel "+t.orientation+(t.editable?" editable":"");t.className&&(e+=" "+util.option.asString(e)),this.frame.className=e;var i=this._repaintChilds();this.frame.style.maxHeight=util.option.asSize(this.options.maxHeight,""),this._updateSize();var s=this._isResized()||i;s&&setTimeout(this.repaint.bind(this),0)},RootPanel.prototype._initWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},RootPanel.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?void(t.frame&&(t.frame.clientWidth!=t.lastWidth||t.frame.clientHeight!=t.lastHeight)&&(t.lastWidth=t.frame.clientWidth,t.lastHeight=t.frame.clientHeight,t.repaint())):void t._unwatch()};util.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},RootPanel.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},TimeAxis.prototype=new Component,TimeAxis.prototype.setOptions=Component.prototype.setOptions,TimeAxis.prototype._create=function(){this.frame=document.createElement("div")},TimeAxis.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},TimeAxis.prototype.getFrame=function(){return this.frame},TimeAxis.prototype.repaint=function(){var t=util.option.asSize,e=this.options,i=this.props,s=this.frame;s.className="timeaxis";var n=s.parentNode;if(n){this._calculateCharSize();var o=this.getOption("orientation"),a=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),h=this.parent.height;i.minorLabelHeight=a?i.minorCharHeight:0,i.majorLabelHeight=r?i.majorCharHeight:0,this.height=i.minorLabelHeight+i.majorLabelHeight,this.width=s.offsetWidth,i.minorLineHeight=h+i.minorLabelHeight,i.minorLineWidth=1,i.majorLineHeight=h+this.height,i.majorLineWidth=1;var d=s.nextSibling;n.removeChild(s),"top"==o?(s.style.top="0",s.style.left="0",s.style.bottom="",s.style.width=t(e.width,"100%"),s.style.height=this.height+"px"):(s.style.top="",s.style.bottom="0",s.style.left="0",s.style.width=t(e.width,"100%"),s.style.height=this.height+"px"),this._repaintLabels(),this._repaintLine(),d?n.insertBefore(s,d):n.appendChild(s)
+}return this._isResized()},TimeAxis.prototype._repaintLabels=function(){var t=this.getOption("orientation"),e=util.convert(this.range.start,"Number"),i=util.convert(this.range.end,"Number"),s=this.options.toTime(5*(this.props.minorCharWidth||10)).valueOf()-this.options.toTime(0).valueOf(),n=new TimeStep(new Date(e),new Date(i),s);this.step=n;var o=this.dom;o.redundant.majorLines=o.majorLines,o.redundant.majorTexts=o.majorTexts,o.redundant.minorLines=o.minorLines,o.redundant.minorTexts=o.minorTexts,o.majorLines=[],o.majorTexts=[],o.minorLines=[],o.minorTexts=[],n.first();for(var a=void 0,r=0;n.hasNext()&&1e3>r;){r++;var h=n.getCurrent(),d=this.options.toScreen(h),c=n.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(d,n.getLabelMinor(),t),c&&this.getOption("showMajorLabels")?(d>0&&(void 0==a&&(a=d),this._repaintMajorText(d,n.getLabelMajor(),t)),this._repaintMajorLine(d,t)):this._repaintMinorLine(d,t),n.next()}if(this.getOption("showMajorLabels")){var l=this.options.toTime(0),u=n.getLabelMajor(l),p=u.length*(this.props.majorCharWidth||10)+10;(void 0==a||a>p)&&this._repaintMajorText(0,u,t)}util.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},TimeAxis.prototype._repaintMinorText=function(t,e,i){var s=this.dom.redundant.minorTexts.shift();if(!s){var n=document.createTextNode("");s=document.createElement("div"),s.appendChild(n),s.className="text minor",this.frame.appendChild(s)}this.dom.minorTexts.push(s),s.childNodes[0].nodeValue=e,"top"==i?(s.style.top=this.props.majorLabelHeight+"px",s.style.bottom=""):(s.style.top="",s.style.bottom=this.props.majorLabelHeight+"px"),s.style.left=t+"px"},TimeAxis.prototype._repaintMajorText=function(t,e,i){var s=this.dom.redundant.majorTexts.shift();if(!s){var n=document.createTextNode(e);s=document.createElement("div"),s.className="text major",s.appendChild(n),this.frame.appendChild(s)}this.dom.majorTexts.push(s),s.childNodes[0].nodeValue=e,"top"==i?(s.style.top="0px",s.style.bottom=""):(s.style.top="",s.style.bottom="0px"),s.style.left=t+"px"},TimeAxis.prototype._repaintMinorLine=function(t,e){var i=this.dom.redundant.minorLines.shift();i||(i=document.createElement("div"),i.className="grid vertical minor",this.frame.appendChild(i)),this.dom.minorLines.push(i);var s=this.props;"top"==e?(i.style.top=this.props.majorLabelHeight+"px",i.style.bottom=""):(i.style.top="",i.style.bottom=this.props.majorLabelHeight+"px"),i.style.height=s.minorLineHeight+"px",i.style.left=t-s.minorLineWidth/2+"px"},TimeAxis.prototype._repaintMajorLine=function(t,e){var i=this.dom.redundant.majorLines.shift();i||(i=document.createElement("DIV"),i.className="grid vertical major",this.frame.appendChild(i)),this.dom.majorLines.push(i);var s=this.props;"top"==e?(i.style.top="0px",i.style.bottom=""):(i.style.top="",i.style.bottom="0px"),i.style.left=t-s.majorLineWidth/2+"px",i.style.height=s.majorLineHeight+"px"},TimeAxis.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame,i=this.getOption("orientation");this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),"top"==i?(t.style.top=this.height+"px",t.style.bottom=""):(t.style.top="",t.style.bottom=this.height+"px")):t&&t.parentNode&&(t.parentNode.removeChild(t),delete this.dom.line)},TimeAxis.prototype._calculateCharSize=function(){if(!("minorCharHeight"in this.props)){var t=document.createTextNode("0"),e=document.createElement("DIV");e.className="text minor measure",e.appendChild(t),this.frame.appendChild(e),this.props.minorCharHeight=e.clientHeight,this.props.minorCharWidth=e.clientWidth,this.frame.removeChild(e)}if(!("majorCharHeight"in this.props)){var i=document.createTextNode("0"),s=document.createElement("DIV");s.className="text major measure",s.appendChild(i),this.frame.appendChild(s),this.props.majorCharHeight=s.clientHeight,this.props.majorCharWidth=s.clientWidth,this.frame.removeChild(s)}},TimeAxis.prototype.snap=function(t){return this.step.snap(t)},CurrentTime.prototype=new Component,CurrentTime.prototype.setOptions=Component.prototype.setOptions,CurrentTime.prototype._create=function(){var t=document.createElement("div");t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",this.bar=t},CurrentTime.prototype.getFrame=function(){return this.bar},CurrentTime.prototype.repaint=function(){var t=(this.parent,new Date),e=this.options.toScreen(t);return this.bar.style.left=e+"px",this.bar.title="Current time: "+t,!1},CurrentTime.prototype.start=function(){function t(){e.stop();var i=e.range.conversion(e.parent.width).scale,s=1/i/10;30>s&&(s=30),s>1e3&&(s=1e3),e.repaint(),e.currentTimeTimer=setTimeout(t,s)}var e=this;t()},CurrentTime.prototype.stop=function(){void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer)},CustomTime.prototype=new Component,CustomTime.prototype.setOptions=Component.prototype.setOptions,CustomTime.prototype._create=function(){var t=document.createElement("div");t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",this.bar=t;var e=document.createElement("div");e.style.position="relative",e.style.top="0px",e.style.left="-10px",e.style.height="100%",e.style.width="20px",t.appendChild(e),this.hammer=Hammer(t,{prevent_default:!0}),this.hammer.on("dragstart",this._onDragStart.bind(this)),this.hammer.on("drag",this._onDrag.bind(this)),this.hammer.on("dragend",this._onDragEnd.bind(this))},CustomTime.prototype.getFrame=function(){return this.bar},CustomTime.prototype.repaint=function(){var t=this.options.toScreen(this.customTime);return this.bar.style.left=t+"px",this.bar.title="Time: "+this.customTime,!1},CustomTime.prototype.setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},CustomTime.prototype.getCustomTime=function(){return new Date(this.customTime.valueOf())},CustomTime.prototype._onDragStart=function(t){this.eventParams.dragging=!0,this.eventParams.customTime=this.customTime,t.stopPropagation(),t.preventDefault()},CustomTime.prototype._onDrag=function(t){if(this.eventParams.dragging){var e=t.gesture.deltaX,i=this.options.toScreen(this.eventParams.customTime)+e,s=this.options.toTime(i);this.setCustomTime(s),this.emit("timechange",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault()}},CustomTime.prototype._onDragEnd=function(t){this.eventParams.dragging&&(this.emit("timechanged",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault())},ItemSet.prototype=new Panel,ItemSet.types={box:ItemBox,range:ItemRange,rangeoverflow:ItemRangeOverflow,point:ItemPoint},ItemSet.prototype._create=function(){var t=document.createElement("div");t["timeline-itemset"]=this,this.frame=t;var e=document.createElement("div");e.className="background",this.backgroundPanel.frame.appendChild(e),this.dom.background=e;var i=document.createElement("div");i.className="foreground",t.appendChild(i),this.dom.foreground=i;var s=document.createElement("div");s.className="axis",this.dom.axis=s,this.axisPanel.frame.appendChild(s),this.hammer=Hammer(t,{prevent_default:!0}),this.hammer.on("dragstart",this._onDragStart.bind(this)),this.hammer.on("drag",this._onDrag.bind(this)),this.hammer.on("dragend",this._onDragEnd.bind(this))},ItemSet.prototype.setOptions=Component.prototype.setOptions,ItemSet.prototype.hide=function(){this.dom.axis.parentNode&&this.dom.axis.parentNode.removeChild(this.dom.axis),this.dom.background.parentNode&&this.dom.background.parentNode.removeChild(this.dom.background)},ItemSet.prototype.show=function(){this.dom.axis.parentNode||this.axisPanel.frame.appendChild(this.dom.axis),this.dom.background.parentNode||this.backgroundPanel.frame.appendChild(this.dom.background)},ItemSet.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},ItemSet.prototype.setSelection=function(t){var e,i,s,n;if(t){if(!Array.isArray(t))throw new TypeError("Array expected");for(e=0,i=this.selection.length;i>e;e++)s=this.selection[e],n=this.items[s],n&&n.unselect();for(this.selection=[],e=0,i=t.length;i>e;e++)s=t[e],n=this.items[s],n&&(this.selection.push(s),n.select())}},ItemSet.prototype.getSelection=function(){return this.selection.concat([])},ItemSet.prototype._deselect=function(t){for(var e=this.selection,i=0,s=e.length;s>i;i++)if(e[i]==t){e.splice(i,1);break}},ItemSet.prototype.getFrame=function(){return this.frame},ItemSet.prototype.repaint=function(){var t=util.option.asSize,e=util.option.asString,i=this.options,s=this.getOption("orientation"),n=this.frame;n.className="itemset"+(i.className?" "+e(i.className):"");var o=this.range.end-this.range.start,a=o!=this.lastVisibleInterval||this.width!=this.lastWidth;this.lastVisibleInterval=o,this.lastWidth=this.width,this.visibleItems=[];for(var r in this.items)if(this.items.hasOwnProperty(r)){var h=this.items[r];h.isVisible(this.range)?(h.displayed||h.show(),h.repositionX(),this.visibleItems.push(h)):h.displayed&&h.hide()}var d=this.stackDirty||a;this.stack.stack(this.visibleItems,d),this.stackDirty=!1;for(var c=0,l=this.visibleItems.length;l>c;c++)this.visibleItems[c].repositionY();var u,p=i.margin&&"axis"in i.margin?i.margin.axis:this.itemOptions.margin.axis,g=i.margin&&"item"in i.margin?i.margin.item:this.itemOptions.margin.item,m=this.visibleItems;if(m.length){var f=m[0].top,v=m[0].top+m[0].height;util.forEach(m,function(t){f=Math.min(f,t.top),v=Math.max(v,t.top+t.height)}),u=v-f+p+g}else u=p+g;return n.style.left=t(i.left,""),n.style.right=t(i.right,""),n.style.top=t("top"==s?"0":""),n.style.bottom=t("top"==s?"":"0"),n.style.width=t(i.width,"100%"),n.style.height=t(u),this.top=n.offsetTop,this.left=n.offsetLeft,this.width=n.offsetWidth,this.height=u,this.dom.axis.style.left=t(i.left,"0"),this.dom.axis.style.right=t(i.right,""),this.dom.axis.style.width=t(i.width,"100%"),this.dom.axis.style.height=t(0),this.dom.axis.style.top=t("top"==s?"0":""),this.dom.axis.style.bottom=t("top"==s?"":"0"),this._isResized()},ItemSet.prototype.getForeground=function(){return this.dom.foreground},ItemSet.prototype.getBackground=function(){return this.dom.background},ItemSet.prototype.getAxis=function(){return this.dom.axis},ItemSet.prototype.setItems=function(t){var e,i=this,s=this.itemsData;if(t){if(!(t instanceof DataSet||t instanceof DataView))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(s&&(util.forEach(this.listeners,function(t,e){s.unsubscribe(e,t)}),e=s.getIds(),this._onRemove(e)),this.itemsData){var n=this.id;util.forEach(this.listeners,function(t,e){i.itemsData.on(e,t,n)}),e=this.itemsData.getIds(),this._onAdd(e)}},ItemSet.prototype.getItems=function(){return this.itemsData},ItemSet.prototype.removeItem=function(t){var e=this.itemsData.get(t),i=this._myDataSet();e&&this.options.onRemove(e,function(e){e&&i.remove(t)})},ItemSet.prototype._onUpdate=function(t){var e=this,i=this.items,s=this.itemOptions;t.forEach(function(t){var n=e.itemsData.get(t),o=i[t],a=n.type||n.start&&n.end&&"range"||e.options.type||"box",r=ItemSet.types[a];if(o&&(r&&o instanceof r?o.data=n:(o.hide(),o=null)),!o){if(!r)throw new TypeError('Unknown item type "'+a+'"');o=new r(e,n,e.options,s),o.id=t}e.items[t]=o}),this._order(),this.stackDirty=!0,this.emit("change")},ItemSet.prototype._onAdd=ItemSet.prototype._onUpdate,ItemSet.prototype._onRemove=function(t){var e=0,i=this;t.forEach(function(t){var s=i.items[t];if(s){e++,s.hide(),delete i.items[t],delete i.visibleItems[t];var n=i.selection.indexOf(t);-1!=n&&i.selection.splice(n,1)}}),e&&(this._order(),this.stackDirty=!0,this.emit("change"))},ItemSet.prototype._order=function(){var t=util.toArray(this.items);this.orderedItems.byStart=t,this.orderedItems.byEnd=[].concat(t),this.stack.orderByStart(this.orderedItems.byStart),this.stack.orderByEnd(this.orderedItems.byEnd)},ItemSet.prototype._onDragStart=function(t){if(this.options.editable){var e=ItemSet.itemFromTarget(t),i=this;if(e&&e.selected){var s=t.target.dragLeftItem,n=t.target.dragRightItem;this.touchParams.itemProps=s?[{item:s,start:e.data.start.valueOf()}]:n?[{item:n,end:e.data.end.valueOf()}]:this.getSelection().map(function(t){var e=i.items[t],s={item:e};return"start"in e.data&&(s.start=e.data.start.valueOf()),"end"in e.data&&(s.end=e.data.end.valueOf()),s}),t.stopPropagation()}}},ItemSet.prototype._onDrag=function(t){if(this.touchParams.itemProps){var e=this.options.snap||null,i=t.gesture.deltaX,s=this.width/(this.range.end-this.range.start),n=i/s;this.touchParams.itemProps.forEach(function(t){if("start"in t){var i=new Date(t.start+n);t.item.data.start=e?e(i):i}if("end"in t){var s=new Date(t.end+n);t.item.data.end=e?e(s):s}}),this.stackDirty=!0,this.emit("change"),t.stopPropagation()}},ItemSet.prototype._onDragEnd=function(t){if(this.touchParams.itemProps){var e=[],i=this,s=this._myDataSet();this.touchParams.itemProps.forEach(function(t){var n=t.item.id,o=i.itemsData.get(n),a=!1;"start"in t.item.data&&(a=t.start!=t.item.data.start.valueOf(),o.start=util.convert(t.item.data.start,s.convert.start)),"end"in t.item.data&&(a=a||t.end!=t.item.data.end.valueOf(),o.end=util.convert(t.item.data.end,s.convert.end)),a&&i.options.onMove(o,function(i){i?(i[s.fieldId]=n,e.push(i)):("start"in t&&(t.item.data.start=t.start),"end"in t&&(t.item.data.end=t.end),this.stackDirty=!0,this.emit("change"))})}),this.touchParams.itemProps=null,e.length&&s.update(e),t.stopPropagation()}},ItemSet.itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},ItemSet.itemSetFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-itemset"))return e["timeline-itemset"];e=e.parentNode}return null},ItemSet.prototype._myDataSet=function(){for(var t=this.itemsData;t instanceof DataView;)t=t.data;return t},Item.prototype.select=function(){this.selected=!0,this.displayed&&this.repaint()},Item.prototype.unselect=function(){this.selected=!1,this.displayed&&this.repaint()},Item.prototype.show=function(){return!1},Item.prototype.hide=function(){return!1},Item.prototype.repaint=function(){},Item.prototype.repositionX=function(){},Item.prototype.repositionY=function(){},Item.prototype._repaintDeleteButton=function(t){if(this.selected&&this.options.editable&&!this.dom.deleteButton){var e=this.parent,i=this.id,s=document.createElement("div");s.className="delete",s.title="Delete this item",Hammer(s,{preventDefault:!0}).on("tap",function(t){e.removeItem(i),t.stopPropagation()}),t.appendChild(s),this.dom.deleteButton=s}else!this.selected&&this.dom.deleteButton&&(this.dom.deleteButton.parentNode&&this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton),this.dom.deleteButton=null)},ItemBox.prototype=new Item(null,null),ItemBox.prototype.isVisible=function(t){var e=(t.end-t.start)/4;return this.data.start>t.start-e&&this.data.startt.start-e&&this.data.startt.start},ItemRange.prototype.repaint=function(){var t=this.dom;if(t||(this.dom={},t=this.dom,t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content),t.box["timeline-item"]=this),!this.parent)throw new Error("Cannot repaint item: no parent attached");if(!t.box.parentNode){var e=this.parent.getForeground();if(!e)throw new Error("Cannot repaint time axis: parent has no foreground container element");e.appendChild(t.box)}if(this.displayed=!0,this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)t.content.innerHTML="",t.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);t.content.innerHTML=this.content}this.dirty=!0}var i=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=i&&(this.className=i,t.box.className=this.baseClassName+i,this.dirty=!0),this.dirty&&(this.props.content.width=this.dom.content.offsetWidth,this.height=this.dom.box.offsetHeight,this.dirty=!1),this._repaintDeleteButton(t.box),this._repaintDragLeft(),this._repaintDragRight()},ItemRange.prototype.show=function(){this.displayed||this.repaint()},ItemRange.prototype.hide=function(){if(this.displayed){var t=this.dom.box;t.parentNode&&t.parentNode.removeChild(t),this.top=null,this.left=null,this.displayed=!1}},ItemRange.prototype.repositionX=function(){var t,e=this.props,i=this.parent.width,s=this.defaultOptions.toScreen(this.data.start),n=this.defaultOptions.toScreen(this.data.end),o="padding"in this.options?this.options.padding:this.defaultOptions.padding;-i>s&&(s=-i),n>2*i&&(n=2*i),t=0>s?Math.min(-s,n-s-e.content.width-2*o):0,this.left=s,this.width=Math.max(n-s,1),this.dom.box.style.left=this.left+"px",this.dom.box.style.width=this.width+"px",this.dom.content.style.left=t+"px"},ItemRange.prototype.repositionY=function(){var t=this.options.orientation||this.defaultOptions.orientation,e=this.dom.box;"top"==t?(e.style.top=this.top+"px",e.style.bottom=""):(e.style.top="",e.style.bottom=this.top+"px")},ItemRange.prototype._repaintDragLeft=function(){if(this.selected&&this.options.editable&&!this.dom.dragLeft){var t=document.createElement("div");t.className="drag-left",t.dragLeftItem=this,Hammer(t,{preventDefault:!0}).on("drag",function(){}),this.dom.box.appendChild(t),this.dom.dragLeft=t}else!this.selected&&this.dom.dragLeft&&(this.dom.dragLeft.parentNode&&this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft),this.dom.dragLeft=null)},ItemRange.prototype._repaintDragRight=function(){if(this.selected&&this.options.editable&&!this.dom.dragRight){var t=document.createElement("div");t.className="drag-right",t.dragRightItem=this,Hammer(t,{preventDefault:!0}).on("drag",function(){}),this.dom.box.appendChild(t),this.dom.dragRight=t}else!this.selected&&this.dom.dragRight&&(this.dom.dragRight.parentNode&&this.dom.dragRight.parentNode.removeChild(this.dom.dragRight),this.dom.dragRight=null)},ItemRangeOverflow.prototype=new ItemRange(null,null),ItemRangeOverflow.prototype.baseClassName="item rangeoverflow",ItemRangeOverflow.prototype.repositionX=function(){{var t,e=this.parent.width,i=this.defaultOptions.toScreen(this.data.start),s=this.defaultOptions.toScreen(this.data.end);"padding"in this.options?this.options.padding:this.defaultOptions.padding}-e>i&&(i=-e),s>2*e&&(s=2*e),t=Math.max(-i,0),this.left=i;var n=Math.max(s-i,1);this.width=this.props.content.width=a&&(a=864e5),n=new Date(n.valueOf()-.05*a),o=new Date(o.valueOf()+.05*a)}void 0!=this.options.start&&(n=util.convert(this.options.start,"Date")),void 0!=this.options.end&&(o=util.convert(this.options.end,"Date")),(null!=n||null!=o)&&this.range.setRange(n,o)}},Timeline.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=util.extend(Object.create(this.options),{top:null,bottom:null,right:null,left:null,width:null,height:null});this.groupsData?(this.itemSet&&(this.itemSet.hide(),this.contentPanel.removeChild(this.itemSet),this.itemSet.setItems(),this.itemSet=null),this.groupSet?this.groupSet.setGroups(this.groupsData):(this.groupSet=new GroupSet(this.contentPanel,this.sideContentPanel,this.backgroundPanel,this.axisPanel,i),this.groupSet.on("change",this.rootPanel.repaint.bind(this.rootPanel)),this.groupSet.setRange(this.range),this.groupSet.setItems(this.itemsData),this.groupSet.setGroups(this.groupsData),this.contentPanel.appendChild(this.groupSet))):(this.groupSet&&(this.groupSet.hide(),this.groupSet.setItems(),this.contentPanel.removeChild(this.groupSet),this.groupSet=null),this.itemSet=new ItemSet(this.backgroundPanel,this.axisPanel,i),this.itemSet.setRange(this.range),this.itemSet.setItems(this.itemsData),this.itemSet.on("change",e.rootPanel.repaint.bind(e.rootPanel)),this.contentPanel.appendChild(this.itemSet))},Timeline.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var s=t.min("start");e=s?s.start.valueOf():null;var n=t.max("start");n&&(i=n.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},Timeline.prototype.setSelection=function(t){var e=this.itemSet||this.groupSet;e&&e.setSelection(t)},Timeline.prototype.getSelection=function(){var t=this.itemSet||this.groupSet;return t?t.getSelection():[]},Timeline.prototype.setWindow=function(t,e){this.range.setRange(t,e)},Timeline.prototype.getWindow=function(){var t=this.range.getRange();return{start:new Date(t.start),end:new Date(t.end)}},Timeline.prototype._onSelectItem=function(t){if(this.options.selectable){var e=t.gesture.srcEvent&&t.gesture.srcEvent.ctrlKey,i=t.gesture.srcEvent&&t.gesture.srcEvent.shiftKey;if(e||i)return void this._onMultiSelectItem(t);var s=this.getSelection(),n=ItemSet.itemFromTarget(t),o=n?[n.id]:[];this.setSelection(o);var a=this.getSelection();util.equalArray(s,a)||this.emit("select",{items:this.getSelection()}),t.stopPropagation()}},Timeline.prototype._onAddItem=function(t){if(this.options.selectable&&this.options.editable){var e=this,i=ItemSet.itemFromTarget(t);if(i){var s=e.itemsData.get(i.id);this.options.onUpdate(s,function(t){t&&e.itemsData.update(t)})}else{var n=vis.util.getAbsoluteLeft(this.rootPanel.frame),o=t.gesture.center.pageX-n,a={start:this.timeAxis.snap(this._toTime(o)),content:"new item"},r=util.randomUUID();a[this.itemsData.fieldId]=r;var h=GroupSet.groupFromTarget(t);h&&(a.group=h.groupId),this.options.onAdd(a,function(t){t&&e.itemsData.add(a)})}}},Timeline.prototype._onMultiSelectItem=function(t){if(this.options.selectable){var e,i=ItemSet.itemFromTarget(t);if(i){e=this.getSelection();var s=e.indexOf(i.id);-1==s?e.push(i.id):e.splice(s,1),this.setSelection(e),this.emit("select",{items:this.getSelection()}),t.stopPropagation()}}},Timeline.prototype._toTime=function(t){var e=this.range.conversion(this.mainPanel.width);return new Date(t/e.scale+e.offset)},Timeline.prototype._toScreen=function(t){var e=this.range.conversion(this.mainPanel.width);return(t.valueOf()-e.offset)*e.scale},function(t){function e(t){return D=t,u()}function i(){C=0,I=D.charAt(0)}function s(){C++,I=D.charAt(C)}function n(){return D.charAt(C+1)}function o(t){return O.test(t)}function a(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function r(t,e,i){for(var s=e.split("."),n=t;s.length;){var o=s.shift();s.length?(n[o]||(n[o]={}),n=n[o]):n[o]=i}}function h(t,e){for(var i,s,n=null,o=[t],r=t;r.parent;)o.push(r.parent),r=r.parent;if(r.nodes)for(i=0,s=r.nodes.length;s>i;i++)if(e.id===r.nodes[i].id){n=r.nodes[i];break}for(n||(n={id:e.id},t.node&&(n.attr=a(n.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(n)&&h.nodes.push(n)}e.attr&&(n.attr=a(n.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=a({},t.edge);e.attr=a(i,e.attr)}}function c(t,e,i,s,n){var o={from:e,to:i,type:s};return t.edge&&(o.attr=a({},t.edge)),o.attr=a(o.attr||{},n),o}function l(){for(N=E.NULL,M="";" "==I||" "==I||"\n"==I||"\r"==I;)s();do{var t=!1;if("#"==I){for(var e=C-1;" "==D.charAt(e)||" "==D.charAt(e);)e--;if("\n"==D.charAt(e)||""==D.charAt(e)){for(;""!=I&&"\n"!=I;)s();t=!0}}if("/"==I&&"/"==n()){for(;""!=I&&"\n"!=I;)s();t=!0}if("/"==I&&"*"==n()){for(;""!=I;){if("*"==I&&"/"==n()){s(),s();break}s()}t=!0}for(;" "==I||" "==I||"\n"==I||"\r"==I;)s()}while(t);if(""==I)return void(N=E.DELIMITER);var i=I+n();if(T[i])return N=E.DELIMITER,M=i,s(),void s();if(T[I])return N=E.DELIMITER,M=I,void s();if(o(I)||"-"==I){for(M+=I,s();o(I);)M+=I,s();return"false"==M?M=!1:"true"==M?M=!0:isNaN(Number(M))||(M=Number(M)),void(N=E.IDENTIFIER)}if('"'==I){for(s();""!=I&&('"'!=I||'"'==I&&'"'==n());)M+=I,'"'==I&&s(),s();if('"'!=I)throw b('End of string " expected');return s(),void(N=E.IDENTIFIER)}for(N=E.UNKNOWN;""!=I;)M+=I,s();throw new SyntaxError('Syntax error in part "'+S(M,30)+'"')}function u(){var t={};if(i(),l(),"strict"==M&&(t.strict=!0,l()),("graph"==M||"digraph"==M)&&(t.type=M,l()),N==E.IDENTIFIER&&(t.id=M,l()),"{"!=M)throw b("Angle bracket { expected");if(l(),p(t),"}"!=M)throw b("Angle bracket } expected");if(l(),""!==M)throw b("End of file expected");return l(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==M&&"}"!=M;)g(t),";"==M&&l()}function g(t){var e=m(t);if(e)return void y(t,e);var i=f(t);if(!i){if(N!=E.IDENTIFIER)throw b("Identifier expected");var s=M;if(l(),"="==M){if(l(),N!=E.IDENTIFIER)throw b("Identifier expected");t[s]=M,l()}else v(t,s)}}function m(t){var e=null;if("subgraph"==M&&(e={},e.type="subgraph",l(),N==E.IDENTIFIER&&(e.id=M,l())),"{"==M){if(l(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=M)throw b("Angle bracket } expected");l(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function f(t){return"node"==M?(l(),t.node=_(),"node"):"edge"==M?(l(),t.edge=_(),"edge"):"graph"==M?(l(),t.graph=_(),"graph"):null}function v(t,e){var i={id:e},s=_();s&&(i.attr=s),h(t,i),y(t,e)}function y(t,e){for(;"->"==M||"--"==M;){var i,s=M;l();var n=m(t);if(n)i=n;else{if(N!=E.IDENTIFIER)throw b("Identifier or subgraph expected");i=M,h(t,{id:i}),l()}var o=_(),a=c(t,e,i,s,o);d(t,a),e=i}}function _(){for(var t=null;"["==M;){for(l(),t={};""!==M&&"]"!=M;){if(N!=E.IDENTIFIER)throw b("Attribute name expected");var e=M;if(l(),"="!=M)throw b("Equal sign = expected");if(l(),N!=E.IDENTIFIER)throw b("Attribute value expected");var i=M;r(t,e,i),l(),","==M&&l()}if("]"!=M)throw b("Bracket ] expected");l()}return t}function b(t){return new SyntaxError(t+', got "'+S(M,30)+'" (char '+C+")")}function S(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function w(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function x(t){function i(t){var e={from:t.from,to:t.to};return a(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var s=e(t),n={nodes:[],edges:[],options:{}};return s.nodes&&s.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};a(e,t.attr),e.image&&(e.shape="image"),n.nodes.push(e)}),s.edges&&s.edges.forEach(function(t){var e,s;e=t.from instanceof Object?t.from.nodes:{id:t.from},s=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);n.edges.push(e)}),w(e,s,function(e,s){var o=c(n,e.id,s.id,t.type,t.attr),a=i(o);n.edges.push(a)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);n.edges.push(e)})}),s.attr&&(n.options=s.attr),n}var E={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},T={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},D="",C=0,I="",M="",N=E.NULL,O=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=x}("undefined"!=typeof util?util:exports),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,a=Math.sqrt(s*s-n*n);this.moveTo(t,e-(a-o)),this.lineTo(t+n,e+o),this.lineTo(t-n,e+o),this.lineTo(t,e-(a-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,a=Math.sqrt(s*s-n*n);this.moveTo(t,e+(a-o)),this.lineTo(t+n,e-o),this.lineTo(t-n,e-o),this.lineTo(t,e+(a-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var s=0;10>s;s++){var n=s%2===0?1.3*i:.5*i;this.lineTo(t+n*Math.sin(2*s*Math.PI/10),e-n*Math.cos(2*s*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,s,n){var o=Math.PI/180;0>i-2*n&&(n=i/2),0>s-2*n&&(n=s/2),this.beginPath(),this.moveTo(t+n,e),this.lineTo(t+i-n,e),this.arc(t+i-n,e+n,n,270*o,360*o,!1),this.lineTo(t+i,e+s-n),this.arc(t+i-n,e+s-n,n,0,90*o,!1),this.lineTo(t+n,e+s),this.arc(t+n,e+s-n,n,90*o,180*o,!1),this.lineTo(t,e+n),this.arc(t+n,e+n,n,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,s){var n=.5522848,o=i/2*n,a=s/2*n,r=t+i,h=e+s,d=t+i/2,c=e+s/2;this.beginPath(),this.moveTo(t,c),this.bezierCurveTo(t,c-a,d-o,e,d,e),this.bezierCurveTo(d+o,e,r,c-a,r,c),this.bezierCurveTo(r,c+a,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,c+a,t,c)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var n=1/3,o=i,a=s*n,r=.5522848,h=o/2*r,d=a/2*r,c=t+o,l=e+a,u=t+o/2,p=e+a/2,g=e+(s-a/2),m=e+s;this.beginPath(),this.moveTo(c,p),this.bezierCurveTo(c,p+d,u+h,l,u,l),this.bezierCurveTo(u-h,l,t,p+d,t,p),this.bezierCurveTo(t,p-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,c,p-d,c,p),this.lineTo(c,g),this.bezierCurveTo(c,g+d,u+h,m,u,m),this.bezierCurveTo(u-h,m,t,g+d,t,g),this.lineTo(t,p)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,s){var n=t-s*Math.cos(i),o=e-s*Math.sin(i),a=t-.9*s*Math.cos(i),r=e-.9*s*Math.sin(i),h=n+s/3*Math.cos(i+.5*Math.PI),d=o+s/3*Math.sin(i+.5*Math.PI),c=n+s/3*Math.cos(i-.5*Math.PI),l=o+s/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(a,r),this.lineTo(c,l),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,s,n){n||(n=[10,5]),0==u&&(u=.001);var o=n.length;this.moveTo(t,e);for(var a=i-t,r=s-e,h=r/a,d=Math.sqrt(a*a+r*r),c=0,l=!0;d>=.1;){var u=n[c++%o];u>d&&(u=d);var p=Math.sqrt(u*u/(1+h*h));0>a&&(p=-p),t+=p,e+=h*p,this[l?"lineTo":"moveTo"](t,e),d-=u,l=!l}}),Node.prototype.resetCluster=function(){this.formationScale=void 0,this.clusterSize=1,this.containedNodes={},this.containedEdges={},this.clusterSessions=[]},Node.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),-1==this.dynamicEdges.indexOf(t)&&this.dynamicEdges.push(t),this.dynamicEdgesLength=this.dynamicEdges.length},Node.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&(this.edges.splice(e,1),this.dynamicEdges.splice(e,1)),this.dynamicEdgesLength=this.dynamicEdges.length},Node.prototype.setProperties=function(t,e){if(t){if(this.originalLabel=void 0,void 0!==t.id&&(this.id=t.id),void 0!==t.label&&(this.label=t.label,this.originalLabel=t.label),void 0!==t.title&&(this.title=t.title),void 0!==t.group&&(this.group=t.group),void 0!==t.x&&(this.x=t.x),void 0!==t.y&&(this.y=t.y),void 0!==t.value&&(this.value=t.value),void 0!==t.level&&(this.level=t.level,this.preassignedLevel=!0),void 0!==t.mass&&(this.mass=t.mass),void 0!==t.horizontalAlignLeft&&(this.horizontalAlignLeft=t.horizontalAlignLeft),void 0!==t.verticalAlignTop&&(this.verticalAlignTop=t.verticalAlignTop),void 0!==t.triggerFunction&&(this.triggerFunction=t.triggerFunction),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var s in i)i.hasOwnProperty(s)&&(this[s]=i[s])}if(void 0!==t.shape&&(this.shape=t.shape),void 0!==t.image&&(this.image=t.image),void 0!==t.radius&&(this.radius=t.radius),void 0!==t.color&&(this.color=util.parseColor(t.color)),void 0!==t.fontColor&&(this.fontColor=t.fontColor),void 0!==t.fontSize&&(this.fontSize=t.fontSize),void 0!==t.fontFace&&(this.fontFace=t.fontFace),void 0!==this.image&&""!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!==t.x&&!t.allowedToMoveX,this.yFixed=this.yFixed||void 0!==t.y&&!t.allowedToMoveY,this.radiusFixed=this.radiusFixed||void 0!==t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},Node.prototype.select=function(){this.selected=!0,this._reset()},Node.prototype.unselect=function(){this.selected=!1,this._reset()},Node.prototype.clearSizeCache=function(){this._reset()},Node.prototype._reset=function(){this.width=void 0,this.height=void 0},Node.prototype.getTitle=function(){return"function"==typeof this.title?this.title():this.title},Node.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var s=this.width/2,n=this.height/2,o=Math.sin(e)*s,a=Math.cos(e)*n;return s*n/Math.sqrt(o*o+a*a);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},Node.prototype._setForce=function(t,e){this.fx=t,this.fy=e},Node.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},Node.prototype.discreteStep=function(t){if(!this.xFixed){var e=this.damping*this.vx,i=(this.fx-e)/this.mass;this.vx+=i*t,this.x+=this.vx*t}if(!this.yFixed){var s=this.damping*this.vy,n=(this.fy-s)/this.mass;this.vy+=n*t,this.y+=this.vy*t}},Node.prototype.discreteStepLimited=function(t,e){if(this.xFixed)this.fx=0;else{var i=this.damping*this.vx,s=(this.fx-i)/this.mass;this.vx+=s*t,this.vx=Math.abs(this.vx)>e?this.vx>0?e:-e:this.vx,this.x+=this.vx*t}if(this.yFixed)this.fy=0;else{var n=this.damping*this.vy,o=(this.fy-n)/this.mass;this.vy+=o*t,this.vy=Math.abs(this.vy)>e?this.vy>0?e:-e:this.vy,this.y+=this.vy*t}},Node.prototype.isFixed=function(){return this.xFixed&&this.yFixed},Node.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t},Node.prototype.isSelected=function(){return this.selected},Node.prototype.getValue=function(){return this.value},Node.prototype.getDistance=function(t,e){var i=this.x-t,s=this.y-e;return Math.sqrt(i*i+s*s)},Node.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}this.baseRadiusValue=this.radius},Node.prototype.draw=function(){throw"Draw method not initialized for node"},Node.prototype.resize=function(){throw"Resize method not initialized for node"},Node.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},Node.prototype._resizeImage=function(){if(!this.width||!this.height){var t,e;if(this.value){this.radius=this.baseRadiusValue;var i=this.imageObj.height/this.imageObj.width;void 0!==i?(t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height):(t=0,e=0)}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e,this.growthIndicator=0,this.width>0&&this.height>0&&(this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-t)}},Node.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;if(0!=this.imageObj.width){if(this.clusterSize>1){var i=this.clusterSize>1?10:0;i*=this.graphScaleInv,i=Math.min(.2*this.width,i),t.globalAlpha=.5,t.drawImage(this.imageObj,this.left-i,this.top-i,this.width+2*i,this.height+2*i)}t.globalAlpha=1,t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2}else e=this.y;this._label(t,this.label,this.x,e,void 0,"top")},Node.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.growthIndicator=this.width-(i.width+2*e)}},Node.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.roundRect(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth,this.radius),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=i.width+2*e;this.width=s,this.height=s,this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-s}},Node.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.database(this.x-this.width/2-2*t.lineWidth,this.y-.5*this.height-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=Math.max(i.width,i.height)+2*e;this.radius=s/2,this.width=s,this.height=s,this.radius+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.radius-.5*s}},Node.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.circle(this.x,this.y,this.radius+2*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.width1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.ellipse(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.ellipse(this.left,this.top,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._drawDot=function(t){this._drawShape(t,"circle")},Node.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},Node.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},Node.prototype._drawSquare=function(t){this._drawShape(t,"square")},Node.prototype._drawStar=function(t){this._drawShape(t,"star")},Node.prototype._resizeShape=function(){if(!this.width){this.radius=this.baseRadiusValue;var t=2*this.radius;this.width=t,this.height=t,this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-t}},Node.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var i=2.5,s=2,n=2;switch(e){case"dot":n=2;break;case"square":n=2;break;case"triangle":n=3;break;case"triangleDown":n=3;break;case"star":n=4}t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?s:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t[e](this.x,this.y,this.radius+n*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?s:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.label&&this._label(t,this.label,this.x,this.y+this.height/2,void 0,"top")},Node.prototype._resizeText=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-(i.width+2*e)}},Node.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._label(t,this.label,this.x,this.y)},Node.prototype._label=function(t,e,i,s,n,o){if(e&&this.fontSize*this.graphScale>this.fontDrawThreshold){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=n||"center",t.textBaseline=o||"middle";for(var a=e.split("\n"),r=a.length,h=this.fontSize+4,d=s+(1-r)/2*h,c=0;r>c;c++)t.fillText(a[c],i,d),d+=h}},Node.prototype.getTextSize=function(t){if(void 0!==this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,s=0,n=0,o=e.length;o>n;n++)s=Math.max(s,t.measureText(e[n]).width);return{width:s,height:i}}return{width:0,height:0}},Node.prototype.inArea=function(){return void 0!==this.width?this.x+this.width*this.graphScaleInv>=this.canvasTopLeft.x&&this.x-this.width*this.graphScaleInv=this.canvasTopLeft.y&&this.y-this.height*this.graphScaleInv=this.canvasTopLeft.x&&this.x=this.canvasTopLeft.y&&this.yh}return!1},Edge.prototype._drawLine=function(t){if(t.strokeStyle=1==this.selected?this.color.highlight:this.color.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var e;if(this.label){if(1==this.smooth){var i=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),s=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));e={x:i,y:s}}else e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}}else{var n,o,a=this.length/4,r=this.from;r.width||r.resize(t),r.width>r.height?(n=r.x+r.width/2,o=r.y-a):(n=r.x+a,o=r.y-r.height/2),this._circle(t,n,o,a),e=this._pointOnCircle(n,o,a,.5),this._label(t,this.label,e.x,e.y)}},Edge.prototype._getLineWidth=function(){return 1==this.selected?Math.min(2*this.width,this.widthMax)*this.graphScaleInv:this.width*this.graphScaleInv},Edge.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,this.to.x,this.to.y):t.lineTo(this.to.x,this.to.y),t.stroke()},Edge.prototype._circle=function(t,e,i,s){t.beginPath(),t.arc(e,i,s,0,2*Math.PI,!1),t.stroke()},Edge.prototype._label=function(t,e,i,s){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontFill;var n=t.measureText(e).width,o=this.fontSize,a=i-n/2,r=s-o/2;t.fillRect(a,r,n,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,a,r)}},Edge.prototype._drawDashLine=function(t){if(t.strokeStyle=1==this.selected?this.color.highlight:this.color.color,t.lineWidth=this._getLineWidth(),void 0!==t.mozDash||void 0!==t.setLineDash){t.beginPath(),t.moveTo(this.from.x,this.from.y);var e=[0];e=void 0!==this.dash.length&&void 0!==this.dash.gap?[this.dash.length,this.dash.gap]:[5,5],"undefined"!=typeof t.setLineDash?(t.setLineDash(e),t.lineDashOffset=0):(t.mozDash=e,t.mozDashOffset=0),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,this.to.x,this.to.y):t.lineTo(this.to.x,this.to.y),t.stroke(),"undefined"!=typeof t.setLineDash?(t.setLineDash([0]),t.lineDashOffset=0):(t.mozDash=[0],t.mozDashOffset=0)}else t.beginPath(),t.lineCap="round",void 0!==this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!==this.dash.length&&void 0!==this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke();if(this.label){var i;if(1==this.smooth){var s=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),n=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));
+i={x:s,y:n}}else i=this._pointOnLine(.5);this._label(t,this.label,i.x,i.y)}},Edge.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},Edge.prototype._pointOnCircle=function(t,e,i,s){var n=2*(s-3/8)*Math.PI;return{x:t+i*Math.cos(n),y:e-i*Math.sin(n)}},Edge.prototype._drawArrowCenter=function(t){var e;if(1==this.selected?(t.strokeStyle=this.color.highlight,t.fillStyle=this.color.highlight):(t.strokeStyle=this.color.color,t.fillStyle=this.color.color),t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),s=10+5*this.width;if(1==this.smooth){var n=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),o=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));e={x:n,y:o}}else e=this._pointOnLine(.5);t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&this._label(t,this.label,e.x,e.y)}else{var a,r,h=.25*Math.max(100,this.length),d=this.from;d.width||d.resize(t),d.width>d.height?(a=d.x+.5*d.width,r=d.y-h):(a=d.x+h,r=d.y-.5*d.height),this._circle(t,a,r,h);var i=.2*Math.PI,s=10+5*this.width;e=this._pointOnCircle(a,r,h,.5),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(a,r,h,.5),this._label(t,this.label,e.x,e.y))}},Edge.prototype._drawArrow=function(t){1==this.selected?(t.strokeStyle=this.color.highlight,t.fillStyle=this.color.highlight):(t.strokeStyle=this.color.color,t.fillStyle=this.color.color),t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var s=this.to.x-this.from.x,n=this.to.y-this.from.y,o=Math.sqrt(s*s+n*n),a=this.from.distanceToBorder(t,e+Math.PI),r=(o-a)/o,h=r*this.from.x+(1-r)*this.to.x,d=r*this.from.y+(1-r)*this.to.y;1==this.smooth&&(e=Math.atan2(this.to.y-this.via.y,this.to.x-this.via.x),s=this.to.x-this.via.x,n=this.to.y-this.via.y,o=Math.sqrt(s*s+n*n));var c,l,u=this.to.distanceToBorder(t,e),p=(o-u)/o;if(1==this.smooth?(c=(1-p)*this.via.x+p*this.to.x,l=(1-p)*this.via.y+p*this.to.y):(c=(1-p)*this.from.x+p*this.to.x,l=(1-p)*this.from.y+p*this.to.y),t.beginPath(),t.moveTo(h,d),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,c,l):t.lineTo(c,l),t.stroke(),i=10+5*this.width,t.arrow(c,l,e,i),t.fill(),t.stroke(),this.label){var g;if(1==this.smooth){var m=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),f=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));g={x:m,y:f}}else g=this._pointOnLine(.5);this._label(t,this.label,g.x,g.y)}}else{var v,y,_,b=this.from,S=.25*Math.max(100,this.length);b.width||b.resize(t),b.width>b.height?(v=b.x+.5*b.width,y=b.y-S,_={x:v,y:b.y,angle:.9*Math.PI}):(v=b.x+S,y=b.y-.5*b.height,_={x:b.x,y:y,angle:.6*Math.PI}),t.beginPath(),t.arc(v,y,S,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(_.x,_.y,_.angle,i),t.fill(),t.stroke(),this.label&&(g=this._pointOnCircle(v,y,S,.5),this._label(t,this.label,g.x,g.y))}},Edge.prototype._getDistanceToEdge=function(t,e,i,s,n,o){if(1==this.smooth){var a,r,h,d,c,l,u=1e9;for(a=0;10>a;a++)r=.1*a,h=Math.pow(1-r,2)*t+2*r*(1-r)*this.via.x+Math.pow(r,2)*i,d=Math.pow(1-r,2)*e+2*r*(1-r)*this.via.y+Math.pow(r,2)*s,c=Math.abs(n-h),l=Math.abs(o-d),u=Math.min(u,Math.sqrt(c*c+l*l));return u}var p=i-t,g=s-e,m=p*p+g*g,f=((n-t)*p+(o-e)*g)/m;f>1?f=1:0>f&&(f=0);var h=t+f*p,d=e+f*g,c=h-n,l=d-o;return Math.sqrt(c*c+l*l)},Edge.prototype.setScale=function(t){this.graphScaleInv=1/t},Edge.prototype.select=function(){this.selected=!0},Edge.prototype.unselect=function(){this.selected=!1},Edge.prototype.positionBezierNode=function(){null!==this.via&&(this.via.x=.5*(this.from.x+this.to.x),this.via.y=.5*(this.from.y+this.to.y))},Popup.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},Popup.prototype.setText=function(t){this.frame.innerHTML=t},Popup.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,s=this.frame.parentNode.clientHeight,n=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>s&&(o=s-e-this.padding),on&&(a=n-i-this.padding),athis.constants.clustering.clusterThreshold&&1==this.constants.clustering.enabled&&this.clusterToFit(this.constants.clustering.reduceToNodes,!1),this._calculateForces())},_calculateForces:function(){this._calculateGravitationalForces(),this._calculateNodeForces(),1==this.constants.smoothCurves?this._calculateSpringForcesWithSupport():this._calculateSpringForces()},_updateCalculationNodes:function(){if(1==this.constants.smoothCurves){this.calculationNodes={},this.calculationNodeIndices=[];for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&(this.calculationNodes[t]=this.nodes[t]);var e=this.sectors.support.nodes;for(var i in e)e.hasOwnProperty(i)&&(this.edges.hasOwnProperty(e[i].parentEdgeId)?this.calculationNodes[i]=e[i]:e[i]._setForce(0,0));for(var s in this.calculationNodes)this.calculationNodes.hasOwnProperty(s)&&this.calculationNodeIndices.push(s)}else this.calculationNodes=this.nodes,this.calculationNodeIndices=this.nodeIndices},_calculateGravitationalForces:function(){var t,e,i,s,n,o=this.calculationNodes,a=this.constants.physics.centralGravity,r=0;for(n=0;n
Simulation Mode:
Barnes Hut
Repulsion
Hierarchical
Barnes Hut
gravitationalConstant
0
-20000
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
Repulsion
nodeDistance
0
300
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
Hierarchical
nodeDistance
0
300
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
direction
1
4
levelSeparation
1
500
nodeSpacing
1
500
Options:
',this.containerElement.parentElement.insertBefore(this.physicsConfiguration,this.containerElement),this.optionsDiv=document.createElement("div"),this.optionsDiv.style.fontSize="14px",this.optionsDiv.style.fontFamily="verdana",this.containerElement.parentElement.insertBefore(this.optionsDiv,this.containerElement);var e;e=document.getElementById("graph_BH_gc"),e.onchange=showValueOfRange.bind(this,"graph_BH_gc",-1,"physics_barnesHut_gravitationalConstant"),e=document.getElementById("graph_BH_cg"),e.onchange=showValueOfRange.bind(this,"graph_BH_cg",1,"physics_centralGravity"),e=document.getElementById("graph_BH_sc"),e.onchange=showValueOfRange.bind(this,"graph_BH_sc",1,"physics_springConstant"),e=document.getElementById("graph_BH_sl"),e.onchange=showValueOfRange.bind(this,"graph_BH_sl",1,"physics_springLength"),e=document.getElementById("graph_BH_damp"),e.onchange=showValueOfRange.bind(this,"graph_BH_damp",1,"physics_damping"),e=document.getElementById("graph_R_nd"),e.onchange=showValueOfRange.bind(this,"graph_R_nd",1,"physics_repulsion_nodeDistance"),e=document.getElementById("graph_R_cg"),e.onchange=showValueOfRange.bind(this,"graph_R_cg",1,"physics_centralGravity"),e=document.getElementById("graph_R_sc"),e.onchange=showValueOfRange.bind(this,"graph_R_sc",1,"physics_springConstant"),e=document.getElementById("graph_R_sl"),e.onchange=showValueOfRange.bind(this,"graph_R_sl",1,"physics_springLength"),e=document.getElementById("graph_R_damp"),e.onchange=showValueOfRange.bind(this,"graph_R_damp",1,"physics_damping"),e=document.getElementById("graph_H_nd"),e.onchange=showValueOfRange.bind(this,"graph_H_nd",1,"physics_hierarchicalRepulsion_nodeDistance"),e=document.getElementById("graph_H_cg"),e.onchange=showValueOfRange.bind(this,"graph_H_cg",1,"physics_centralGravity"),e=document.getElementById("graph_H_sc"),e.onchange=showValueOfRange.bind(this,"graph_H_sc",1,"physics_springConstant"),e=document.getElementById("graph_H_sl"),e.onchange=showValueOfRange.bind(this,"graph_H_sl",1,"physics_springLength"),e=document.getElementById("graph_H_damp"),e.onchange=showValueOfRange.bind(this,"graph_H_damp",1,"physics_damping"),e=document.getElementById("graph_H_direction"),e.onchange=showValueOfRange.bind(this,"graph_H_direction",t,"hierarchicalLayout_direction"),e=document.getElementById("graph_H_levsep"),e.onchange=showValueOfRange.bind(this,"graph_H_levsep",1,"hierarchicalLayout_levelSeparation"),e=document.getElementById("graph_H_nspac"),e.onchange=showValueOfRange.bind(this,"graph_H_nspac",1,"hierarchicalLayout_nodeSpacing");var i=document.getElementById("graph_physicsMethod1"),s=document.getElementById("graph_physicsMethod2"),n=document.getElementById("graph_physicsMethod3");s.checked=!0,this.constants.physics.barnesHut.enabled&&(i.checked=!0),this.constants.hierarchicalLayout.enabled&&(n.checked=!0);var o=document.getElementById("graph_toggleSmooth"),a=document.getElementById("graph_repositionNodes"),r=document.getElementById("graph_generateOptions");o.onclick=graphToggleSmoothCurves.bind(this),a.onclick=graphRepositionNodes.bind(this),r.onclick=graphGenerateOptions.bind(this),o.style.background=1==this.constants.smoothCurves?"#A4FF56":"#FF8532",switchConfigurations.apply(this),i.onchange=switchConfigurations.bind(this),s.onchange=switchConfigurations.bind(this),n.onchange=switchConfigurations.bind(this)}},_overWriteGraphConstants:function(t,e){var i=t.split("_");1==i.length?this.constants[i[0]]=e:2==i.length?this.constants[i[0]][i[1]]=e:3==i.length&&(this.constants[i[0]][i[1]][i[2]]=e)}},hierarchalRepulsionMixin={_calculateNodeForces:function(){var t,e,i,s,n,o,a,r,h,d,c=this.calculationNodes,l=this.calculationNodeIndices,u=5,p=.5*-u,g=this.constants.physics.hierarchicalRepulsion.nodeDistance,m=g;for(h=0;hi&&(o=f*i+u,0==i?i=.01:o/=i,s=t*o,n=e*o,a.fx-=s,a.fy-=n,r.fx+=s,r.fy+=n)}}},barnesHutMixin={_calculateNodeForces:function(){if(0!=this.constants.physics.barnesHut.gravitationalConstant){var t,e=this.calculationNodes,i=this.calculationNodeIndices,s=i.length;this._formBarnesHutTree(e,i);for(var n=this.barnesHutTree,o=0;s>o;o++)t=e[i[o]],this._getForceContribution(n.root.children.NW,t),this._getForceContribution(n.root.children.NE,t),this._getForceContribution(n.root.children.SW,t),this._getForceContribution(n.root.children.SE,t)}},_getForceContribution:function(t,e){if(t.childrenCount>0){var i,s,n;if(i=t.centerOfMass.x-e.x,s=t.centerOfMass.y-e.y,n=Math.sqrt(i*i+s*s),n*t.calcSize>this.constants.physics.barnesHut.theta){0==n&&(n=.1*Math.random(),i=n);var o=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(n*n*n),a=i*o,r=s*o;e.fx+=a,e.fy+=r}else if(4==t.childrenCount)this._getForceContribution(t.children.NW,e),this._getForceContribution(t.children.NE,e),this._getForceContribution(t.children.SW,e),this._getForceContribution(t.children.SE,e);else if(t.children.data.id!=e.id){0==n&&(n=.5*Math.random(),i=n);var o=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(n*n*n),a=i*o,r=s*o;e.fx+=a,e.fy+=r}}},_formBarnesHutTree:function(t,e){for(var i,s=e.length,n=Number.MAX_VALUE,o=Number.MAX_VALUE,a=-Number.MAX_VALUE,r=-Number.MAX_VALUE,h=0;s>h;h++){var d=t[e[h]].x,c=t[e[h]].y;n>d&&(n=d),d>a&&(a=d),o>c&&(o=c),c>r&&(r=c)}var l=Math.abs(a-n)-Math.abs(r-o);l>0?(o-=.5*l,r+=.5*l):(n+=.5*l,a-=.5*l);var u=1e-5,p=Math.max(u,Math.abs(a-n)),g=.5*p,m=.5*(n+a),f=.5*(o+r),v={root:{centerOfMass:{x:0,y:0},mass:0,range:{minX:m-g,maxX:m+g,minY:f-g,maxY:f+g},size:p,calcSize:1/p,children:{data:null},maxWidth:0,level:0,childrenCount:4}};for(this._splitBranch(v.root),h=0;s>h;h++)i=t[e[h]],this._placeInTree(v.root,i);this.barnesHutTree=v},_updateBranchMass:function(t,e){var i=t.mass+e.mass,s=1/i;t.centerOfMass.x=t.centerOfMass.x*t.mass+e.x*e.mass,t.centerOfMass.x*=s,t.centerOfMass.y=t.centerOfMass.y*t.mass+e.y*e.mass,t.centerOfMass.y*=s,t.mass=i;var n=Math.max(Math.max(e.height,e.radius),e.width);t.maxWidth=t.maxWidthe.x?t.children.NW.range.maxY>e.y?this._placeInRegion(t,e,"NW"):this._placeInRegion(t,e,"SW"):t.children.NW.range.maxY>e.y?this._placeInRegion(t,e,"NE"):this._placeInRegion(t,e,"SE")},_placeInRegion:function(t,e,i){switch(t.children[i].childrenCount){case 0:t.children[i].children.data=e,t.children[i].childrenCount=1,this._updateBranchMass(t.children[i],e);break;case 1:t.children[i].children.data.x==e.x&&t.children[i].children.data.y==e.y?(e.x+=Math.random(),e.y+=Math.random()):(this._splitBranch(t.children[i]),this._placeInTree(t.children[i],e));break;case 4:this._placeInTree(t.children[i],e)}},_splitBranch:function(t){var e=null;1==t.childrenCount&&(e=t.children.data,t.mass=0,t.centerOfMass.x=0,t.centerOfMass.y=0),t.childrenCount=4,t.children.data=null,this._insertRegion(t,"NW"),this._insertRegion(t,"NE"),this._insertRegion(t,"SW"),this._insertRegion(t,"SE"),null!=e&&this._placeInTree(t,e)},_insertRegion:function(t,e){var i,s,n,o,a=.5*t.size;switch(e){case"NW":i=t.range.minX,s=t.range.minX+a,n=t.range.minY,o=t.range.minY+a;break;case"NE":i=t.range.minX+a,s=t.range.maxX,n=t.range.minY,o=t.range.minY+a;break;case"SW":i=t.range.minX,s=t.range.minX+a,n=t.range.minY+a,o=t.range.maxY;break;case"SE":i=t.range.minX+a,s=t.range.maxX,n=t.range.minY+a,o=t.range.maxY}t.children[e]={centerOfMass:{x:0,y:0},mass:0,range:{minX:i,maxX:s,minY:n,maxY:o},size:.5*t.size,calcSize:2*t.calcSize,children:{data:null},maxWidth:0,level:t.level+1,childrenCount:0}},_drawTree:function(t,e){void 0!==this.barnesHutTree&&(t.lineWidth=1,this._drawBranch(this.barnesHutTree.root,t,e))},_drawBranch:function(t,e,i){void 0===i&&(i="#FF0000"),4==t.childrenCount&&(this._drawBranch(t.children.NW,e),this._drawBranch(t.children.NE,e),this._drawBranch(t.children.SE,e),this._drawBranch(t.children.SW,e)),e.strokeStyle=i,e.beginPath(),e.moveTo(t.range.minX,t.range.minY),e.lineTo(t.range.maxX,t.range.minY),e.stroke(),e.beginPath(),e.moveTo(t.range.maxX,t.range.minY),e.lineTo(t.range.maxX,t.range.maxY),e.stroke(),e.beginPath(),e.moveTo(t.range.maxX,t.range.maxY),e.lineTo(t.range.minX,t.range.maxY),e.stroke(),e.beginPath(),e.moveTo(t.range.minX,t.range.maxY),e.lineTo(t.range.minX,t.range.minY),e.stroke()}},repulsionMixin={_calculateNodeForces:function(){var t,e,i,s,n,o,a,r,h,d,c,l=this.calculationNodes,u=this.calculationNodeIndices,p=-2/3,g=4/3,m=this.constants.physics.repulsion.nodeDistance,f=m;for(d=0;di&&(a=.5*f>i?1:v*i+g,a*=0==o?1:1+o*this.constants.clustering.forceAmplification,a/=i,s=t*a,n=e*a,r.fx-=s,r.fy-=n,h.fx+=s,h.fy+=n)}}},HierarchicalLayoutMixin={_resetLevels:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];0==e.preassignedLevel&&(e.level=-1)}},_setupHierarchicalLayout:function(){if(1==this.constants.hierarchicalLayout.enabled){"RL"==this.constants.hierarchicalLayout.direction||"DU"==this.constants.hierarchicalLayout.direction?this.constants.hierarchicalLayout.levelSeparation*=-1:this.constants.hierarchicalLayout.levelSeparation=Math.abs(this.constants.hierarchicalLayout.levelSeparation);var t,e,i=0,s=!1,n=!1;for(e in this.nodes)this.nodes.hasOwnProperty(e)&&(t=this.nodes[e],-1!=t.level?s=!0:n=!0,is&&(o.xFixed=!1,o.x=i[o.level].minPos,a=!0):o.yFixed&&o.level>s&&(o.yFixed=!1,o.y=i[o.level].minPos,a=!0),1==a&&(i[o.level].minPos+=i[o.level].nodeSpacing,o.edges.length>1&&this._placeBranchNodes(o.edges,o.id,i,o.level))}},_setLevel:function(t,e,i){for(var s=0;st)&&(n.level=t,e.length>1&&this._setLevel(t+1,n.edges,n.id))}},_restoreNodes:function(){for(nodeId in this.nodes)this.nodes.hasOwnProperty(nodeId)&&(this.nodes[nodeId].xFixed=!1,this.nodes[nodeId].yFixed=!1)}},manipulationMixin={_clearManipulatorBar:function(){for(;this.manipulationDiv.hasChildNodes();)this.manipulationDiv.removeChild(this.manipulationDiv.firstChild)},_restoreOverloadedFunctions:function(){for(var t in this.cachedFunctions)this.cachedFunctions.hasOwnProperty(t)&&(this[t]=this.cachedFunctions[t])},_toggleEditMode:function(){this.editMode=!this.editMode;var t=document.getElementById("graph-manipulationDiv"),e=document.getElementById("graph-manipulation-closeDiv"),i=document.getElementById("graph-manipulation-editMode");1==this.editMode?(t.style.display="block",e.style.display="block",i.style.display="none",e.onclick=this._toggleEditMode.bind(this)):(t.style.display="none",e.style.display="none",i.style.display="block",e.onclick=null),this._createManipulatorBar()},_createManipulatorBar:function(){if(this.boundFunction&&this.off("select",this.boundFunction),this._restoreOverloadedFunctions(),this.freezeSimulation=!1,this.blockConnectingEdgeSelection=!1,this.forceAppendSelection=!1,1==this.editMode){for(;this.manipulationDiv.hasChildNodes();)this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);this.manipulationDiv.innerHTML=""+this.constants.labels.add+""+this.constants.labels.link+"",1==this._getSelectedNodeCount()&&this.triggerFunctions.edit&&(this.manipulationDiv.innerHTML+=""+this.constants.labels.editNode+""),0==this._selectionIsEmpty()&&(this.manipulationDiv.innerHTML+=""+this.constants.labels.del+"");
+var t=document.getElementById("graph-manipulate-addNode");t.onclick=this._createAddNodeToolbar.bind(this);var e=document.getElementById("graph-manipulate-connectNode");if(e.onclick=this._createAddEdgeToolbar.bind(this),1==this._getSelectedNodeCount()&&this.triggerFunctions.edit){var i=document.getElementById("graph-manipulate-editNode");i.onclick=this._editNode.bind(this)}if(0==this._selectionIsEmpty()){var s=document.getElementById("graph-manipulate-delete");s.onclick=this._deleteSelected.bind(this)}var n=document.getElementById("graph-manipulation-closeDiv");n.onclick=this._toggleEditMode.bind(this),this.boundFunction=this._createManipulatorBar.bind(this),this.on("select",this.boundFunction)}else{this.editModeDiv.innerHTML=""+this.constants.labels.edit+"";var o=document.getElementById("graph-manipulate-editModeButton");o.onclick=this._toggleEditMode.bind(this)}},_createAddNodeToolbar:function(){this._clearManipulatorBar(),this.boundFunction&&this.off("select",this.boundFunction),this.manipulationDiv.innerHTML=""+this.constants.labels.back+" "+this.constants.labels.addDescription+"";var t=document.getElementById("graph-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._addNode.bind(this),this.on("select",this.boundFunction)},_createAddEdgeToolbar:function(){this._clearManipulatorBar(),this._unselectAll(!0),this.freezeSimulation=!0,this.boundFunction&&this.off("select",this.boundFunction),this._unselectAll(),this.forceAppendSelection=!1,this.blockConnectingEdgeSelection=!0,this.manipulationDiv.innerHTML=""+this.constants.labels.back+" "+this.constants.labels.linkDescription+"";var t=document.getElementById("graph-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._handleConnect.bind(this),this.on("select",this.boundFunction),this.cachedFunctions._handleTouch=this._handleTouch,this.cachedFunctions._handleOnRelease=this._handleOnRelease,this._handleTouch=this._handleConnect,this._handleOnRelease=this._finishConnect,this._redraw()},_handleConnect:function(t){if(0==this._getSelectedNodeCount()){var e=this._getNodeAt(t);null!=e&&(e.clusterSize>1?alert("Cannot create edges to a cluster."):(this._selectObject(e,!1),this.sectors.support.nodes.targetNode=new Node({id:"targetNode"},{},{},this.constants),this.sectors.support.nodes.targetNode.x=e.x,this.sectors.support.nodes.targetNode.y=e.y,this.sectors.support.nodes.targetViaNode=new Node({id:"targetViaNode"},{},{},this.constants),this.sectors.support.nodes.targetViaNode.x=e.x,this.sectors.support.nodes.targetViaNode.y=e.y,this.sectors.support.nodes.targetViaNode.parentEdgeId="connectionEdge",this.edges.connectionEdge=new Edge({id:"connectionEdge",from:e.id,to:this.sectors.support.nodes.targetNode.id},this,this.constants),this.edges.connectionEdge.from=e,this.edges.connectionEdge.connected=!0,this.edges.connectionEdge.smooth=!0,this.edges.connectionEdge.selected=!0,this.edges.connectionEdge.to=this.sectors.support.nodes.targetNode,this.edges.connectionEdge.via=this.sectors.support.nodes.targetViaNode,this.cachedFunctions._handleOnDrag=this._handleOnDrag,this._handleOnDrag=function(t){var e=this._getPointer(t.gesture.center);this.sectors.support.nodes.targetNode.x=this._canvasToX(e.x),this.sectors.support.nodes.targetNode.y=this._canvasToY(e.y),this.sectors.support.nodes.targetViaNode.x=.5*(this._canvasToX(e.x)+this.edges.connectionEdge.from.x),this.sectors.support.nodes.targetViaNode.y=this._canvasToY(e.y)},this.moving=!0,this.start()))}},_finishConnect:function(t){if(1==this._getSelectedNodeCount()){this._handleOnDrag=this.cachedFunctions._handleOnDrag,delete this.cachedFunctions._handleOnDrag;var e=this.edges.connectionEdge.fromId;delete this.edges.connectionEdge,delete this.sectors.support.nodes.targetNode,delete this.sectors.support.nodes.targetViaNode;var i=this._getNodeAt(t);null!=i&&(i.clusterSize>1?alert("Cannot create edges to a cluster."):(this._createEdge(e,i.id),this._createManipulatorBar())),this._unselectAll()}},_addNode:function(){if(this._selectionIsEmpty()&&1==this.editMode){var t=this._pointerToPositionObject(this.pointerPosition),e={id:util.randomUUID(),x:t.left,y:t.top,label:"new",allowedToMoveX:!0,allowedToMoveY:!0};if(this.triggerFunctions.add)if(2==this.triggerFunctions.add.length){var i=this;this.triggerFunctions.add(e,function(t){i.nodesData.add(t),i._createManipulatorBar(),i.moving=!0,i.start()})}else alert(this.constants.labels.addError),this._createManipulatorBar(),this.moving=!0,this.start();else this.nodesData.add(e),this._createManipulatorBar(),this.moving=!0,this.start()}},_createEdge:function(t,e){if(1==this.editMode){var i={from:t,to:e};if(this.triggerFunctions.connect)if(2==this.triggerFunctions.connect.length){var s=this;this.triggerFunctions.connect(i,function(t){s.edgesData.add(t),s.moving=!0,s.start()})}else alert(this.constants.labels.linkError),this.moving=!0,this.start();else this.edgesData.add(i),this.moving=!0,this.start()}},_editNode:function(){if(this.triggerFunctions.edit&&1==this.editMode){var t=this._getSelectedNode(),e={id:t.id,label:t.label,group:t.group,shape:t.shape,color:{background:t.color.background,border:t.color.border,highlight:{background:t.color.highlight.background,border:t.color.highlight.border}}};if(2==this.triggerFunctions.edit.length){var i=this;this.triggerFunctions.edit(e,function(t){i.nodesData.update(t),i._createManipulatorBar(),i.moving=!0,i.start()})}else alert(this.constants.labels.editError)}else alert(this.constants.labels.editBoundError)},_deleteSelected:function(){if(!this._selectionIsEmpty()&&1==this.editMode)if(this._clusterInSelection())alert(this.constants.labels.deleteClusterError);else{var t=this.getSelectedNodes(),e=this.getSelectedEdges();if(this.triggerFunctions.del){var i=this,s={nodes:t,edges:e};(this.triggerFunctions.del.length=2)?this.triggerFunctions.del(s,function(t){i.edgesData.remove(t.edges),i.nodesData.remove(t.nodes),i._unselectAll(),i.moving=!0,i.start()}):alert(this.constants.labels.deleteError)}else this.edgesData.remove(e),this.nodesData.remove(t),this._unselectAll(),this.moving=!0,this.start()}}},SectorMixin={_putDataInSector:function(){this.sectors.active[this._sector()].nodes=this.nodes,this.sectors.active[this._sector()].edges=this.edges,this.sectors.active[this._sector()].nodeIndices=this.nodeIndices},_switchToSector:function(t,e){void 0===e||"active"==e?this._switchToActiveSector(t):this._switchToFrozenSector(t)},_switchToActiveSector:function(t){this.nodeIndices=this.sectors.active[t].nodeIndices,this.nodes=this.sectors.active[t].nodes,this.edges=this.sectors.active[t].edges},_switchToSupportSector:function(){this.nodeIndices=this.sectors.support.nodeIndices,this.nodes=this.sectors.support.nodes,this.edges=this.sectors.support.edges},_switchToFrozenSector:function(t){this.nodeIndices=this.sectors.frozen[t].nodeIndices,this.nodes=this.sectors.frozen[t].nodes,this.edges=this.sectors.frozen[t].edges},_loadLatestSector:function(){this._switchToSector(this._sector())},_sector:function(){return this.activeSector[this.activeSector.length-1]},_previousSector:function(){if(this.activeSector.length>1)return this.activeSector[this.activeSector.length-2];throw new TypeError("there are not enough sectors in the this.activeSector array.")},_setActiveSector:function(t){this.activeSector.push(t)},_forgetLastSector:function(){this.activeSector.pop()},_createNewSector:function(t){this.sectors.active[t]={nodes:{},edges:{},nodeIndices:[],formationScale:this.scale,drawingNode:void 0},this.sectors.active[t].drawingNode=new Node({id:t,color:{background:"#eaefef",border:"495c5e"}},{},{},this.constants),this.sectors.active[t].drawingNode.clusterSize=2},_deleteActiveSector:function(t){delete this.sectors.active[t]},_deleteFrozenSector:function(t){delete this.sectors.frozen[t]},_freezeSector:function(t){this.sectors.frozen[t]=this.sectors.active[t],this._deleteActiveSector(t)},_activateSector:function(t){this.sectors.active[t]=this.sectors.frozen[t],this._deleteFrozenSector(t)},_mergeThisWithFrozen:function(t){for(var e in this.nodes)this.nodes.hasOwnProperty(e)&&(this.sectors.frozen[t].nodes[e]=this.nodes[e]);for(var i in this.edges)this.edges.hasOwnProperty(i)&&(this.sectors.frozen[t].edges[i]=this.edges[i]);for(var s=0;s1?this[t](s[0],s[1]):this[t](e)}this._loadLatestSector()},_doInSupportSector:function(t,e){if(void 0===e)this._switchToSupportSector(),this[t]();else{this._switchToSupportSector();var i=Array.prototype.splice.call(arguments,1);i.length>1?this[t](i[0],i[1]):this[t](e)}this._loadLatestSector()},_doInAllFrozenSectors:function(t,e){if(void 0===e)for(var i in this.sectors.frozen)this.sectors.frozen.hasOwnProperty(i)&&(this._switchToFrozenSector(i),this[t]());else for(var i in this.sectors.frozen)if(this.sectors.frozen.hasOwnProperty(i)){this._switchToFrozenSector(i);var s=Array.prototype.splice.call(arguments,1);s.length>1?this[t](s[0],s[1]):this[t](e)}this._loadLatestSector()},_doInAllSectors:function(t,e){var i=Array.prototype.splice.call(arguments,1);void 0===e?(this._doInAllActiveSectors(t),this._doInAllFrozenSectors(t)):i.length>1?(this._doInAllActiveSectors(t,i[0],i[1]),this._doInAllFrozenSectors(t,i[0],i[1])):(this._doInAllActiveSectors(t,e),this._doInAllFrozenSectors(t,e))},_clearNodeIndexList:function(){var t=this._sector();this.sectors.active[t].nodeIndices=[],this.nodeIndices=this.sectors.active[t].nodeIndices},_drawSectorNodes:function(t,e){var i,s=1e9,n=-1e9,o=1e9,a=-1e9;for(var r in this.sectors[e])if(this.sectors[e].hasOwnProperty(r)&&void 0!==this.sectors[e][r].drawingNode){this._switchToSector(r,e),s=1e9,n=-1e9,o=1e9,a=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),o>i.x-.5*i.width&&(o=i.x-.5*i.width),ai.y-.5*i.height&&(s=i.y-.5*i.height),nt&&s>n;)n%3==0?(this.forceAggregateHubs(!0),this.normalizeClusterLevels()):this.increaseClusterLevel(),i=this.nodeIndices.length,n+=1;n>0&&1==e&&this.repositionNodes(),this._updateCalculationNodes()},openCluster:function(t){var e=this.moving;if(t.clusterSize>this.constants.clustering.sectorThreshold&&this._nodeInActiveArea(t)&&("default"!=this._sector()||1!=this.nodeIndices.length)){this._addSector(t);for(var i=0;this.nodeIndices.lengthi;)this.decreaseClusterLevel(),i+=1}else this._expandClusterNode(t,!1,!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this._updateCalculationNodes(),this.updateLabels();this.moving!=e&&this.start()},updateClustersDefault:function(){1==this.constants.clustering.enabled&&this.updateClusters(0,!1,!1)},increaseClusterLevel:function(){this.updateClusters(-1,!1,!0)},decreaseClusterLevel:function(){this.updateClusters(1,!1,!0)},updateClusters:function(t,e,i,s){var n=this.moving,o=this.nodeIndices.length;this.previousScale>this.scale&&0==t&&this._collapseSector(),this.previousScale>this.scale||-1==t?this._formClusters(i):(this.previousScalethis.scale||-1==t)&&(this._aggregateHubs(i),this._updateNodeIndexList()),(this.previousScale>this.scale||-1==t)&&(this.handleChains(),this._updateNodeIndexList()),this.previousScale=this.scale,this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.lengththis.constants.clustering.chainThreshold&&this._reduceAmountOfChains(1-this.constants.clustering.chainThreshold/t)},_aggregateHubs:function(t){this._getHubSize(),this._formClustersByHub(t,!1)},forceAggregateHubs:function(t){var e=this.moving,i=this.nodeIndices.length;this._aggregateHubs(!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.length!=i&&(this.clusterSession+=1),(0==t||void 0===t)&&this.moving!=e&&this.start()},_openClustersBySize:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];1==e.inView()&&(e.width*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientWidth||e.height*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientHeight)&&this.openCluster(e)}},_openClusters:function(t,e){for(var i=0;i1&&(t.clusterSizei)){var a=o.from,r=o.to;o.to.mass>o.from.mass&&(a=o.to,r=o.from),1==r.dynamicEdgesLength?this._addToCluster(a,r,!1):1==a.dynamicEdgesLength&&this._addToCluster(r,a,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],s=i.toId==e.id?this.nodes[i.fromId]:this.nodes[i.toId];e.id!=s.id&&(s.mass>e.mass?this._addToCluster(s,e,!0):this._addToCluster(e,s,!0))}}},_clusterToSmallestNeighbour:function(t){for(var e=-1,i=null,s=0;sn.clusterSessions.length&&(e=n.clusterSessions.length,i=n)}null!=n&&void 0!==this.nodes[n.id]&&this._addToCluster(n,t,!0)},_formClustersByHub:function(t,e){for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&this._formClusterFromHub(this.nodes[i],t,e)},_formClusterFromHub:function(t,e,i,s){if(void 0===s&&(s=0),t.dynamicEdgesLength>=this.hubThreshold&&0==i||t.dynamicEdgesLength==this.hubThreshold&&1==i){for(var n,o,a,r=this.constants.clustering.clusterEdgeThreshold/this.scale,h=!1,d=[],c=t.dynamicEdges.length,l=0;c>l;l++)d.push(t.dynamicEdges[l].id);if(0==e)for(h=!1,l=0;c>l;l++){var u=this.edges[d[l]];if(void 0!==u&&u.connected&&u.toId!=u.fromId&&(n=u.to.x-u.from.x,o=u.to.y-u.from.y,a=Math.sqrt(n*n+o*o),r>a)){h=!0;break}}if(!e&&h||e)for(l=0;c>l;l++)if(u=this.edges[d[l]],void 0!==u){var p=this.nodes[u.fromId==t.id?u.toId:u.fromId];p.dynamicEdges.length<=this.hubThreshold+s&&p.id!=t.id&&this._addToCluster(t,p,e)}}},_addToCluster:function(t,e,i){t.containedNodes[e.id]=e;for(var s=0;s1)for(var s=0;s1&&(e.label="[".concat(String(e.clusterSize),"]"))}for(t in this.nodes)this.nodes.hasOwnProperty(t)&&(e=this.nodes[t],1==e.clusterSize&&(e.label=void 0!==e.originalLabel?e.originalLabel:String(e.id)))},normalizeClusterLevels:function(){var t=0,e=1e9,i=0;for(var s in this.nodes)this.nodes.hasOwnProperty(s)&&(i=this.nodes[s].clusterSessions.length,i>t&&(t=i),e>i&&(e=i));if(t-e>this.constants.clustering.clusterLevelDifference){var n=this.nodeIndices.length,o=t-this.constants.clustering.clusterLevelDifference;for(var s in this.nodes)this.nodes.hasOwnProperty(s)&&this.nodes[s].clusterSessions.lengths&&(s=o.dynamicEdgesLength),t+=o.dynamicEdgesLength,e+=Math.pow(o.dynamicEdgesLength,2),i+=1}t/=i,e/=i;var a=e-Math.pow(t,2),r=Math.sqrt(a);this.hubThreshold=Math.floor(t+2*r),this.hubThreshold>s&&(this.hubThreshold=s)},_reduceAmountOfChains:function(t){this.hubThreshold=2;var e=Math.floor(this.nodeIndices.length*t);for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&e>0&&(this._formClusterFromHub(this.nodes[i],!0,!0,1),e-=1)},_getChainFraction:function(){var t=0,e=0;for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&(2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&(t+=1),e+=1);return t/e}},SelectionMixin={_getNodesOverlappingWith:function(t,e){var i=this.nodes;for(var s in i)i.hasOwnProperty(s)&&i[s].isOverlappingWith(t)&&e.push(s)},_getAllNodesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getNodesOverlappingWith",t,e),e},_pointerToPositionObject:function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y);return{left:e,top:i,right:e,bottom:i}},_getNodeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllNodesOverlappingWith(e);return i.length>0?this.nodes[i[i.length-1]]:null},_getEdgesOverlappingWith:function(t,e){var i=this.edges;for(var s in i)i.hasOwnProperty(s)&&i[s].isOverlappingWith(t)&&e.push(s)},_getAllEdgesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getEdgesOverlappingWith",t,e),e},_getEdgeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllEdgesOverlappingWith(e);return i.length>0?this.edges[i[i.length-1]]:null},_addToSelection:function(t){t instanceof Node?this.selectionObj.nodes[t.id]=t:this.selectionObj.edges[t.id]=t},_removeFromSelection:function(t){t instanceof Node?delete this.selectionObj.nodes[t.id]:delete this.selectionObj.edges[t.id]},_unselectAll:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&this.selectionObj.nodes[e].unselect();for(var i in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(i)&&this.selectionObj.edges[i].unselect();this.selectionObj={nodes:{},edges:{}},0==t&&this.emit("select",this.getSelection())},_unselectClusters:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&this.selectionObj.nodes[e].clusterSize>1&&(this.selectionObj.nodes[e].unselect(),this._removeFromSelection(this.selectionObj.nodes[e]));0==t&&this.emit("select",this.getSelection())},_getSelectedNodeCount:function(){var t=0;for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&(t+=1);return t},_getSelectedNode:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t))return this.selectionObj.nodes[t];return null},_getSelectedEdgeCount:function(){var t=0;for(var e in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(e)&&(t+=1);return t},_getSelectedObjectCount:function(){var t=0;for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&(t+=1);for(var i in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(i)&&(t+=1);return t},_selectionIsEmpty:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t))return!1;for(var e in this.selectionObj.edges)if(this.selectionObj.edges.hasOwnProperty(e))return!1;return!0},_clusterInSelection:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t)&&this.selectionObj.nodes[t].clusterSize>1)return!0;return!1},_selectConnectedEdges:function(t){for(var e=0;ee;e++){s=t[e];var n=this.nodes[s];if(!n)throw new RangeError('Node with id "'+s+'" not found');this._selectObject(n,!0,!0)}this.redraw()},_updateSelection:function(){for(var t in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(t)&&(this.nodes.hasOwnProperty(t)||delete this.selectionObj.nodes[t]);for(var e in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(e)&&(this.edges.hasOwnProperty(e)||delete this.selectionObj.edges[e])}},NavigationMixin={_cleanNavigation:function(){var t=document.getElementById("graph-navigation_wrapper");null!=t&&this.containerElement.removeChild(t),document.onmouseup=null},_loadNavigationElements:function(){this._cleanNavigation(),this.navigationDivs={};var t=["up","down","left","right","zoomIn","zoomOut","zoomExtends"],e=["_moveUp","_moveDown","_moveLeft","_moveRight","_zoomIn","_zoomOut","zoomExtent"];this.navigationDivs.wrapper=document.createElement("div"),this.navigationDivs.wrapper.id="graph-navigation_wrapper",this.navigationDivs.wrapper.style.position="absolute",this.navigationDivs.wrapper.style.width=this.frame.canvas.clientWidth+"px",this.navigationDivs.wrapper.style.height=this.frame.canvas.clientHeight+"px",this.containerElement.insertBefore(this.navigationDivs.wrapper,this.frame);for(var i=0;it.x&&(s=t.x),nt.y&&(e=t.y),i=this.constants.clustering.initialMaxNodes?49.07548/(n+142.05338)+91444e-8:12.662/(n+7.4147)+.0964822:1==this.constants.clustering.enabled&&n>=this.constants.clustering.initialMaxNodes?77.5271985/(n+187.266146)+476710517e-13:30.5062972/(n+19.93597763)+.08413486;var o=Math.min(this.frame.canvas.clientWidth/600,this.frame.canvas.clientHeight/600);i*=o}else{var a=1.1*(Math.abs(s.minX)+Math.abs(s.maxX)),r=1.1*(Math.abs(s.minY)+Math.abs(s.maxY)),h=this.frame.canvas.clientWidth/a,d=this.frame.canvas.clientHeight/r;i=d>=h?h:d}i>1&&(i=1),this._setScale(i),this._centerGraph(s),0==e&&(this.moving=!0,this.start())},Graph.prototype._updateNodeIndexList=function(){this._clearNodeIndexList();for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodeIndices.push(t)},Graph.prototype.setData=function(t,e){if(void 0===e&&(e=!1),t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var i=vis.util.DOTToGraph(t.dot);return void this.setData(i)}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this._putDataInSector(),e||(this.stabilize&&this._stabilize(),this.start())},Graph.prototype.setOptions=function(t){if(t){var e;if(void 0!==t.width&&(this.width=t.width),void 0!==t.height&&(this.height=t.height),void 0!==t.stabilize&&(this.stabilize=t.stabilize),void 0!==t.selectable&&(this.selectable=t.selectable),void 0!==t.smoothCurves&&(this.constants.smoothCurves=t.smoothCurves),void 0!==t.freezeForStabilization&&(this.constants.freezeForStabilization=t.freezeForStabilization),void 0!==t.configurePhysics&&(this.constants.configurePhysics=t.configurePhysics),void 0!==t.stabilizationIterations&&(this.constants.stabilizationIterations=t.stabilizationIterations),void 0!==t.labels)for(e in t.labels)t.labels.hasOwnProperty(e)&&(this.constants.labels[e]=t.labels[e]);if(t.onAdd&&(this.triggerFunctions.add=t.onAdd),t.onEdit&&(this.triggerFunctions.edit=t.onEdit),t.onConnect&&(this.triggerFunctions.connect=t.onConnect),t.onDelete&&(this.triggerFunctions.del=t.onDelete),t.physics){if(t.physics.barnesHut){this.constants.physics.barnesHut.enabled=!0;for(e in t.physics.barnesHut)t.physics.barnesHut.hasOwnProperty(e)&&(this.constants.physics.barnesHut[e]=t.physics.barnesHut[e])}if(t.physics.repulsion){this.constants.physics.barnesHut.enabled=!1;for(e in t.physics.repulsion)t.physics.repulsion.hasOwnProperty(e)&&(this.constants.physics.repulsion[e]=t.physics.repulsion[e])}}if(t.hierarchicalLayout){this.constants.hierarchicalLayout.enabled=!0;for(e in t.hierarchicalLayout)t.hierarchicalLayout.hasOwnProperty(e)&&(this.constants.hierarchicalLayout[e]=t.hierarchicalLayout[e])}else void 0!==t.hierarchicalLayout&&(this.constants.hierarchicalLayout.enabled=!1);if(t.clustering){this.constants.clustering.enabled=!0;for(e in t.clustering)t.clustering.hasOwnProperty(e)&&(this.constants.clustering[e]=t.clustering[e])}else void 0!==t.clustering&&(this.constants.clustering.enabled=!1);if(t.navigation){this.constants.navigation.enabled=!0;for(e in t.navigation)t.navigation.hasOwnProperty(e)&&(this.constants.navigation[e]=t.navigation[e])}else void 0!==t.navigation&&(this.constants.navigation.enabled=!1);if(t.keyboard){this.constants.keyboard.enabled=!0;for(e in t.keyboard)t.keyboard.hasOwnProperty(e)&&(this.constants.keyboard[e]=t.keyboard[e])}else void 0!==t.keyboard&&(this.constants.keyboard.enabled=!1);if(t.dataManipulation){this.constants.dataManipulation.enabled=!0;for(e in t.dataManipulation)t.dataManipulation.hasOwnProperty(e)&&(this.constants.dataManipulation[e]=t.dataManipulation[e])}else void 0!==t.dataManipulation&&(this.constants.dataManipulation.enabled=!1);if(t.edges){for(e in t.edges)t.edges.hasOwnProperty(e)&&"object"!=typeof t.edges[e]&&(this.constants.edges[e]=t.edges[e]);void 0!==t.edges.color&&(util.isString(t.edges.color)?(this.constants.edges.color={},this.constants.edges.color.color=t.edges.color,this.constants.edges.color.highlight=t.edges.color):(void 0!==t.edges.color.color&&(this.constants.edges.color.color=t.edges.color.color),void 0!==t.edges.color.highlight&&(this.constants.edges.color.highlight=t.edges.color.highlight))),t.edges.fontColor||void 0!==t.edges.color&&(util.isString(t.edges.color)?this.constants.edges.fontColor=t.edges.color:void 0!==t.edges.color.color&&(this.constants.edges.fontColor=t.edges.color.color)),t.edges.dash&&(void 0!==t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!==t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!==t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=util.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var s=t.groups[i];this.groups.add(i,s)}if(t.tooltip){for(e in t.tooltip)t.tooltip.hasOwnProperty(e)&&(this.constants.tooltip[e]=t.tooltip[e]);t.tooltip.color&&(this.constants.tooltip.color=util.parseColor(t.tooltip.color))}}this._loadPhysicsSystem(),this._loadNavigationControls(),this._loadManipulationSystem(),this._configureSmoothCurves(),this._createKeyBinds(),this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1),this._redraw()},Graph.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=Hammer(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("doubletap",e._onDoubleTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("release",e._onRelease.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("DOMMouseScroll",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},Graph.prototype._createKeyBinds=function(){var t=this;this.mousetrap=mousetrap,this.mousetrap.reset(),1==this.constants.keyboard.enabled&&(this.mousetrap.bind("up",this._moveUp.bind(t),"keydown"),this.mousetrap.bind("up",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("down",this._moveDown.bind(t),"keydown"),this.mousetrap.bind("down",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("left",this._moveLeft.bind(t),"keydown"),this.mousetrap.bind("left",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("right",this._moveRight.bind(t),"keydown"),this.mousetrap.bind("right",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("=",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("=",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("-",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("-",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("[",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("[",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("]",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("]",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pageup",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("pageup",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pagedown",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("pagedown",this._stopZoom.bind(t),"keyup")),1==this.constants.dataManipulation.enabled&&(this.mousetrap.bind("escape",this._createManipulatorBar.bind(t)),this.mousetrap.bind("del",this._deleteSelected.bind(t)))},Graph.prototype._getPointer=function(t){return{x:t.pageX-vis.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-vis.util.getAbsoluteTop(this.frame.canvas)}},Graph.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.center),this.drag.pinched=!1,this.pinch.scale=this._getScale(),this._handleTouch(this.drag.pointer)},Graph.prototype._onDragStart=function(){this._handleDragStart()},Graph.prototype._handleDragStart=function(){var t=this.drag,e=this._getNodeAt(t.pointer);if(t.dragging=!0,t.selection=[],t.translation=this._getTranslation(),t.nodeId=null,null!=e){t.nodeId=e.id,e.isSelected()||this._selectObject(e,!1);for(var i in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(i)){var s=this.selectionObj.nodes[i],n={id:s.id,node:s,x:s.x,y:s.y,xFixed:s.xFixed,yFixed:s.yFixed};s.xFixed=!0,s.yFixed=!0,t.selection.push(n)}}},Graph.prototype._onDrag=function(t){this._handleOnDrag(t)},Graph.prototype._handleOnDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.center),i=this,s=this.drag,n=s.selection;if(n&&n.length){var o=e.x-s.pointer.x,a=e.y-s.pointer.y;n.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+a))}),this.moving||(this.moving=!0,this.start())}else{var r=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+r,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},Graph.prototype._onDragEnd=function(){this.drag.dragging=!1;var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},Graph.prototype._onTap=function(t){var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleTap(e)},Graph.prototype._onDoubleTap=function(t){var e=this._getPointer(t.gesture.center);this._handleDoubleTap(e)},Graph.prototype._onHold=function(t){var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleOnHold(e)},Graph.prototype._onRelease=function(t){var e=this._getPointer(t.gesture.center);this._handleOnRelease(e)},Graph.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},Graph.prototype._zoom=function(t,e){var i=this._getScale();1e-5>t&&(t=1e-5),t>10&&(t=10);var s=this._getTranslation(),n=t/i,o=(1-n)*e.x+s.x*n,a=(1-n)*e.y+s.y*n;return this.areaCenter={x:this._canvasToX(e.x),y:this._canvasToY(e.y)},this._setScale(t),this._setTranslation(o,a),this.updateClustersDefault(),this._redraw(),t},Graph.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){var i=this._getScale(),s=e/10;0>e&&(s/=1-s),i*=1+s;var n=util.fakeGesture(this,t),o=this._getPointer(n.center);this._zoom(i,o)}t.preventDefault()},Graph.prototype._onMouseMoveTitle=function(t){var e=util.fakeGesture(this,t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var s=this,n=function(){s._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.drag.dragging||(this.popupTimer=setTimeout(n,this.constants.tooltip.delay))},Graph.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},s=this.popupNode;if(void 0==this.popupNode){var n=this.nodes;for(e in n)if(n.hasOwnProperty(e)){var o=n[e];if(void 0!==o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0===this.popupNode){var a=this.edges;for(e in a)if(a.hasOwnProperty(e)){var r=a[e];if(r.connected&&void 0!==r.getTitle()&&r.isOverlappingWith(i)){this.popupNode=r;break}}}if(this.popupNode){if(this.popupNode!=s){var h=this;h.popup||(h.popup=new Popup(h.frame,h.constants.tooltip)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},Graph.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},Graph.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight,void 0!==this.manipulationDiv&&(this.manipulationDiv.style.width=this.frame.canvas.clientWidth+"px"),void 0!==this.navigationDivs&&void 0!==this.navigationDivs.wrapper&&(this.navigationDivs.wrapper.style.width=this.frame.canvas.clientWidth+"px",this.navigationDivs.wrapper.style.height=this.frame.canvas.clientHeight+"px"),this.emit("resize",{width:this.frame.canvas.width,height:this.frame.canvas.height})},Graph.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof DataSet||t instanceof DataView)this.nodesData=t;else if(t instanceof Array)this.nodesData=new DataSet,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new DataSet}if(e&&util.forEach(this.nodesListeners,function(t,i){e.off(i,t)}),this.nodes={},this.nodesData){var i=this;util.forEach(this.nodesListeners,function(t,e){i.nodesData.on(e,t)});var s=this.nodesData.getIds();this._addNodes(s)}this._updateSelection()},Graph.prototype._addNodes=function(t){for(var e,i=0,s=t.length;s>i;i++){e=t[i];var n=this.nodesData.get(e),o=new Node(n,this.images,this.groups,this.constants);if(this.nodes[e]=o,!(0!=o.xFixed&&0!=o.yFixed||null!==o.x&&null!==o.y)){var a=1*t.length,r=2*Math.PI*Math.random();0==o.xFixed&&(o.x=a*Math.cos(r)),0==o.yFixed&&(o.y=a*Math.sin(r))}this.moving=!0}this._updateNodeIndexList(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes(),this._reconnectEdges(),this._updateValueRange(this.nodes),this.updateLabels()},Graph.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,s=0,n=t.length;n>s;s++){var o=t[s],a=e[o],r=i.get(o);a?a.setProperties(r,this.constants):(a=new Node(properties,this.images,this.groups,this.constants),e[o]=a,a.isFixed()||(this.moving=!0))}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(e)},Graph.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,s=t.length;s>i;i++){var n=t[i];delete e[n]}this._updateNodeIndexList(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes(),this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},Graph.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof DataSet||t instanceof DataView)this.edgesData=t;else if(t instanceof Array)this.edgesData=new DataSet,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new DataSet}if(e&&util.forEach(this.edgesListeners,function(t,i){e.off(i,t)}),this.edges={},this.edgesData){var i=this;util.forEach(this.edgesListeners,function(t,e){i.edgesData.on(e,t)});var s=this.edgesData.getIds();this._addEdges(s)}this._reconnectEdges()},Graph.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],a=e[o];a&&a.disconnect();var r=i.get(o,{showInternalIds:!0});e[o]=new Edge(r,this,this.constants)}this.moving=!0,this._updateValueRange(e),this._createBezierNodes(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes()},Graph.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],a=i.get(o),r=e[o];r?(r.disconnect(),r.setProperties(a,this.constants),r.connect()):(r=new Edge(a,this,this.constants),this.edges[o]=r)}this._createBezierNodes(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this.moving=!0,this._updateValueRange(e)},Graph.prototype._removeEdges=function(t){for(var e=this.edges,i=0,s=t.length;s>i;i++){var n=t[i],o=e[n];o&&(null!=o.via&&delete this.sectors.support.nodes[o.via.id],o.disconnect(),delete e[n])}this.moving=!0,this._updateValueRange(e),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes()},Graph.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var s=i[t];s.from=null,s.to=null,s.connect()}},Graph.prototype._updateValueRange=function(t){var e,i=void 0,s=void 0;for(e in t)if(t.hasOwnProperty(e)){var n=t[e].getValue();void 0!==n&&(i=void 0===i?n:Math.min(n,i),s=void 0===s?n:Math.max(n,s))}if(void 0!==i&&void 0!==s)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,s)},Graph.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},Graph.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this.canvasTopLeft={x:this._canvasToX(0),y:this._canvasToY(0)},this.canvasBottomRight={x:this._canvasToX(this.frame.canvas.clientWidth),y:this._canvasToY(this.frame.canvas.clientHeight)},this._doInAllSectors("_drawAllSectorNodes",t),this._doInAllSectors("_drawEdges",t),this._doInAllSectors("_drawNodes",t,!1),t.restore()},Graph.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},Graph.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},Graph.prototype._setScale=function(t){this.scale=t},Graph.prototype._getScale=function(){return this.scale},Graph.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},Graph.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},Graph.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},Graph.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},Graph.prototype._drawNodes=function(t,e){void 0===e&&(e=!1);var i=this.nodes,s=[];for(var n in i)i.hasOwnProperty(n)&&(i[n].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight),i[n].isSelected()?s.push(n):(i[n].inArea()||e)&&i[n].draw(t));for(var o=0,a=s.length;a>o;o++)(i[s[o]].inArea()||e)&&i[s[o]].draw(t)},Graph.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var s=e[i];s.setScale(this.scale),s.connected&&e[i].draw(t)}},Graph.prototype._stabilize=function(){1==this.constants.freezeForStabilization&&this._freezeDefinedNodes();for(var t=0;this.moving&&t0)for(t in i)i.hasOwnProperty(t)&&(i[t].discreteStepLimited(e,this.constants.maxVelocity),s=!0);else for(t in i)i.hasOwnProperty(t)&&(i[t].discreteStep(e),s=!0);if(1==s){var n=this.constants.minVelocity/Math.max(this.scale,.05);this.moving=n>.5*this.constants.maxVelocity?!0:this._isMoving(n)}},Graph.prototype._physicsTick=function(){this.freezeSimulation||this.moving&&(this._doInAllActiveSectors("_initializeForceCalculation"),this._doInAllActiveSectors("_discreteStepNodes"),this.constants.smoothCurves&&this._doInSupportSector("_discreteStepNodes"),this._findCenter(this._getRange()))},Graph.prototype._animationStep=function(){this.timer=void 0,this._handleNavigation(),this.start();var t=Date.now(),e=1;this._physicsTick();for(var i=Date.now()-t;is;++s)i[s].apply(this,e)}return this},i.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks[t]||[]},i.prototype.hasListeners=function(t){return!!this.listeners(t).length}},{}],3:[function(t,e){!function(t,i){"use strict";function s(){if(!n.READY){n.event.determineEventTypes();for(var t in n.gestures)n.gestures.hasOwnProperty(t)&&n.detection.register(n.gestures[t]);n.event.onTouch(n.DOCUMENT,n.EVENT_MOVE,n.detection.detect),n.event.onTouch(n.DOCUMENT,n.EVENT_END,n.detection.detect),n.READY=!0}}var n=function(t,e){return new n.Instance(t,e||{})};n.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},n.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,n.HAS_TOUCHEVENTS="ontouchstart"in t,n.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,n.NO_MOUSEEVENTS=n.HAS_TOUCHEVENTS&&navigator.userAgent.match(n.MOBILE_REGEX),n.EVENT_TYPES={},n.DIRECTION_DOWN="down",n.DIRECTION_LEFT="left",n.DIRECTION_UP="up",n.DIRECTION_RIGHT="right",n.POINTER_MOUSE="mouse",n.POINTER_TOUCH="touch",n.POINTER_PEN="pen",n.EVENT_START="start",n.EVENT_MOVE="move",n.EVENT_END="end",n.DOCUMENT=document,n.plugins={},n.READY=!1,n.Instance=function(t,e){var i=this;return s(),this.element=t,this.enabled=!0,this.options=n.utils.extend(n.utils.extend({},n.defaults),e||{}),this.options.stop_browser_behavior&&n.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),n.event.onTouch(t,n.EVENT_START,function(t){i.enabled&&n.detection.startDetect(i,t)}),this},n.Instance.prototype={on:function(t,e){for(var i=t.split(" "),s=0;s0&&e==n.EVENT_END?e=n.EVENT_MOVE:c||(e=n.EVENT_END),c||null===o?o=h:h=o,i.call(n.detection,s.collectEventData(t,e,h)),n.HAS_POINTEREVENTS&&e==n.EVENT_END&&(c=n.PointerEvent.updatePointer(e,h))),c||(o=null,a=!1,r=!1,n.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=n.HAS_POINTEREVENTS?n.PointerEvent.getEvents():n.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],n.EVENT_TYPES[n.EVENT_START]=t[0],n.EVENT_TYPES[n.EVENT_MOVE]=t[1],n.EVENT_TYPES[n.EVENT_END]=t[2]
+},getTouchList:function(t){return n.HAS_POINTEREVENTS?n.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var s=this.getTouchList(i,e),o=n.POINTER_TOUCH;return(i.type.match(/mouse/)||n.PointerEvent.matchType(n.POINTER_MOUSE,i))&&(o=n.POINTER_MOUSE),{center:n.utils.getCenter(s),timeStamp:(new Date).getTime(),target:i.target,touches:s,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return n.detection.stopDetect()}}}},n.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==n.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[n.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==n.POINTER_MOUSE,i[n.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==n.POINTER_TOUCH,i[n.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==n.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},n.utils={extend:function(t,e,s){for(var n in e)t[n]!==i&&s||(t[n]=e[n]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],s=0,n=t.length;n>s;s++)e.push(t[s].pageX),i.push(t[s].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,s=e.pageX-t.pageX;return 180*Math.atan2(i,s)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),s=Math.abs(t.pageY-e.pageY);return i>=s?t.pageX-e.pageX>0?n.DIRECTION_LEFT:n.DIRECTION_RIGHT:t.pageY-e.pageY>0?n.DIRECTION_UP:n.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,s=e.pageY-t.pageY;return Math.sqrt(i*i+s*s)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==n.DIRECTION_UP||t==n.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,s=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var n=0;ni;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==n.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=n.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,s=t.touches.length;s>i;i++)e.touches.push(n.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,a=t.center.pageX-e.center.pageX,r=t.center.pageY-e.center.pageY,h=n.utils.getVelocity(o,a,r);return n.utils.extend(t,{deltaTime:o,deltaX:a,deltaY:r,velocityX:h.x,velocityY:h.y,distance:n.utils.getDistance(e.center,t.center),angle:n.utils.getAngle(e.center,t.center),direction:n.utils.getDirection(e.center,t.center),scale:n.utils.getScale(e.touches,t.touches),rotation:n.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),n.utils.extend(n.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},n.gestures=n.gestures||{},n.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case n.EVENT_START:clearTimeout(this.timer),n.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==n.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case n.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case n.EVENT_END:clearTimeout(this.timer)}}},n.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==n.EVENT_END){var i=n.detection.previous,s=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},n.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(n.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),void(this.triggered=!1);if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case n.EVENT_START:this.triggered=!1;break;case n.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case n.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},n.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==n.POINTER_MOUSE?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==n.EVENT_START&&e.trigger(this.name,t)))}},n.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==n.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=n:(t.Hammer=n,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return n}))}(this)},{}],4:[function(t,e){var i="undefined"!=typeof self?self:"undefined"!=typeof window?window:{};(function(s){function n(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function o(t,e){function i(){le.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}var s=!0;return l(function(){return s&&(i(),s=!1),e.apply(this,arguments)},e)}function a(t,e){return function(i){return g(t.call(this,i),e)}}function r(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function h(){}function d(t){C(t),l(this,t)}function c(t){var e=b(t),i=e.year||0,s=e.quarter||0,n=e.month||0,o=e.week||0,a=e.day||0,r=e.hour||0,h=e.minute||0,d=e.second||0,c=e.millisecond||0;this._milliseconds=+c+1e3*d+6e4*h+36e5*r,this._days=+a+7*o,this._months=+n+3*s+12*i,this._data={},this._bubble()}function l(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function u(t){var e,i={};for(e in t)t.hasOwnProperty(e)&&Te.hasOwnProperty(e)&&(i[e]=t[e]);return i}function p(t){return 0>t?Math.ceil(t):Math.floor(t)}function g(t,e,i){for(var s=""+Math.abs(t),n=t>=0;s.lengths;s++)(i&&t[s]!==e[s]||!i&&w(t[s])!==w(e[s]))&&a++;return a+o}function _(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=ti[t]||ei[e]||e}return t}function b(t){var e,i,s={};for(i in t)t.hasOwnProperty(i)&&(e=_(i),e&&(s[e]=t[i]));return s}function S(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}le[t]=function(n,o){var a,r,h=le.fn._lang[t],d=[];if("number"==typeof n&&(o=n,n=s),r=function(t){var e=le().utc().set(i,t);return h.call(le.fn._lang,e,n||"")},null!=o)return r(o);for(a=0;e>a;a++)d.push(r(a));return d}}function w(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function x(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function E(t,e,i){return ee(le([t,11,31+e-i]),e,i).week}function T(t){return D(t)?366:365}function D(t){return t%4===0&&t%100!==0||t%400===0}function C(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[ye]<0||t._a[ye]>11?ye:t._a[_e]<1||t._a[_e]>x(t._a[ve],t._a[ye])?_e:t._a[be]<0||t._a[be]>23?be:t._a[Se]<0||t._a[Se]>59?Se:t._a[we]<0||t._a[we]>59?we:t._a[xe]<0||t._a[xe]>999?xe:-1,t._pf._overflowDayOfYear&&(ve>e||e>_e)&&(e=_e),t._pf.overflow=e)}function I(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function M(t){return t?t.toLowerCase().replace("_","-"):t}function N(t,e){return e._isUTC?le(t).zone(e._offset||0):le(t).local()}function O(t,e){return e.abbr=t,Ee[t]||(Ee[t]=new h),Ee[t].set(e),Ee[t]}function L(t){delete Ee[t]}function P(e){var i,s,n,o,a=0,r=function(e){if(!Ee[e]&&De)try{t("./lang/"+e)}catch(i){}return Ee[e]};if(!e)return le.fn._lang;if(!f(e)){if(s=r(e))return s;e=[e]}for(;a0;){if(s=r(o.slice(0,i).join("-")))return s;if(n&&n.length>=i&&y(o,n,!0)>=i-1)break;i--}a++}return le.fn._lang}function k(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function A(t){var e,i,s=t.match(Ne);for(e=0,i=s.length;i>e;e++)s[e]=oi[s[e]]?oi[s[e]]:k(s[e]);return function(n){var o="";for(e=0;i>e;e++)o+=s[e]instanceof Function?s[e].call(n,t):s[e];return o}}function z(t,e){return t.isValid()?(e=F(e,t.lang()),ii[e]||(ii[e]=A(e)),ii[e](t)):t.lang().invalidDate()}function F(t,e){function i(t){return e.longDateFormat(t)||t}var s=5;for(Oe.lastIndex=0;s>=0&&Oe.test(t);)t=t.replace(Oe,i),Oe.lastIndex=0,s-=1;return t}function R(t,e){var i,s=e._strict;switch(t){case"Q":return We;case"DDDD":return je;case"YYYY":case"GGGG":case"gggg":return s?Ve:ke;case"Y":case"G":case"g":return Xe;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return s?Ue:Ae;case"S":if(s)return We;case"SS":if(s)return Be;case"SSS":if(s)return je;case"DDD":return Pe;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Fe;case"a":case"A":return P(e._l)._meridiemParse;case"X":return Ye;case"Z":case"ZZ":return Re;case"T":return He;case"SSSS":return ze;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return s?Be:Le;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Le;case"Do":return Ge;default:return i=new RegExp(U(V(t.replace("\\","")),"i"))}}function H(t){t=t||"";var e=t.match(Re)||[],i=e[e.length-1]||[],s=(i+"").match(Je)||["-",0,0],n=+(60*s[1])+w(s[2]);return"+"===s[0]?-n:n}function Y(t,e,i){var s,n=i._a;switch(t){case"Q":null!=e&&(n[ye]=3*(w(e)-1));break;case"M":case"MM":null!=e&&(n[ye]=w(e)-1);break;case"MMM":case"MMMM":s=P(i._l).monthsParse(e),null!=s?n[ye]=s:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(n[_e]=w(e));break;case"Do":null!=e&&(n[_e]=w(parseInt(e,10)));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=w(e));break;case"YY":n[ve]=le.parseTwoDigitYear(e);break;case"YYYY":case"YYYYY":case"YYYYYY":n[ve]=w(e);break;case"a":case"A":i._isPm=P(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":n[be]=w(e);break;case"m":case"mm":n[Se]=w(e);break;case"s":case"ss":n[we]=w(e);break;case"S":case"SS":case"SSS":case"SSSS":n[xe]=w(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=H(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function G(t){var e,i,s,n,o,a,r,h,d,c,l=[];if(!t._d){for(s=B(t),t._w&&null==t._a[_e]&&null==t._a[ye]&&(o=function(e){var i=parseInt(e,10);return e?e.length<3?i>68?1900+i:2e3+i:i:null==t._a[ve]?le().weekYear():t._a[ve]},a=t._w,null!=a.GG||null!=a.W||null!=a.E?r=ie(o(a.GG),a.W||1,a.E,4,1):(h=P(t._l),d=null!=a.d?J(a.d,h):null!=a.e?parseInt(a.e,10)+h._week.dow:0,c=parseInt(a.w,10)||1,null!=a.d&&dT(n)&&(t._pf._overflowDayOfYear=!0),i=$(n,0,t._dayOfYear),t._a[ye]=i.getUTCMonth(),t._a[_e]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=l[e]=s[e];for(;7>e;e++)t._a[e]=l[e]=null==t._a[e]?2===e?1:0:t._a[e];l[be]+=w((t._tzm||0)/60),l[Se]+=w((t._tzm||0)%60),t._d=(t._useUTC?$:K).apply(null,l)}}function W(t){var e;t._d||(e=b(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],G(t))}function B(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function j(t){t._a=[],t._pf.empty=!0;var e,i,s,n,o,a=P(t._l),r=""+t._i,h=r.length,d=0;for(s=F(t._f,a).match(Ne)||[],e=0;e0&&t._pf.unusedInput.push(o),r=r.slice(r.indexOf(i)+i.length),d+=i.length),oi[n]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(n),Y(n,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(n);t._pf.charsLeftOver=h-d,r.length>0&&t._pf.unusedInput.push(r),t._isPm&&t._a[be]<12&&(t._a[be]+=12),t._isPm===!1&&12===t._a[be]&&(t._a[be]=0),G(t),C(t)}function V(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,s,n){return e||i||s||n})}function U(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function X(t){var e,i,s,o,a;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(o=0;oa)&&(s=a,i=e));l(t,i||e)}function q(t){var e,i,s=t._i,n=qe.exec(s);if(n){for(t._pf.iso=!0,e=0,i=Ke.length;i>e;e++)if(Ke[e][1].exec(s)){t._f=Ke[e][0]+(n[6]||" ");break}for(e=0,i=$e.length;i>e;e++)if($e[e][1].exec(s)){t._f+=$e[e][0];break}s.match(Re)&&(t._f+="Z"),j(t)}else le.createFromInputFallback(t)}function Z(t){var e=t._i,i=Ce.exec(e);e===s?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?q(t):f(e)?(t._a=e.slice(0),G(t)):v(e)?t._d=new Date(+e):"object"==typeof e?W(t):"number"==typeof e?t._d=new Date(e):le.createFromInputFallback(t)}function K(t,e,i,s,n,o,a){var r=new Date(t,e,i,s,n,o,a);return 1970>t&&r.setFullYear(t),r}function $(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function J(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function Q(t,e,i,s,n){return n.relativeTime(e||1,!!i,t,s)}function te(t,e,i){var s=fe(Math.abs(t)/1e3),n=fe(s/60),o=fe(n/60),a=fe(o/24),r=fe(a/365),h=45>s&&["s",s]||1===n&&["m"]||45>n&&["mm",n]||1===o&&["h"]||22>o&&["hh",o]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",fe(a/30)]||1===r&&["y"]||["yy",r];return h[2]=e,h[3]=t>0,h[4]=i,Q.apply({},h)}function ee(t,e,i){var s,n=i-e,o=i-t.day();return o>n&&(o-=7),n-7>o&&(o+=7),s=le(t).add("d",o),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function ie(t,e,i,s,n){var o,a,r=$(t,0,1).getUTCDay();return i=null!=i?i:n,o=n-r+(r>s?7:0)-(n>r?7:0),a=7*(e-1)+(i-n)+o+1,{year:a>0?t:t-1,dayOfYear:a>0?a:T(t-1)+a}}function se(t){var e=t._i,i=t._f;return null===e||i===s&&""===e?le.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=P().preparse(e)),le.isMoment(e)?(t=u(e),t._d=new Date(+e._d)):i?f(i)?X(t):j(t):Z(t),new d(t))}function ne(t,e){var i;return"string"==typeof e&&(e=t.lang().monthsParse(e),"number"!=typeof e)?t:(i=Math.min(t.date(),x(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,i),t)}function oe(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function ae(t,e,i){return"Month"===e?ne(t,i):t._d["set"+(t._isUTC?"UTC":"")+e](i)}function re(t,e){return function(i){return null!=i?(ae(this,t,i),le.updateOffset(this,e),this):oe(this,t)}}function he(t){le.duration.fn[t]=function(){return this._data[t]}}function de(t,e){le.duration.fn["as"+t]=function(){return+this/e}}function ce(t){"undefined"==typeof ender&&(ue=me.moment,me.moment=t?o("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.",le):le)}for(var le,ue,pe,ge="2.6.0",me="undefined"!=typeof i?i:this,fe=Math.round,ve=0,ye=1,_e=2,be=3,Se=4,we=5,xe=6,Ee={},Te={_isAMomentObject:null,_i:null,_f:null,_l:null,_strict:null,_isUTC:null,_offset:null,_pf:null,_lang:null},De="undefined"!=typeof e&&e.exports,Ce=/^\/?Date\((\-?\d+)/i,Ie=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Me=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Ne=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,Oe=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Le=/\d\d?/,Pe=/\d{1,3}/,ke=/\d{1,4}/,Ae=/[+\-]?\d{1,6}/,ze=/\d+/,Fe=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Re=/Z|[\+\-]\d\d:?\d\d/gi,He=/T/i,Ye=/[\+\-]?\d+(\.\d{1,3})?/,Ge=/\d{1,2}/,We=/\d/,Be=/\d\d/,je=/\d{3}/,Ve=/\d{4}/,Ue=/[+-]?\d{6}/,Xe=/[+-]?\d+/,qe=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ze="YYYY-MM-DDTHH:mm:ssZ",Ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],$e=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Je=/([\+\-]|\d\d)/gi,Qe=("Date|Hours|Minutes|Seconds|Milliseconds".split("|"),{Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6}),ti={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",Q:"quarter",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},ei={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},ii={},si="DDD w W M D d".split(" "),ni="M D H h m s w W".split(" "),oi={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return g(this.year()%100,2)},YYYY:function(){return g(this.year(),4)},YYYYY:function(){return g(this.year(),5)},YYYYYY:function(){var t=this.year(),e=t>=0?"+":"-";return e+g(Math.abs(t),6)},gg:function(){return g(this.weekYear()%100,2)},gggg:function(){return g(this.weekYear(),4)},ggggg:function(){return g(this.weekYear(),5)},GG:function(){return g(this.isoWeekYear()%100,2)},GGGG:function(){return g(this.isoWeekYear(),4)},GGGGG:function(){return g(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return w(this.milliseconds()/100)},SS:function(){return g(w(this.milliseconds()/10),2)},SSS:function(){return g(this.milliseconds(),3)},SSSS:function(){return g(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(w(t/60),2)+":"+g(w(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(w(t/60),2)+g(w(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ai=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];si.length;)pe=si.pop(),oi[pe+"o"]=r(oi[pe],pe);for(;ni.length;)pe=ni.pop(),oi[pe+pe]=a(oi[pe],2);for(oi.DDDD=a(oi.DDD,3),l(h.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=le.utc([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=le([2e3,1]).day(e),s="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,s){var n=this._relativeTime[i];return"function"==typeof n?n(t,e,i,s):n.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return ee(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),le=function(t,e,i,o){var a;return"boolean"==typeof i&&(o=i,i=s),a={},a._isAMomentObject=!0,a._i=t,a._f=e,a._l=i,a._strict=o,a._isUTC=!1,a._pf=n(),se(a)},le.suppressDeprecationWarnings=!1,le.createFromInputFallback=o("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i)}),le.utc=function(t,e,i,o){var a;return"boolean"==typeof i&&(o=i,i=s),a={},a._isAMomentObject=!0,a._useUTC=!0,a._isUTC=!0,a._l=i,a._i=t,a._f=e,a._strict=o,a._pf=n(),se(a).utc()},le.unix=function(t){return le(1e3*t)},le.duration=function(t,e){var i,s,n,o=t,a=null;return le.isDuration(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(a=Ie.exec(t))?(i="-"===a[1]?-1:1,o={y:0,d:w(a[_e])*i,h:w(a[be])*i,m:w(a[Se])*i,s:w(a[we])*i,ms:w(a[xe])*i}):(a=Me.exec(t))&&(i="-"===a[1]?-1:1,n=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},o={y:n(a[2]),M:n(a[3]),d:n(a[4]),h:n(a[5]),m:n(a[6]),s:n(a[7]),w:n(a[8])}),s=new c(o),le.isDuration(t)&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},le.version=ge,le.defaultFormat=Ze,le.momentProperties=Te,le.updateOffset=function(){},le.lang=function(t,e){var i;return t?(e?O(M(t),e):null===e?(L(t),t="en"):Ee[t]||P(t),i=le.duration.fn._lang=le.fn._lang=P(t),i._abbr):le.fn._lang._abbr},le.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),P(t)},le.isMoment=function(t){return t instanceof d||null!=t&&t.hasOwnProperty("_isAMomentObject")},le.isDuration=function(t){return t instanceof c},pe=ai.length-1;pe>=0;--pe)S(ai[pe]);le.normalizeUnits=function(t){return _(t)},le.invalid=function(t){var e=le.utc(0/0);return null!=t?l(e._pf,t):e._pf.userInvalidated=!0,e},le.parseZone=function(){return le.apply(null,arguments).parseZone()},le.parseTwoDigitYear=function(t){return w(t)+(w(t)>68?1900:2e3)},l(le.fn=d.prototype,{clone:function(){return le(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=le(this).utc();return 00:!1},parsingFlags:function(){return l({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=z(this,t||le.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?le.duration(+e,t):le.duration(t,e),m(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?le.duration(+e,t):le.duration(t,e),m(this,i,-1),this},diff:function(t,e,i){var s,n,o=N(t,this),a=6e4*(this.zone()-o.zone());return e=_(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+o.daysInMonth()),n=12*(this.year()-o.year())+(this.month()-o.month()),n+=(this-le(this).startOf("month")-(o-le(o).startOf("month")))/s,n-=6e4*(this.zone()-le(this).startOf("month").zone()-(o.zone()-le(o).startOf("month").zone()))/s,"year"===e&&(n/=12)):(s=this-o,n="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),i?n:p(n)},from:function(t,e){return le.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(le(),t)},calendar:function(){var t=N(le(),this).startOf("day"),e=this.diff(t,"days",!0),i=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(this.lang().calendar(i,this))},isLeapYear:function(){return D(this.year())},isDST:function(){return this.zone()+le(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+le(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+N(t,this).startOf(e)
+},min:function(t){return t=le.apply(null,arguments),this>t?this:t},max:function(t){return t=le.apply(null,arguments),t>this?this:t},zone:function(t,e){var i=this._offset||0;return null==t?this._isUTC?i:this._d.getTimezoneOffset():("string"==typeof t&&(t=H(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,i!==t&&(!e||this._changeInProgress?m(this,le.duration(i-t,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,le.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?le(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return x(this.year(),this.month())},dayOfYear:function(t){var e=fe((le(this).startOf("day")-le(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},weekYear:function(t){var e=ee(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=ee(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=ee(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var t=this._lang._week;return E(this.year(),t.dow,t.doy)},get:function(t){return t=_(t),this[t]()},set:function(t,e){return t=_(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===s?this._lang:(this._lang=P(t),this)}}),le.fn.millisecond=le.fn.milliseconds=re("Milliseconds",!1),le.fn.second=le.fn.seconds=re("Seconds",!1),le.fn.minute=le.fn.minutes=re("Minutes",!1),le.fn.hour=le.fn.hours=re("Hours",!0),le.fn.date=re("Date",!0),le.fn.dates=o("dates accessor is deprecated. Use date instead.",re("Date",!0)),le.fn.year=re("FullYear",!0),le.fn.years=o("years accessor is deprecated. Use year instead.",re("FullYear",!0)),le.fn.days=le.fn.day,le.fn.months=le.fn.month,le.fn.weeks=le.fn.week,le.fn.isoWeeks=le.fn.isoWeek,le.fn.quarters=le.fn.quarter,le.fn.toJSON=le.fn.toISOString,l(le.duration.fn=c.prototype,{_bubble:function(){var t,e,i,s,n=this._milliseconds,o=this._days,a=this._months,r=this._data;r.milliseconds=n%1e3,t=p(n/1e3),r.seconds=t%60,e=p(t/60),r.minutes=e%60,i=p(e/60),r.hours=i%24,o+=p(i/24),r.days=o%30,a+=p(o/30),r.months=a%12,s=p(a/12),r.years=s},weeks:function(){return p(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*w(this._months/12)},humanize:function(t){var e=+this,i=te(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},add:function(t,e){var i=le.duration(t,e);return this._milliseconds+=i._milliseconds,this._days+=i._days,this._months+=i._months,this._bubble(),this},subtract:function(t,e){var i=le.duration(t,e);return this._milliseconds-=i._milliseconds,this._days-=i._days,this._months-=i._months,this._bubble(),this},get:function(t){return t=_(t),this[t.toLowerCase()+"s"]()},as:function(t){return t=_(t),this["as"+t.charAt(0).toUpperCase()+t.slice(1)+"s"]()},lang:le.fn.lang,toIsoString:function(){var t=Math.abs(this.years()),e=Math.abs(this.months()),i=Math.abs(this.days()),s=Math.abs(this.hours()),n=Math.abs(this.minutes()),o=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(t?t+"Y":"")+(e?e+"M":"")+(i?i+"D":"")+(s||n||o?"T":"")+(s?s+"H":"")+(n?n+"M":"")+(o?o+"S":""):"P0D"}});for(pe in Qe)Qe.hasOwnProperty(pe)&&(de(pe,Qe[pe]),he(pe.toLowerCase()));de("Weeks",6048e5),le.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},le.lang("en",{ordinal:function(t){var e=t%10,i=1===w(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),De?e.exports=le:"function"==typeof define&&define.amd?(define("moment",function(t,e,i){return i.config&&i.config()&&i.config().noGlobal===!0&&(me.moment=ue),le}),ce(!0)):ce()}).call(this)},{}],5:[function(t,e){function i(t,e,i){return t.addEventListener?t.addEventListener(e,i,!1):void t.attachEvent("on"+e,i)}function s(t){return"keypress"==t.type?String.fromCharCode(t.which):S[t.which]?S[t.which]:w[t.which]?w[t.which]:String.fromCharCode(t.which).toLowerCase()}function n(t){var e=t.target||t.srcElement,i=e.tagName;return(" "+e.className+" ").indexOf(" mousetrap ")>-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function o(t,e){return t.sort().join(",")===e.sort().join(",")}function a(t){t=t||{};var e,i=!1;for(e in C)t[e]?i=!0:C[e]=0;i||(M=!1)}function r(t,e,i,s,n){var a,r,h=[];if(!T[t])return[];for("keyup"==i&&u(t)&&(e=[t]),a=0;a95&&112>t||S.hasOwnProperty(t)&&(_[S[t]]=t)}return _}function m(t,e,i){return i||(i=g()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function f(t,e,i,n){C[t]=0,n||(n=m(e[0],[]));var o,r=function(){M=n,++C[t],p()},h=function(t){d(i,t),"keyup"!==n&&(I=s(t)),setTimeout(a,10)};for(o=0;o1)return f(t,d,e,i);for(h="+"===t?["+"]:t.split("+"),o=0;o":".","?":"/","|":"\\"},E={option:"alt",command:"meta","return":"enter",escape:"esc"},T={},D={},C={},I=!1,M=!1,N=1;20>N;++N)S[111+N]="f"+N;for(N=0;9>=N;++N)S[N+96]=N;i(document,"keypress",l),i(document,"keydown",l),i(document,"keyup",l);var O={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),D[t+":"+i]=e,this},unbind:function(t,e){return D[t+":"+e]&&(delete D[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return D[t+":"+e](),this},reset:function(){return T={},D={},this}};e.exports=O},{}]},{},[1])(1)});
\ No newline at end of file
diff --git a/docs/dataset.html b/docs/dataset.html
index cbb2ec8f..477d26a4 100644
--- a/docs/dataset.html
+++ b/docs/dataset.html
@@ -107,7 +107,7 @@ console.log('formatted items', items);
-var data = new vis.DataSet(options)
+var data = new vis.DataSet([data] [, options])
@@ -116,6 +116,11 @@ var data = new vis.DataSet(options)
Data Manipulation.
+
+ The parameter datacode> is optional and can be an Array or
+ Google DataTable with items.
+
+
The parameter options is optional and is an object which can
contain the following properties:
diff --git a/docs/timeline.html b/docs/timeline.html
index 70243349..8a664610 100644
--- a/docs/timeline.html
+++ b/docs/timeline.html
@@ -456,6 +456,7 @@ var options = {
+