diff --git a/dist/vis.js b/dist/vis.js
index 65b70e3a..89956db4 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 3.5.0
- * @date 2014-10-09
+ * @date 2014-10-10
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -100,32 +100,32 @@ return /******/ (function(modules) { // webpackBootstrap
// Timeline
exports.Timeline = __webpack_require__(17);
- exports.Graph2d = __webpack_require__(39);
+ exports.Graph2d = __webpack_require__(33);
exports.timeline = {
DateUtil: __webpack_require__(23),
- DataStep: __webpack_require__(42),
+ DataStep: __webpack_require__(36),
Range: __webpack_require__(20),
- stack: __webpack_require__(45),
+ stack: __webpack_require__(39),
TimeStep: __webpack_require__(26),
components: {
items: {
- Item: __webpack_require__(33),
- BackgroundItem: __webpack_require__(36),
- BoxItem: __webpack_require__(32),
- PointItem: __webpack_require__(34),
- RangeItem: __webpack_require__(35)
+ Item: __webpack_require__(40),
+ BackgroundItem: __webpack_require__(41),
+ BoxItem: __webpack_require__(43),
+ PointItem: __webpack_require__(44),
+ RangeItem: __webpack_require__(42)
},
Component: __webpack_require__(22),
CurrentTime: __webpack_require__(27),
CustomTime: __webpack_require__(29),
- DataAxis: __webpack_require__(41),
- GraphGroup: __webpack_require__(43),
- Group: __webpack_require__(31),
+ DataAxis: __webpack_require__(35),
+ GraphGroup: __webpack_require__(37),
+ Group: __webpack_require__(45),
ItemSet: __webpack_require__(30),
- Legend: __webpack_require__(44),
- LineGraph: __webpack_require__(40),
+ Legend: __webpack_require__(38),
+ LineGraph: __webpack_require__(34),
TimeAxis: __webpack_require__(25)
}
};
@@ -12192,7 +12192,6 @@ return /******/ (function(modules) { // webpackBootstrap
var duration = DateUtil.getHiddenDuration(this.body.hiddenDates, this);
interval -= duration;
-
var width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height;
var diffRange = -delta / width * interval;
var newStart = this.props.touch.start + diffRange;
@@ -12860,9 +12859,8 @@ return /******/ (function(modules) { // webpackBootstrap
time = hidden.startDate;
}
- var res = exports.correctTimeForDuration(Core.body.hiddenDates, Core.range, time);
- var duration = res.duration;
- time = res.time;
+ var duration = exports.getHiddenDuration(Core.body.hiddenDates, Core.range);
+ time = exports.getHiddenTimeBefore(Core.body.hiddenDates, Core.range, time);
var conversion = Core.range.conversion(width, duration);
return (time.valueOf() - conversion.offset) * conversion.scale;
@@ -12878,10 +12876,13 @@ return /******/ (function(modules) { // webpackBootstrap
* @returns {Date}
*/
exports.toTime = function(body, range, x, width) {
- var duration = exports.getHiddenDuration(body.hiddenDates, range);
- var conversion = range.conversion(width, duration);
+ var hiddenDuration = exports.getHiddenDuration(body.hiddenDates, range);
+ var totalDuration = range.end - range.start - hiddenDuration;
+ var partialDuration = totalDuration * x / width;
+ var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(body.hiddenDates,range, partialDuration);
- return new Date(x / conversion.scale + conversion.offset);
+ var newTime = new Date(accumulatedHiddenDuration + partialDuration + range.start);
+ return newTime;
};
@@ -12908,32 +12909,63 @@ return /******/ (function(modules) { // webpackBootstrap
/**
* Support function
- * @param hiddenTimes
+ * @param hiddenDates
* @param range
* @param time
* @returns {{duration: number, time: *, offset: number}}
*/
- exports.correctTimeForDuration = function(hiddenTimes, range, time) {
- var duration = 0;
+ exports.getHiddenTimeBefore = function(hiddenDates, range, time) {
var timeOffset = 0;
time = moment(time).toDate().valueOf();
- for (var i = 0; i < hiddenTimes.length; i++) {
- var startDate = hiddenTimes[i].start;
- var endDate = hiddenTimes[i].end;
+ for (var i = 0; i < hiddenDates.length; i++) {
+ var startDate = hiddenDates[i].start;
+ var endDate = hiddenDates[i].end;
// if time after the cutout, and the
if (startDate >= range.start && endDate < range.end) {
- duration += (endDate - startDate);
if (time >= endDate) {
timeOffset += (endDate - startDate);
}
}
}
time -= timeOffset;
- return {duration: duration, time:time, offset: timeOffset};
+ return time;
+ };
+
+ /**
+ * Support function
+ * @param hiddenDates
+ * @param range
+ * @param time
+ * @returns {{duration: number, time: *, offset: number}}
+ */
+ exports.getAccumulatedHiddenDuration = function(hiddenDates, range, requiredDuration) {
+ var hiddenDuration = 0;
+ var duration = 0;
+ var previousPoint = range.start;
+ //exports.printDates(hiddenDates)
+ for (var i = 0; i < hiddenDates.length; i++) {
+ var startDate = hiddenDates[i].start;
+ var endDate = hiddenDates[i].end;
+ // if time after the cutout, and the
+ if (startDate >= range.start && endDate < range.end) {
+ duration += startDate - previousPoint;
+
+ previousPoint = endDate;
+ if (duration >= requiredDuration) {
+ break;
+ }
+ else {
+ hiddenDuration += endDate - startDate;
+ }
+ }
+ }
+
+ return hiddenDuration;
};
+
/**
* used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true
* @param hiddenTimes
@@ -13004,7 +13036,7 @@ return /******/ (function(modules) { // webpackBootstrap
var CurrentTime = __webpack_require__(27);
var CustomTime = __webpack_require__(29);
var ItemSet = __webpack_require__(30);
- var Activator = __webpack_require__(37);
+ var Activator = __webpack_require__(31);
var DateUtil = __webpack_require__(23);
/**
@@ -15189,11 +15221,12 @@ return /******/ (function(modules) { // webpackBootstrap
var DataSet = __webpack_require__(7);
var DataView = __webpack_require__(8);
var Component = __webpack_require__(22);
- var Group = __webpack_require__(31);
- var BoxItem = __webpack_require__(32);
- var PointItem = __webpack_require__(34);
- var RangeItem = __webpack_require__(35);
- var BackgroundItem = __webpack_require__(36);
+ var Group = __webpack_require__(45);
+ var BoxItem = __webpack_require__(43);
+ var PointItem = __webpack_require__(44);
+ var RangeItem = __webpack_require__(42);
+ var BackgroundItem = __webpack_require__(41);
+ var DateUtil = __webpack_require__(23);
var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
@@ -16222,18 +16255,17 @@ return /******/ (function(modules) { // webpackBootstrap
return;
}
- var item = this.touchParams.item || null,
- me = this,
- props;
+ var item = this.touchParams.item || null;
+ var me = this;
+ var props = {};
+ props.initialX = event.gesture.center.clientX;
if (item && item.selected) {
var dragLeftItem = event.target.dragLeftItem;
var dragRightItem = event.target.dragRightItem;
if (dragLeftItem) {
- props = {
- item: dragLeftItem
- };
+ props.item = dragLeftItem;
if (me.options.editable.updateTime) {
props.start = item.data.start.valueOf();
@@ -16245,9 +16277,7 @@ return /******/ (function(modules) { // webpackBootstrap
this.touchParams.itemProps = [props];
}
else if (dragRightItem) {
- props = {
- item: dragRightItem
- };
+ props.item = dragRightItem;
if (me.options.editable.updateTime) {
props.end = item.data.end.valueOf();
@@ -16261,9 +16291,7 @@ return /******/ (function(modules) { // webpackBootstrap
else {
this.touchParams.itemProps = this.getSelection().map(function (id) {
var item = me.items[id];
- var props = {
- item: item
- };
+ props.item = item;
if (me.options.editable.updateTime) {
if ('start' in item.data) props.start = item.data.start.valueOf();
@@ -16289,23 +16317,27 @@ return /******/ (function(modules) { // webpackBootstrap
ItemSet.prototype._onDrag = function (event) {
if (this.touchParams.itemProps) {
var me = this;
- var range = this.body.range;
var snap = this.body.util.snap || null;
- var deltaX = event.gesture.deltaX;
- var scale = (this.props.width / (range.end - range.start));
- var offset = deltaX / scale;
// move
this.touchParams.itemProps.forEach(function (props) {
var newProps = {};
-
- if ('start' in props) {
- var start = new Date(props.start + offset);
+ if ('start' in props && !('end' in props)) { // only start in props
+ var start = me.body.util.toTime(event.gesture.center.clientX)
newProps.start = snap ? snap(start) : start;
}
-
- if ('end' in props) {
+ else if ('start' in props) { // start and end in props
+ var current = me.body.util.toTime(event.gesture.center.clientX);
+ var initial = me.body.util.toTime(props.initialX);
+ var offset = current - initial;
+ var start = new Date(props.start + offset);
var end = new Date(props.end + offset);
+
+ newProps.start = snap ? snap(start) : start;
+ newProps.end = snap ? snap(end) : end;
+ }
+ else if ('end' in props) { // only end in props
+ var end = me.body.util.toTime(event.gesture.center.clientX);
newProps.end = snap ? snap(end) : end;
}
@@ -16608,4901 +16640,4922 @@ return /******/ (function(modules) { // webpackBootstrap
/* 31 */
/***/ function(module, exports, __webpack_require__) {
+ var mousetrap = __webpack_require__(32);
+ var Emitter = __webpack_require__(10);
+ var Hammer = __webpack_require__(18);
var util = __webpack_require__(1);
- var stack = __webpack_require__(45);
- var RangeItem = __webpack_require__(35);
- var DateUtil = __webpack_require__(23);
/**
- * @constructor Group
- * @param {Number | String} groupId
- * @param {Object} data
- * @param {ItemSet} itemSet
+ * Turn an element into an clickToUse element.
+ * When not active, the element has a transparent overlay. When the overlay is
+ * clicked, the mode is changed to active.
+ * When active, the element is displayed with a blue border around it, and
+ * the interactive contents of the element can be used. When clicked outside
+ * the element, the elements mode is changed to inactive.
+ * @param {Element} container
+ * @constructor
*/
- function Group (groupId, data, itemSet) {
- this.groupId = groupId;
- this.subgroups = {};
- this.visibleSubgroups = 0;
- this.itemSet = itemSet;
+ function Activator(container) {
+ this.active = false;
- this.dom = {};
- this.props = {
- label: {
- width: 0,
- height: 0
- }
+ this.dom = {
+ container: container
};
- this.className = null;
- this.items = {}; // items filtered by groupId of this group
- this.visibleItems = []; // items currently visible in window
- this.orderedItems = { // items sorted by start and by end
- byStart: [],
- byEnd: []
- };
+ this.dom.overlay = document.createElement('div');
+ this.dom.overlay.className = 'overlay';
- this._create();
+ this.dom.container.appendChild(this.dom.overlay);
- this.setData(data);
- }
+ this.hammer = Hammer(this.dom.overlay, {prevent_default: false});
+ this.hammer.on('tap', this._onTapOverlay.bind(this));
- /**
- * Create DOM elements for the group
- * @private
- */
- Group.prototype._create = function() {
- var label = document.createElement('div');
- label.className = 'vlabel';
- this.dom.label = label;
+ // block all touch events (except tap)
+ var me = this;
+ var events = [
+ 'touch', 'pinch',
+ 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
+ ];
+ events.forEach(function (event) {
+ me.hammer.on(event, function (event) {
+ event.stopPropagation();
+ });
+ });
- var inner = document.createElement('div');
- inner.className = 'inner';
- label.appendChild(inner);
- this.dom.inner = inner;
+ // attach a tap event to the window, in order to deactivate when clicking outside the timeline
+ this.windowHammer = Hammer(window, {prevent_default: false});
+ this.windowHammer.on('tap', function (event) {
+ // deactivate when clicked outside the container
+ if (!_hasParent(event.target, container)) {
+ me.deactivate();
+ }
+ });
- var foreground = document.createElement('div');
- foreground.className = 'group';
- foreground['timeline-group'] = this;
- this.dom.foreground = foreground;
+ // mousetrap listener only bounded when active)
+ this.escListener = this.deactivate.bind(this);
+ }
- this.dom.background = document.createElement('div');
- this.dom.background.className = 'group';
+ // turn into an event emitter
+ Emitter(Activator.prototype);
- this.dom.axis = document.createElement('div');
- this.dom.axis.className = 'group';
+ // The currently active activator
+ Activator.current = null;
- // create a hidden marker to detect when the Timelines container is attached
- // to the DOM, or the style of a parent of the Timeline is changed from
- // display:none is changed to visible.
- this.dom.marker = document.createElement('div');
- this.dom.marker.style.visibility = 'hidden'; // TODO: ask jos why this is not none?
- this.dom.marker.innerHTML = '?';
- this.dom.background.appendChild(this.dom.marker);
+ /**
+ * Destroy the activator. Cleans up all created DOM and event listeners
+ */
+ Activator.prototype.destroy = function () {
+ this.deactivate();
+
+ // remove dom
+ this.dom.overlay.parentNode.removeChild(this.dom.overlay);
+
+ // cleanup hammer instances
+ this.hammer = null;
+ this.windowHammer = null;
+ // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory)
};
/**
- * Set the group data for this group
- * @param {Object} data Group data, can contain properties content and className
+ * Activate the element
+ * Overlay is hidden, element is decorated with a blue shadow border
*/
- Group.prototype.setData = function(data) {
- // update contents
- var content = data && data.content;
- if (content instanceof Element) {
- this.dom.inner.appendChild(content);
- }
- else if (content !== undefined && content !== null) {
- this.dom.inner.innerHTML = content;
- }
- else {
- this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null
+ Activator.prototype.activate = function () {
+ // we allow only one active activator at a time
+ if (Activator.current) {
+ Activator.current.deactivate();
}
+ Activator.current = this;
- // update title
- this.dom.label.title = data && data.title || '';
-
- if (!this.dom.inner.firstChild) {
- util.addClassName(this.dom.inner, 'hidden');
- }
- else {
- util.removeClassName(this.dom.inner, 'hidden');
- }
+ this.active = true;
+ this.dom.overlay.style.display = 'none';
+ util.addClassName(this.dom.container, 'vis-active');
- // update className
- var className = data && data.className || null;
- if (className != this.className) {
- if (this.className) {
- util.removeClassName(this.dom.label, this.className);
- util.removeClassName(this.dom.foreground, this.className);
- util.removeClassName(this.dom.background, this.className);
- util.removeClassName(this.dom.axis, this.className);
- }
- util.addClassName(this.dom.label, className);
- util.addClassName(this.dom.foreground, className);
- util.addClassName(this.dom.background, className);
- util.addClassName(this.dom.axis, className);
- this.className = className;
- }
+ this.emit('change');
+ this.emit('activate');
- // update style
- if (this.style) {
- util.removeCssText(this.dom.label, this.style);
- this.style = null;
- }
- if (data && data.style) {
- util.addCssText(this.dom.label, data.style);
- this.style = data.style;
- }
+ // ugly hack: bind ESC after emitting the events, as the Network rebinds all
+ // keyboard events on a 'change' event
+ mousetrap.bind('esc', this.escListener);
};
/**
- * Get the width of the group label
- * @return {number} width
+ * Deactivate the element
+ * Overlay is displayed on top of the element
*/
- Group.prototype.getLabelWidth = function() {
- return this.props.label.width;
- };
+ Activator.prototype.deactivate = function () {
+ this.active = false;
+ this.dom.overlay.style.display = '';
+ util.removeClassName(this.dom.container, 'vis-active');
+ mousetrap.unbind('esc', this.escListener);
+ this.emit('change');
+ this.emit('deactivate');
+ };
/**
- * Repaint this group
- * @param {{start: number, end: number}} range
- * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
- * @param {boolean} [restack=false] Force restacking of all items
- * @return {boolean} Returns true if the group is resized
+ * Handle a tap event: activate the container
+ * @param event
+ * @private
*/
- Group.prototype.redraw = function(range, margin, restack) {
- var resized = false;
-
- this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
-
- // force recalculation of the height of the items when the marker height changed
- // (due to the Timeline being attached to the DOM or changed from display:none to visible)
- var markerHeight = this.dom.marker.clientHeight;
- if (markerHeight != this.lastMarkerHeight) {
- this.lastMarkerHeight = markerHeight;
+ Activator.prototype._onTapOverlay = function (event) {
+ // activate the container
+ this.activate();
+ event.stopPropagation();
+ };
- util.forEach(this.items, function (item) {
- item.dirty = true;
- if (item.displayed) item.redraw();
- });
-
- restack = true;
- }
-
- // reposition visible items vertically
- if (this.itemSet.options.stack) { // TODO: ugly way to access options...
- stack.stack(this.visibleItems, margin, restack);
- }
- else { // no stacking
- stack.nostack(this.visibleItems, margin, this.subgroups);
- }
-
- // recalculate the height of the group
- var height;
- var visibleItems = this.visibleItems;
- var visibleSubgroups = [];
- this.visibleSubgroups = 0;
- var me = this;
- if (visibleItems.length) {
- var min = visibleItems[0].top;
- var max = visibleItems[0].top + visibleItems[0].height;
- util.forEach(visibleItems, function (item) {
- min = Math.min(min, item.top);
- max = Math.max(max, (item.top + item.height));
- if (item.data.subgroup !== undefined) {
- me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height,item.height);
- if (visibleSubgroups.indexOf(item.data.subgroup) == -1){
- visibleSubgroups.push(item.data.subgroup);
- me.visibleSubgroups += 1;
- }
- }
- });
- if (min > margin.axis) {
- // there is an empty gap between the lowest item and the axis
- var offset = min - margin.axis;
- max -= offset;
- util.forEach(visibleItems, function (item) {
- item.top -= offset;
- });
+ /**
+ * Test whether the element has the requested parent element somewhere in
+ * its chain of parent nodes.
+ * @param {HTMLElement} element
+ * @param {HTMLElement} parent
+ * @returns {boolean} Returns true when the parent is found somewhere in the
+ * chain of parent nodes.
+ * @private
+ */
+ function _hasParent(element, parent) {
+ while (element) {
+ if (element === parent) {
+ return true
}
- height = max + margin.item.vertical / 2;
- }
- else {
- height = margin.axis + margin.item.vertical;
+ element = element.parentNode;
}
- height = Math.max(height, this.props.label.height);
-
- // calculate actual size and position
- var foreground = this.dom.foreground;
- this.top = foreground.offsetTop;
- this.left = foreground.offsetLeft;
- this.width = foreground.offsetWidth;
- resized = util.updateProperty(this, 'height', height) || resized;
-
- // recalculate size of label
- resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
- resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
+ return false;
+ }
- // apply new height
- this.dom.background.style.height = height + 'px';
- this.dom.foreground.style.height = height + 'px';
- this.dom.label.style.height = height + 'px';
+ module.exports = Activator;
- // update vertical position of items after they are re-stacked and the height of the group is calculated
- for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
- var item = this.visibleItems[i];
- item.repositionY(margin);
- }
- return resized;
- };
+/***/ },
+/* 32 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * Show this group: attach to the DOM
+ * Copyright 2012 Craig Campbell
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 1.1.2
+ * @url craig.is/killing/mice
*/
- Group.prototype.show = function() {
- if (!this.dom.label.parentNode) {
- this.itemSet.dom.labelSet.appendChild(this.dom.label);
- }
- if (!this.dom.foreground.parentNode) {
- this.itemSet.dom.foreground.appendChild(this.dom.foreground);
- }
+ /**
+ * mapping of special keycodes to their corresponding keys
+ *
+ * everything in this dictionary cannot use keypress events
+ * so it has to be here to map to the correct keycodes for
+ * keyup/keydown events
+ *
+ * @type {Object}
+ */
+ var _MAP = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'ins',
+ 46: 'del',
+ 91: 'meta',
+ 93: 'meta',
+ 224: 'meta'
+ },
- if (!this.dom.background.parentNode) {
- this.itemSet.dom.background.appendChild(this.dom.background);
- }
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ _KEYCODE_MAP = {
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111 : '/',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ },
- if (!this.dom.axis.parentNode) {
- this.itemSet.dom.axis.appendChild(this.dom.axis);
- }
- };
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ _SHIFT_MAP = {
+ '~': '`',
+ '!': '1',
+ '@': '2',
+ '#': '3',
+ '$': '4',
+ '%': '5',
+ '^': '6',
+ '&': '7',
+ '*': '8',
+ '(': '9',
+ ')': '0',
+ '_': '-',
+ '+': '=',
+ ':': ';',
+ '\"': '\'',
+ '<': ',',
+ '>': '.',
+ '?': '/',
+ '|': '\\'
+ },
- /**
- * Hide this group: remove from the DOM
- */
- Group.prototype.hide = function() {
- var label = this.dom.label;
- if (label.parentNode) {
- label.parentNode.removeChild(label);
- }
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ _SPECIAL_ALIASES = {
+ 'option': 'alt',
+ 'command': 'meta',
+ 'return': 'enter',
+ 'escape': 'esc'
+ },
- var foreground = this.dom.foreground;
- if (foreground.parentNode) {
- foreground.parentNode.removeChild(foreground);
- }
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ _REVERSE_MAP,
- var background = this.dom.background;
- if (background.parentNode) {
- background.parentNode.removeChild(background);
- }
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
+ *
+ * @type {Object}
+ */
+ _callbacks = {},
- var axis = this.dom.axis;
- if (axis.parentNode) {
- axis.parentNode.removeChild(axis);
- }
- };
+ /**
+ * direct map of string combinations to callbacks used for trigger()
+ *
+ * @type {Object}
+ */
+ _direct_map = {},
- /**
- * Add an item to the group
- * @param {Item} item
- */
- Group.prototype.add = function(item) {
- this.items[item.id] = item;
- item.setParent(this);
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ _sequence_levels = {},
- // add to
- if (item.data.subgroup !== undefined) {
- if (this.subgroups[item.data.subgroup] === undefined) {
- this.subgroups[item.data.subgroup] = {height:0};
- }
- }
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ _reset_timer,
- if (this.visibleItems.indexOf(item) == -1) {
- var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
- this._checkIfVisible(item, this.visibleItems, range);
- }
- };
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ _ignore_next_keyup = false,
- /**
- * Remove an item from the group
- * @param {Item} item
- */
- Group.prototype.remove = function(item) {
- delete this.items[item.id];
- item.setParent(this.itemSet);
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ _inside_sequence = false;
- // remove from visible items
- var index = this.visibleItems.indexOf(item);
- if (index != -1) this.visibleItems.splice(index, 1);
+ /**
+ * loop through the f keys, f1 to f19 and add them to the map
+ * programatically
+ */
+ for (var i = 1; i < 20; ++i) {
+ _MAP[111 + i] = 'f' + i;
+ }
- // TODO: also remove from ordered items?
- };
+ /**
+ * loop through to map numbers on the numeric keypad
+ */
+ for (i = 0; i <= 9; ++i) {
+ _MAP[i + 96] = i;
+ }
- /**
- * Remove an item from the corresponding DataSet
- * @param {Item} item
- */
- Group.prototype.removeFromDataSet = function(item) {
- this.itemSet.removeItem(item.id);
- };
+ /**
+ * cross browser add event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {Function} callback
+ * @returns void
+ */
+ function _addEvent(object, type, callback) {
+ if (object.addEventListener) {
+ return object.addEventListener(type, callback, false);
+ }
- /**
- * Reorder the items
- */
- Group.prototype.order = function() {
- var array = util.toArray(this.items);
- this.orderedItems.byStart = array;
- this.orderedItems.byEnd = this._constructByEndArray(array);
+ object.attachEvent('on' + type, callback);
+ }
- stack.orderByStart(this.orderedItems.byStart);
- stack.orderByEnd(this.orderedItems.byEnd);
- };
+ /**
+ * takes the event and returns the key character
+ *
+ * @param {Event} e
+ * @return {string}
+ */
+ function _characterFromEvent(e) {
- /**
- * Create an array containing all items being a range (having an end date)
- * @param {Item[]} array
- * @returns {RangeItem[]}
- * @private
- */
- Group.prototype._constructByEndArray = function(array) {
- var endArray = [];
+ // for keypress events we should return the character as is
+ if (e.type == 'keypress') {
+ return String.fromCharCode(e.which);
+ }
- for (var i = 0; i < array.length; i++) {
- if (array[i] instanceof RangeItem) {
- endArray.push(array[i]);
- }
- }
- return endArray;
- };
+ // for non keypress events the special maps are needed
+ if (_MAP[e.which]) {
+ return _MAP[e.which];
+ }
- /**
- * Update the visible items
- * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
- * @param {Item[]} visibleItems The previously visible items.
- * @param {{start: number, end: number}} range Visible range
- * @return {Item[]} visibleItems The new visible items.
- * @private
- */
- Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
- var initialPosByStart,
- newVisibleItems = [],
- i;
+ if (_KEYCODE_MAP[e.which]) {
+ return _KEYCODE_MAP[e.which];
+ }
- // first check if the items that were in view previously are still in view.
- // this handles the case for the RangeItem that is both before and after the current one.
- if (visibleItems.length > 0) {
- for (i = 0; i < visibleItems.length; i++) {
- this._checkIfVisible(visibleItems[i], newVisibleItems, range);
- }
+ // if it is not in the special map
+ return String.fromCharCode(e.which).toLowerCase();
}
- // If there were no visible items previously, use binarySearch to find a visible PointItem or RangeItem (based on startTime)
- if (newVisibleItems.length == 0) {
- initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
- }
- else {
- initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
- }
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @return {boolean}
+ */
+ function _stop(e) {
+ var element = e.target || e.srcElement,
+ tag_name = element.tagName;
- // use visible search to find a visible RangeItem (only based on endTime)
- var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
- // if we found a initial ID to use, trace it up and down until we meet an invisible item.
- if (initialPosByStart != -1) {
- for (i = initialPosByStart; i >= 0; i--) {
- if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
- }
- for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
- if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
- }
+ // stop for input, select, and textarea
+ return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
}
- // if we found a initial ID to use, trace it up and down until we meet an invisible item.
- if (initialPosByEnd != -1) {
- for (i = initialPosByEnd; i >= 0; i--) {
- if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
- }
- for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
- if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
- }
+ /**
+ * checks if two arrays are equal
+ *
+ * @param {Array} modifiers1
+ * @param {Array} modifiers2
+ * @returns {boolean}
+ */
+ function _modifiersMatch(modifiers1, modifiers2) {
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
- return newVisibleItems;
- };
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} do_not_reset
+ * @returns void
+ */
+ function _resetSequences(do_not_reset) {
+ do_not_reset = do_not_reset || {};
+ var active_sequences = false,
+ key;
+ for (key in _sequence_levels) {
+ if (do_not_reset[key]) {
+ active_sequences = true;
+ continue;
+ }
+ _sequence_levels[key] = 0;
+ }
- /**
- * this function checks if an item is invisible. If it is NOT we make it visible
- * and add it to the global visible items. If it is, return true.
- *
- * @param {Item} item
- * @param {Item[]} visibleItems
- * @param {{start:number, end:number}} range
- * @returns {boolean}
- * @private
- */
- Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
- if (item.isVisible(range)) {
- if (!item.displayed) item.show();
- item.repositionX();
- if (visibleItems.indexOf(item) == -1) {
- visibleItems.push(item);
+ if (!active_sequences) {
+ _inside_sequence = false;
}
- return false;
- }
- else {
- if (item.displayed) item.hide();
- return true;
- }
- };
+ }
- /**
- * this function is very similar to the _checkIfInvisible() but it does not
- * return booleans, hides the item if it should not be seen and always adds to
- * the visibleItems.
- * this one is for brute forcing and hiding.
- *
- * @param {Item} item
- * @param {Array} visibleItems
- * @param {{start:number, end:number}} range
- * @private
- */
- Group.prototype._checkIfVisible = function(item, visibleItems, range) {
- if (item.isVisible(range)) {
- if (!item.displayed) item.show();
- // reposition item horizontally
- item.repositionX();
- visibleItems.push(item);
- }
- else {
- if (item.displayed) item.hide();
- }
- };
-
- module.exports = Group;
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {string} action
+ * @param {boolean=} remove - should we remove any matches
+ * @param {string=} combination
+ * @returns {Array}
+ */
+ function _getMatches(character, modifiers, action, remove, combination) {
+ var i,
+ callback,
+ matches = [];
+ // if there are no events related to this keycode
+ if (!_callbacks[character]) {
+ return [];
+ }
-/***/ },
-/* 32 */
-/***/ function(module, exports, __webpack_require__) {
+ // if a modifier key is coming up on its own we should allow it
+ if (action == 'keyup' && _isModifier(character)) {
+ modifiers = [character];
+ }
- var Item = __webpack_require__(33);
- var util = __webpack_require__(1);
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < _callbacks[character].length; ++i) {
+ callback = _callbacks[character][i];
- /**
- * @constructor BoxItem
- * @extends Item
- * @param {Object} data Object containing parameters start
- * content, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} [options] Configuration options
- * // TODO: describe available options
- */
- function BoxItem (data, conversion, options) {
- this.props = {
- dot: {
- width: 0,
- height: 0
- },
- line: {
- width: 0,
- height: 0
- }
- };
+ // if this is a sequence but it is not at the right level
+ // then move onto the next match
+ if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
+ continue;
+ }
- // validate data
- if (data) {
- if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data);
- }
- }
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
- Item.call(this, data, conversion, options);
- }
+ // if this is a keypress event that means that we need to only
+ // look at the character, otherwise check the modifiers as
+ // well
+ if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
- BoxItem.prototype = new Item (null, null, null);
+ // remove is used so if you change your mind and call bind a
+ // second time with a new function the first one is overwritten
+ if (remove && callback.combo == combination) {
+ _callbacks[character].splice(i, 1);
+ }
- /**
- * 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
- */
- BoxItem.prototype.isVisible = function(range) {
- // determine visibility
- // TODO: account for the real width of the item. Right now we just add 1/4 to the window
- var interval = (range.end - range.start) / 4;
- return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
- };
+ matches.push(callback);
+ }
+ }
- /**
- * Repaint the item
- */
- BoxItem.prototype.redraw = function() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
+ return matches;
+ }
- // create main box
- dom.box = document.createElement('DIV');
+ /**
+ * takes a key event and figures out what the modifiers are
+ *
+ * @param {Event} e
+ * @returns {Array}
+ */
+ function _eventModifiers(e) {
+ var modifiers = [];
- // contents box (inside the background box). used for making margins
- dom.content = document.createElement('DIV');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+ if (e.shiftKey) {
+ modifiers.push('shift');
+ }
- // line to axis
- dom.line = document.createElement('DIV');
- dom.line.className = 'line';
+ if (e.altKey) {
+ modifiers.push('alt');
+ }
- // dot on axis
- dom.dot = document.createElement('DIV');
- dom.dot.className = 'dot';
+ if (e.ctrlKey) {
+ modifiers.push('ctrl');
+ }
- // attach this item as attribute
- dom.box['timeline-item'] = this;
+ if (e.metaKey) {
+ modifiers.push('meta');
+ }
- this.dirty = true;
+ return modifiers;
}
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot redraw item: no parent attached');
- }
- if (!dom.box.parentNode) {
- var foreground = this.parent.dom.foreground;
- if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element');
- foreground.appendChild(dom.box);
- }
- if (!dom.line.parentNode) {
- var background = this.parent.dom.background;
- if (!background) throw new Error('Cannot redraw item: parent has no background container element');
- background.appendChild(dom.line);
- }
- if (!dom.dot.parentNode) {
- var axis = this.parent.dom.axis;
- if (!background) throw new Error('Cannot redraw item: parent has no axis container element');
- axis.appendChild(dom.dot);
- }
- this.displayed = true;
+ /**
+ * actually calls the callback function
+ *
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
+ *
+ * @param {Function} callback
+ * @param {Event} e
+ * @returns void
+ */
+ function _fireCallback(callback, e) {
+ if (callback(e) === false) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
- // Update DOM when item is marked dirty. An item is marked dirty when:
- // - the item is not yet rendered
- // - the item's data is changed
- // - the item is selected/deselected
- if (this.dirty) {
- this._updateContents(this.dom.content);
- this._updateTitle(this.dom.box);
- this._updateDataAttributes(this.dom.box);
- this._updateStyle(this.dom.box);
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- dom.box.className = 'item box' + className;
- dom.line.className = 'item line' + className;
- dom.dot.className = 'item dot' + className;
+ e.returnValue = false;
+ e.cancelBubble = true;
+ }
+ }
- // recalculate size
- 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;
+ /**
+ * handles a character key event
+ *
+ * @param {string} character
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleCharacter(character, e) {
- this.dirty = false;
- }
+ // if this event should not happen stop here
+ if (_stop(e)) {
+ return;
+ }
- this._repaintDeleteButton(dom.box);
- };
+ var callbacks = _getMatches(character, _eventModifiers(e), e.type),
+ i,
+ do_not_reset = {},
+ processed_sequence_callback = false;
- /**
- * Show the item in the DOM (when not already displayed). The items DOM will
- * be created when needed.
- */
- BoxItem.prototype.show = function() {
- if (!this.displayed) {
- this.redraw();
- }
- };
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
- /**
- * Hide the item from the DOM (when visible)
- */
- BoxItem.prototype.hide = function() {
- if (this.displayed) {
- var dom = this.dom;
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+ processed_sequence_callback = true;
- if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
- if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
- if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
+ // keep a list of which sequences were matches for later
+ do_not_reset[callbacks[i].seq] = 1;
+ _fireCallback(callbacks[i].callback, e);
+ continue;
+ }
- this.top = null;
- this.left = null;
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processed_sequence_callback && !_inside_sequence) {
+ _fireCallback(callbacks[i].callback, e);
+ }
+ }
- this.displayed = false;
+ // if you are inside of a sequence and the key you are pressing
+ // is not a modifier key then we should reset all sequences
+ // that were not matched by this key event
+ if (e.type == _inside_sequence && !_isModifier(character)) {
+ _resetSequences(do_not_reset);
+ }
}
- };
-
- /**
- * Reposition the item horizontally
- * @Override
- */
- BoxItem.prototype.repositionX = function() {
- var start = this.conversion.toScreen(this.data.start);
- var align = this.options.align;
- var left;
- var box = this.dom.box;
- var line = this.dom.line;
- var 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;
- }
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleKey(e) {
- // reposition box
- box.style.left = this.left + 'px';
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ e.which = typeof e.which == "number" ? e.which : e.keyCode;
- // reposition line
- line.style.left = (start - this.props.line.width / 2) + 'px';
+ var character = _characterFromEvent(e);
- // reposition dot
- dot.style.left = (start - this.props.dot.width / 2) + 'px';
- };
+ // no character found then stop
+ if (!character) {
+ return;
+ }
- /**
- * Reposition the item vertically
- * @Override
- */
- BoxItem.prototype.repositionY = function() {
- var orientation = this.options.orientation;
- var box = this.dom.box;
- var line = this.dom.line;
- var dot = this.dom.dot;
+ if (e.type == 'keyup' && _ignore_next_keyup == character) {
+ _ignore_next_keyup = false;
+ return;
+ }
- if (orientation == 'top') {
- box.style.top = (this.top || 0) + 'px';
+ _handleCharacter(character, e);
+ }
- line.style.top = '0';
- line.style.height = (this.parent.top + this.top + 1) + 'px';
- line.style.bottom = '';
+ /**
+ * determines if the keycode specified is a modifier key or not
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function _isModifier(key) {
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
}
- else { // orientation 'bottom'
- var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
- var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
- box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
- line.style.top = (itemSetHeight - lineHeight) + 'px';
- line.style.bottom = '0';
+ /**
+ * called to set a 1 second timeout on the specified sequence
+ *
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
+ *
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_reset_timer);
+ _reset_timer = setTimeout(_resetSequences, 1000);
}
- dot.style.top = (-this.props.dot.height / 2) + 'px';
- };
+ /**
+ * reverses the map lookup so that we can look for specific keys
+ * to see what can and can't use keypress
+ *
+ * @return {Object}
+ */
+ function _getReverseMap() {
+ if (!_REVERSE_MAP) {
+ _REVERSE_MAP = {};
+ for (var key in _MAP) {
- module.exports = BoxItem;
+ // pull out the numeric keypad from here cause keypress should
+ // be able to detect the keys from the character
+ if (key > 95 && key < 112) {
+ continue;
+ }
+ if (_MAP.hasOwnProperty(key)) {
+ _REVERSE_MAP[_MAP[key]] = key;
+ }
+ }
+ }
+ return _REVERSE_MAP;
+ }
-/***/ },
-/* 33 */
-/***/ function(module, exports, __webpack_require__) {
+ /**
+ * picks the best action based on the key combination
+ *
+ * @param {string} key - character for key
+ * @param {Array} modifiers
+ * @param {string=} action passed in
+ */
+ function _pickBestAction(key, modifiers, action) {
- var Hammer = __webpack_require__(18);
- var util = __webpack_require__(1);
+ // if no action was picked in we should try to pick the one
+ // that we think would work best for this key
+ if (!action) {
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
+ }
- /**
- * @constructor Item
- * @param {Object} data Object containing (optional) parameters type,
- * start, end, content, group, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} options Configuration options
- * // TODO: describe available options
- */
- function Item (data, conversion, options) {
- this.id = null;
- this.parent = null;
- this.data = data;
- this.dom = null;
- this.conversion = conversion || {};
- this.options = options || {};
+ // modifier keys don't work as expected with keypress,
+ // switch to keydown
+ if (action == 'keypress' && modifiers.length) {
+ action = 'keydown';
+ }
- this.selected = false;
- this.displayed = false;
- this.dirty = true;
+ return action;
+ }
- this.top = null;
- this.left = null;
- this.width = null;
- this.height = null;
+ /**
+ * binds a key sequence to an event
+ *
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
+ * @returns void
+ */
+ function _bindSequence(combo, keys, callback, action) {
- this.ignoreStacking = false;
- }
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequence_levels[combo] = 0;
- /**
- * Select current item
- */
- Item.prototype.select = function() {
- this.selected = true;
- this.dirty = true;
- if (this.displayed) this.redraw();
- };
+ // if there is no action pick the best one for the first key
+ // in the sequence
+ if (!action) {
+ action = _pickBestAction(keys[0], []);
+ }
- /**
- * Unselect current item
- */
- Item.prototype.unselect = function() {
- this.selected = false;
- this.dirty = true;
- if (this.displayed) this.redraw();
- };
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ var _increaseSequence = function(e) {
+ _inside_sequence = action;
+ ++_sequence_levels[combo];
+ _resetSequenceTimer();
+ },
- /**
- * Set data for the item. Existing data will be updated. The id should not
- * be changed. When the item is displayed, it will be redrawn immediately.
- * @param {Object} data
- */
- Item.prototype.setData = function(data) {
- this.data = data;
- this.dirty = true;
- if (this.displayed) this.redraw();
- };
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ _callbackAndReset = function(e) {
+ _fireCallback(callback, e);
- /**
- * Set a parent for the item
- * @param {ItemSet | Group} parent
- */
- Item.prototype.setParent = function(parent) {
- if (this.displayed) {
- this.hide();
- this.parent = parent;
- if (this.parent) {
- this.show();
- }
- }
- else {
- this.parent = parent;
- }
- };
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== 'keyup') {
+ _ignore_next_keyup = _characterFromEvent(e);
+ }
- /**
- * Check whether this item is visible inside given range
- * @returns {{start: Number, end: Number}} range with a timestamp for start and end
- * @returns {boolean} True if visible
- */
- Item.prototype.isVisible = function(range) {
- // Should be implemented by Item implementations
- return false;
- };
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ },
+ i;
- /**
- * Show the Item in the DOM (when not already visible)
- * @return {Boolean} changed
- */
- Item.prototype.show = function() {
- return false;
- };
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ for (i = 0; i < keys.length; ++i) {
+ _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
+ }
+ }
- /**
- * Hide the Item from the DOM (when visible)
- * @return {Boolean} changed
- */
- Item.prototype.hide = function() {
- return false;
- };
+ /**
+ * binds a single keyboard combination
+ *
+ * @param {string} combination
+ * @param {Function} callback
+ * @param {string=} action
+ * @param {string=} sequence_name - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
+ * @returns void
+ */
+ function _bindSingle(combination, callback, action, sequence_name, level) {
- /**
- * Repaint the item
- */
- Item.prototype.redraw = function() {
- // should be implemented by the item
- };
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, ' ');
- /**
- * Reposition the Item horizontally
- */
- Item.prototype.repositionX = function() {
- // should be implemented by the item
- };
+ var sequence = combination.split(' '),
+ i,
+ key,
+ keys,
+ modifiers = [];
- /**
- * Reposition the Item vertically
- */
- Item.prototype.repositionY = function() {
- // should be implemented by the item
- };
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ return _bindSequence(combination, sequence, callback, action);
+ }
- /**
- * Repaint a delete button on the top right of the item when the item is selected
- * @param {HTMLElement} anchor
- * @protected
- */
- Item.prototype._repaintDeleteButton = function (anchor) {
- if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
- // create and show button
- var me = this;
+ // take the keys from this pattern and figure out what the actual
+ // pattern is all about
+ keys = combination === '+' ? ['+'] : combination.split('+');
- var deleteButton = document.createElement('div');
- deleteButton.className = 'delete';
- deleteButton.title = 'Delete this item';
+ for (i = 0; i < keys.length; ++i) {
+ key = keys[i];
- Hammer(deleteButton, {
- preventDefault: true
- }).on('tap', function (event) {
- me.parent.removeFromDataSet(me);
- event.stopPropagation();
- });
+ // normalize key names
+ if (_SPECIAL_ALIASES[key]) {
+ key = _SPECIAL_ALIASES[key];
+ }
- 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;
- }
- };
+ // if this is not a keypress event then we should
+ // be smart about using shift keys
+ // this will only work for US keyboards however
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
+ key = _SHIFT_MAP[key];
+ modifiers.push('shift');
+ }
- /**
- * Set HTML contents for the item
- * @param {Element} element HTML element to fill with the contents
- * @private
- */
- Item.prototype._updateContents = function (element) {
- var content;
- if (this.options.template) {
- var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset
- content = this.options.template(itemData);
- }
- else {
- content = this.data.content;
- }
+ // if this key is a modifier then add it to the list of modifiers
+ if (_isModifier(key)) {
+ modifiers.push(key);
+ }
+ }
- if(content !== this.content) {
- // only replace the content when changed
- if (content instanceof Element) {
- element.innerHTML = '';
- element.appendChild(content);
- }
- else if (content != undefined) {
- element.innerHTML = content;
- }
- else {
- if (!(this.data.type == 'background' && this.data.content === undefined)) {
- throw new Error('Property "content" missing in item ' + this.id);
+ // depending on what the key combination is
+ // we will try to pick the best event for it
+ action = _pickBestAction(key, modifiers, action);
+
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ if (!_callbacks[key]) {
+ _callbacks[key] = [];
}
- }
- this.content = content;
- }
- };
+ // remove an existing match if there is one
+ _getMatches(key, modifiers, action, !sequence_name, combination);
- /**
- * Set HTML contents for the item
- * @param {Element} element HTML element to fill with the contents
- * @private
- */
- Item.prototype._updateTitle = function (element) {
- if (this.data.title != null) {
- element.title = this.data.title || '';
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ _callbacks[key][sequence_name ? 'unshift' : 'push']({
+ callback: callback,
+ modifiers: modifiers,
+ action: action,
+ seq: sequence_name,
+ level: level,
+ combo: combination
+ });
}
- else {
- element.removeAttribute('title');
+
+ /**
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
+ */
+ function _bindMultiple(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
}
- };
- /**
- * Process dataAttributes timeline option and set as data- attributes on dom.content
- * @param {Element} element HTML element to which the attributes will be attached
- * @private
- */
- Item.prototype._updateDataAttributes = function(element) {
- if (this.options.dataAttributes && this.options.dataAttributes.length > 0) {
- var attributes = [];
+ // start!
+ _addEvent(document, 'keypress', _handleKey);
+ _addEvent(document, 'keydown', _handleKey);
+ _addEvent(document, 'keyup', _handleKey);
- if (Array.isArray(this.options.dataAttributes)) {
- attributes = this.options.dataAttributes;
- }
- else if (this.options.dataAttributes == 'all') {
- attributes = Object.keys(this.data);
- }
- else {
- return;
- }
+ var mousetrap = {
- for (var i = 0; i < attributes.length; i++) {
- var name = attributes[i];
- var value = this.data[name];
+ /**
+ * binds an event to mousetrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * a comma separated list of keys, an array of keys, or
+ * a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ bind: function(keys, callback, action) {
+ _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
+ _direct_map[keys + ':' + action] = callback;
+ return this;
+ },
- if (value != null) {
- element.setAttribute('data-' + name, value);
- }
- else {
- element.removeAttribute('data-' + name);
- }
- }
- }
- };
+ /**
+ * unbinds an event to mousetrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _direct_map dict.
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ unbind: function(keys, action) {
+ if (_direct_map[keys + ':' + action]) {
+ delete _direct_map[keys + ':' + action];
+ this.bind(keys, function() {}, action);
+ }
+ return this;
+ },
- /**
- * Update custom styles of the element
- * @param element
- * @private
- */
- Item.prototype._updateStyle = function(element) {
- // remove old styles
- if (this.style) {
- util.removeCssText(element, this.style);
- this.style = null;
- }
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ trigger: function(keys, action) {
+ _direct_map[keys + ':' + action]();
+ return this;
+ },
- // append new styles
- if (this.data.style) {
- util.addCssText(element, this.data.style);
- this.style = this.data.style;
- }
- };
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ reset: function() {
+ _callbacks = {};
+ _direct_map = {};
+ return this;
+ }
+ };
+
+ module.exports = mousetrap;
- module.exports = Item;
/***/ },
-/* 34 */
+/* 33 */
/***/ function(module, exports, __webpack_require__) {
- var Item = __webpack_require__(33);
+ var Emitter = __webpack_require__(10);
+ var Hammer = __webpack_require__(18);
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(8);
+ var Range = __webpack_require__(20);
+ var Core = __webpack_require__(24);
+ var TimeAxis = __webpack_require__(25);
+ var CurrentTime = __webpack_require__(27);
+ var CustomTime = __webpack_require__(29);
+ var LineGraph = __webpack_require__(34);
/**
- * @constructor PointItem
- * @extends Item
- * @param {Object} data Object containing parameters start
- * content, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} [options] Configuration options
- * // TODO: describe available options
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {Object} [options] See Graph2d.setOptions for the available options.
+ * @constructor
+ * @extends Core
*/
- function PointItem (data, conversion, options) {
- this.props = {
- dot: {
- top: 0,
- width: 0,
- height: 0
- },
- content: {
- height: 0,
- marginLeft: 0
- }
+ function Graph2d (container, items, groups, options) {
+ // if the third element is options, the forth is groups (optionally);
+ if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) {
+ var forthArgument = options;
+ options = groups;
+ groups = forthArgument;
+ }
+
+ var me = this;
+ this.defaultOptions = {
+ start: null,
+ end: null,
+
+ autoResize: true,
+
+ orientation: 'bottom',
+ width: null,
+ height: null,
+ maxHeight: null,
+ minHeight: null
};
+ this.options = util.deepExtend({}, this.defaultOptions);
- // validate data
- if (data) {
- if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data);
- }
- }
+ // Create the DOM, props, and emitter
+ this._create(container);
- Item.call(this, data, conversion, options);
- }
+ // all components listed here will be repainted automatically
+ this.components = [];
- PointItem.prototype = new Item (null, null, null);
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
+ },
+ util: {
+ snap: null, // will be specified after TimeAxis is created
+ toScreen: me._toScreen.bind(me),
+ toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+ toTime: me._toTime.bind(me),
+ toGlobalTime : me._toGlobalTime.bind(me)
+ }
+ };
- /**
- * 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
- */
- PointItem.prototype.isVisible = function(range) {
- // determine visibility
- // TODO: account for the real width of the item. Right now we just add 1/4 to the window
- var interval = (range.end - range.start) / 4;
- return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
- };
+ // range
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
- /**
- * Repaint the item
- */
- PointItem.prototype.redraw = function() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.components.push(this.timeAxis);
+ this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
- // background box
- dom.point = document.createElement('div');
- // className is updated in redraw()
+ // current time bar
+ this.currentTime = new CurrentTime(this.body);
+ this.components.push(this.currentTime);
- // contents box, right from the dot
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.point.appendChild(dom.content);
+ // custom time bar
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(this.body);
+ this.components.push(this.customTime);
- // dot at start
- dom.dot = document.createElement('div');
- dom.point.appendChild(dom.dot);
+ // item set
+ this.linegraph = new LineGraph(this.body);
+ this.components.push(this.linegraph);
- // attach this item as attribute
- dom.point['timeline-item'] = this;
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
- this.dirty = true;
+ // apply options
+ if (options) {
+ this.setOptions(options);
}
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot redraw item: no parent attached');
+ // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
+ if (groups) {
+ this.setGroups(groups);
}
- if (!dom.point.parentNode) {
- var foreground = this.parent.dom.foreground;
- if (!foreground) {
- throw new Error('Cannot redraw item: parent has no foreground container element');
- }
- foreground.appendChild(dom.point);
- }
- this.displayed = true;
-
- // Update DOM when item is marked dirty. An item is marked dirty when:
- // - the item is not yet rendered
- // - the item's data is changed
- // - the item is selected/deselected
- if (this.dirty) {
- this._updateContents(this.dom.content);
- this._updateTitle(this.dom.point);
- this._updateDataAttributes(this.dom.point);
- this._updateStyle(this.dom.point);
-
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- dom.point.className = 'item point' + className;
- dom.dot.className = 'item dot' + className;
-
- // recalculate size
- this.width = dom.point.offsetWidth;
- this.height = dom.point.offsetHeight;
- this.props.dot.width = dom.dot.offsetWidth;
- this.props.dot.height = dom.dot.offsetHeight;
- this.props.content.height = dom.content.offsetHeight;
-
- // resize contents
- dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
- //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
-
- dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
- dom.dot.style.left = (this.props.dot.width / 2) + 'px';
- this.dirty = false;
+ // create itemset
+ if (items) {
+ this.setItems(items);
+ }
+ else {
+ this.redraw();
}
+ }
- this._repaintDeleteButton(dom.point);
- };
+ // Extend the functionality from Core
+ Graph2d.prototype = new Core();
/**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
+ * Set items
+ * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
*/
- PointItem.prototype.show = function() {
- if (!this.displayed) {
- this.redraw();
+ Graph2d.prototype.setItems = function(items) {
+ var initialLoad = (this.itemsData == null);
+
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!items) {
+ newDataSet = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ newDataSet = items;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(items, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
}
- };
- /**
- * Hide the item from the DOM (when visible)
- */
- PointItem.prototype.hide = function() {
- if (this.displayed) {
- if (this.dom.point.parentNode) {
- this.dom.point.parentNode.removeChild(this.dom.point);
- }
+ // set items
+ this.itemsData = newDataSet;
+ this.linegraph && this.linegraph.setItems(newDataSet);
- this.top = null;
- this.left = null;
+ if (initialLoad) {
+ if (this.options.start != undefined || this.options.end != undefined) {
+ var start = this.options.start != undefined ? this.options.start : null;
+ var end = this.options.end != undefined ? this.options.end : null;
- this.displayed = false;
+ this.setWindow(start, end, {animate: false});
+ }
+ else {
+ this.fit({animate: false});
+ }
}
};
/**
- * Reposition the item horizontally
- * @Override
+ * Set groups
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/
- PointItem.prototype.repositionX = function() {
- var start = this.conversion.toScreen(this.data.start);
-
- this.left = start - this.props.dot.width;
+ Graph2d.prototype.setGroups = function(groups) {
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!groups) {
+ newDataSet = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ newDataSet = groups;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(groups);
+ }
- // reposition point
- this.dom.point.style.left = this.left + 'px';
+ this.groupsData = newDataSet;
+ this.linegraph.setGroups(newDataSet);
};
/**
- * Reposition the item vertically
- * @Override
+ * Returns an object containing an SVG element with the icon of the group (size determined by iconWidth and iconHeight), the label of the group (content) and the yAxisOrientation of the group (left or right).
+ * @param groupId
+ * @param width
+ * @param height
*/
- PointItem.prototype.repositionY = function() {
- var orientation = this.options.orientation,
- point = this.dom.point;
-
- if (orientation == 'top') {
- point.style.top = this.top + 'px';
+ Graph2d.prototype.getLegend = function(groupId, width, height) {
+ if (width === undefined) {width = 15;}
+ if (height === undefined) {height = 15;}
+ if (this.linegraph.groups[groupId] !== undefined) {
+ return this.linegraph.groups[groupId].getLegend(width,height);
}
else {
- point.style.top = (this.parent.height - this.top - this.height) + 'px';
+ return "cannot find group:" + groupId;
}
- };
-
- module.exports = PointItem;
-
+ }
-/***/ },
-/* 35 */
-/***/ function(module, exports, __webpack_require__) {
+ /**
+ * This checks if the visible option of the supplied group (by ID) is true or false.
+ * @param groupId
+ * @returns {*}
+ */
+ Graph2d.prototype.isGroupVisible = function(groupId) {
+ if (this.linegraph.groups[groupId] !== undefined) {
+ return (this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true));
+ }
+ else {
+ return false;
+ }
+ }
- var Hammer = __webpack_require__(18);
- var Item = __webpack_require__(33);
/**
- * @constructor RangeItem
- * @extends Item
- * @param {Object} data Object containing parameters start, end
- * content, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} [options] Configuration options
- * // TODO: describe options
+ * Get the data range of the item set.
+ * @returns {{min: Date, max: Date}} range A range with a start and end Date.
+ * When no minimum is found, min==null
+ * When no maximum is found, max==null
*/
- function RangeItem (data, conversion, options) {
- this.props = {
- content: {
- width: 0
- }
- };
- this.overflow = false; // if contents can overflow (css styling), this flag is set to true
+ Graph2d.prototype.getItemRange = function() {
+ var min = null;
+ var max = null;
- // 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);
+ // calculate min from start filed
+ for (var groupId in this.linegraph.groups) {
+ if (this.linegraph.groups.hasOwnProperty(groupId)) {
+ if (this.linegraph.groups[groupId].visible == true) {
+ for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) {
+ var item = this.linegraph.groups[groupId].itemsData[i];
+ var value = util.convert(item.x, 'Date').valueOf();
+ min = min == null ? value : min > value ? value : min;
+ max = max == null ? value : max < value ? value : max;
+ }
+ }
}
}
- Item.call(this, data, conversion, options);
- }
-
- RangeItem.prototype = new Item (null, null, null);
+ return {
+ min: (min != null) ? new Date(min) : null,
+ max: (max != null) ? new Date(max) : null
+ };
+ };
- RangeItem.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
- */
- RangeItem.prototype.isVisible = function(range) {
- // determine visibility
- return (this.data.start < range.end) && (this.data.end > range.start);
- };
- /**
- * Repaint the item
- */
- RangeItem.prototype.redraw = function() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
+ module.exports = Graph2d;
- // background box
- dom.box = document.createElement('div');
- // className is updated in redraw()
- // contents box
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+/***/ },
+/* 34 */
+/***/ function(module, exports, __webpack_require__) {
- // attach this item as attribute
- dom.box['timeline-item'] = this;
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+ var DataSet = __webpack_require__(7);
+ var DataView = __webpack_require__(8);
+ var Component = __webpack_require__(22);
+ var DataAxis = __webpack_require__(35);
+ var GraphGroup = __webpack_require__(37);
+ var Legend = __webpack_require__(38);
- this.dirty = true;
- }
+ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot redraw item: no parent attached');
- }
- if (!dom.box.parentNode) {
- var foreground = this.parent.dom.foreground;
- if (!foreground) {
- throw new Error('Cannot redraw item: parent has no foreground container element');
+ /**
+ * This is the constructor of the LineGraph. It requires a Timeline body and options.
+ *
+ * @param body
+ * @param options
+ * @constructor
+ */
+ function LineGraph(body, options) {
+ this.id = util.randomUUID();
+ this.body = body;
+
+ this.defaultOptions = {
+ yAxisOrientation: 'left',
+ defaultGroup: 'default',
+ sort: true,
+ sampling: true,
+ graphHeight: '400px',
+ shaded: {
+ enabled: false,
+ orientation: 'bottom' // top, bottom
+ },
+ style: 'line', // line, bar
+ barChart: {
+ width: 50,
+ handleOverlap: 'overlap',
+ align: 'center' // left, center, right
+ },
+ catmullRom: {
+ enabled: true,
+ parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
+ alpha: 0.5
+ },
+ drawPoints: {
+ enabled: true,
+ size: 6,
+ style: 'square' // square, circle
+ },
+ dataAxis: {
+ showMinorLabels: true,
+ showMajorLabels: true,
+ icons: false,
+ width: '40px',
+ visible: true,
+ customRange: {
+ left: {min:undefined, max:undefined},
+ right: {min:undefined, max:undefined}
+ }
+ },
+ legend: {
+ enabled: false,
+ icons: true,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,right
+ },
+ right: {
+ visible: true,
+ position: 'top-right' // top/bottom - left,right
+ }
+ },
+ groups: {
+ visibility: {}
}
- foreground.appendChild(dom.box);
- }
- this.displayed = true;
+ };
- // Update DOM when item is marked dirty. An item is marked dirty when:
- // - the item is not yet rendered
- // - the item's data is changed
- // - the item is selected/deselected
- if (this.dirty) {
- this._updateContents(this.dom.content);
- this._updateTitle(this.dom.box);
- this._updateDataAttributes(this.dom.box);
- this._updateStyle(this.dom.box);
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
+ this.dom = {};
+ this.props = {};
+ this.hammer = null;
+ this.groups = {};
+ this.abortedGraphUpdate = false;
- // update class
- var className = (this.data.className ? (' ' + this.data.className) : '') +
- (this.selected ? ' selected' : '');
- dom.box.className = this.baseClassName + className;
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
- // determine from css whether this box has overflow
- this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
+ // listeners for the DataSet of the items
+ this.itemListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemove(params.items);
+ }
+ };
- // recalculate size
- this.props.content.width = this.dom.content.offsetWidth;
- this.height = this.dom.box.offsetHeight;
+ // listeners for the DataSet of the groups
+ this.groupListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAddGroups(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdateGroups(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemoveGroups(params.items);
+ }
+ };
- this.dirty = false;
- }
+ this.items = {}; // object with an Item for every data item
+ this.selection = []; // list with the ids of all selected nodes
+ this.lastStart = this.body.range.start;
+ this.touchParams = {}; // stores properties while dragging
- this._repaintDeleteButton(dom.box);
- this._repaintDragLeft();
- this._repaintDragRight();
- };
+ this.svgElements = {};
+ this.setOptions(options);
+ this.groupsUsingDefaultStyles = [0];
+
+ this.body.emitter.on("rangechanged", function() {
+ me.lastStart = me.body.range.start;
+ me.svg.style.left = util.option.asSize(-me.width);
+ me._updateGraph.apply(me);
+ });
+
+ // create the HTML DOM
+ this._create();
+ this.body.emitter.emit("change");
+ }
+
+ LineGraph.prototype = new Component();
/**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
+ * Create the HTML DOM for the ItemSet
*/
- RangeItem.prototype.show = function() {
- if (!this.displayed) {
- this.redraw();
- }
+ LineGraph.prototype._create = function(){
+ var frame = document.createElement('div');
+ frame.className = 'LineGraph';
+ this.dom.frame = frame;
+
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "relative";
+ this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
+ this.svg.style.display = "block";
+ frame.appendChild(this.svg);
+
+ // data axis
+ this.options.dataAxis.orientation = 'left';
+ this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
+
+ this.options.dataAxis.orientation = 'right';
+ this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
+ delete this.options.dataAxis.orientation;
+
+ // legends
+ this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
+ this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
+
+ this.show();
};
/**
- * Hide the item from the DOM (when visible)
- * @return {Boolean} changed
+ * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
+ * @param options
*/
- RangeItem.prototype.hide = function() {
- if (this.displayed) {
- var box = this.dom.box;
+ LineGraph.prototype.setOptions = function(options) {
+ if (options) {
+ var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
+ util.mergeOptions(this.options, options,'legend');
- if (box.parentNode) {
- box.parentNode.removeChild(box);
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
+ }
+ }
+ }
}
- this.top = null;
- this.left = null;
+ if (this.yAxisLeft) {
+ if (options.dataAxis !== undefined) {
+ this.yAxisLeft.setOptions(this.options.dataAxis);
+ this.yAxisRight.setOptions(this.options.dataAxis);
+ }
+ }
- this.displayed = false;
+ if (this.legendLeft) {
+ if (options.legend !== undefined) {
+ this.legendLeft.setOptions(this.options.legend);
+ this.legendRight.setOptions(this.options.legend);
+ }
+ }
+
+ if (this.groups.hasOwnProperty(UNGROUPED)) {
+ this.groups[UNGROUPED].setOptions(options);
+ }
+ }
+ if (this.dom.frame) {
+ this._updateGraph();
}
};
/**
- * Reposition the item horizontally
- * @Override
+ * Hide the component from the DOM
*/
- RangeItem.prototype.repositionX = function() {
- var parentWidth = this.parent.width;
- var start = this.conversion.toScreen(this.data.start);
- var end = this.conversion.toScreen(this.data.end);
- var contentLeft;
- var contentWidth;
-
- // 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;
+ LineGraph.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
- var boxWidth = Math.max(end - start, 1);
+ };
- if (this.overflow) {
- this.left = start;
- this.width = boxWidth + this.props.content.width;
- contentWidth = this.props.content.width;
+ /**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+ LineGraph.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
+ }
+ };
- // Note: The calculation of width is an optimistic calculation, giving
- // a width which will not change when moving the Timeline
- // So no re-stacking needed, which is nicer for the eye;
+
+ /**
+ * Set items
+ * @param {vis.DataSet | null} items
+ */
+ LineGraph.prototype.setItems = function(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
+
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
}
else {
- this.left = start;
- this.width = boxWidth;
- contentWidth = Math.min(end - start, this.props.content.width);
+ throw new TypeError('Data must be an instance of DataSet or DataView');
}
- this.dom.box.style.left = this.left + 'px';
- this.dom.box.style.width = boxWidth + 'px';
-
- switch (this.options.align) {
- case 'left':
- this.dom.content.style.left = '0';
- break;
+ if (oldItemsData) {
+ // unsubscribe from old dataset
+ util.forEach(this.itemListeners, function (callback, event) {
+ oldItemsData.off(event, callback);
+ });
- case 'right':
- this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px';
- break;
+ // remove all drawn items
+ ids = oldItemsData.getIds();
+ this._onRemove(ids);
+ }
- case 'center':
- this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px';
- break;
+ if (this.itemsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.itemListeners, function (callback, event) {
+ me.itemsData.on(event, callback, id);
+ });
- default: // 'auto'
- if (this.overflow) {
- // when range exceeds left of the window, position the contents at the left of the visible area
- contentLeft = Math.max(-start, 0);
- }
- else {
- // 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 - this.props.content.width - 2 * this.options.padding));
- // TODO: remove the need for options.padding. it's terrible.
- }
- else {
- contentLeft = 0;
- }
- }
- this.dom.content.style.left = contentLeft + 'px';
+ // add all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
}
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
/**
- * Reposition the item vertically
- * @Override
+ * Set groups
+ * @param {vis.DataSet} groups
*/
- RangeItem.prototype.repositionY = function() {
- var orientation = this.options.orientation,
- box = this.dom.box;
+ LineGraph.prototype.setGroups = function(groups) {
+ var me = this,
+ ids;
- if (orientation == 'top') {
- box.style.top = this.top + 'px';
+ // unsubscribe from current dataset
+ if (this.groupsData) {
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.unsubscribe(event, callback);
+ });
+
+ // remove all drawn groups
+ ids = this.groupsData.getIds();
+ this.groupsData = null;
+ this._onRemoveGroups(ids); // note: this will cause a redraw
+ }
+
+ // replace the dataset
+ if (!groups) {
+ this.groupsData = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ this.groupsData = groups;
}
else {
- box.style.top = (this.parent.height - this.top - this.height) + 'px';
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
+
+ if (this.groupsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.on(event, callback, id);
+ });
+
+ // draw all ms
+ ids = this.groupsData.getIds();
+ this._onAddGroups(ids);
}
+ this._onUpdate();
};
+
/**
- * Repaint a drag area on the left side of the range when the range is selected
- * @protected
+ * Update the datapoints
+ * @param [ids]
+ * @private
*/
- RangeItem.prototype._repaintDragLeft = function () {
- if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
- // create and show drag area
- var dragLeft = document.createElement('div');
- dragLeft.className = 'drag-left';
- dragLeft.dragLeftItem = this;
+ LineGraph.prototype._onUpdate = function(ids) {
+ this._updateUngrouped();
+ this._updateAllGroupData();
+ this._updateGraph();
+ this.redraw();
+ };
+ LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
+ LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
+ LineGraph.prototype._onUpdateGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ var group = this.groupsData.get(groupIds[i]);
+ this._updateGroup(group, groupIds[i]);
+ }
- // TODO: this should be redundant?
- Hammer(dragLeft, {
- preventDefault: true
- }).on('drag', function () {
- //console.log('drag left')
- });
+ this._updateGraph();
+ this.redraw();
+ };
+ LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
- 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);
+ LineGraph.prototype._onRemoveGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ if (!this.groups.hasOwnProperty(groupIds[i])) {
+ if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
+ this.yAxisRight.removeGroup(groupIds[i]);
+ this.legendRight.removeGroup(groupIds[i]);
+ this.legendRight.redraw();
+ }
+ else {
+ this.yAxisLeft.removeGroup(groupIds[i]);
+ this.legendLeft.removeGroup(groupIds[i]);
+ this.legendLeft.redraw();
+ }
+ delete this.groups[groupIds[i]];
}
- this.dom.dragLeft = null;
}
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
/**
- * Repaint a drag area on the right side of the range when the range is selected
- * @protected
+ * update a group object
+ *
+ * @param group
+ * @param groupId
+ * @private
*/
- RangeItem.prototype._repaintDragRight = function () {
- if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
- // create and show drag area
- var dragRight = document.createElement('div');
- dragRight.className = 'drag-right';
- dragRight.dragRightItem = this;
-
- // TODO: this should be redundant?
- Hammer(dragRight, {
- preventDefault: true
- }).on('drag', function () {
- //console.log('drag right')
- });
-
- this.dom.box.appendChild(dragRight);
- this.dom.dragRight = dragRight;
+ LineGraph.prototype._updateGroup = function (group, groupId) {
+ if (!this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.addGroup(groupId, this.groups[groupId]);
+ this.legendRight.addGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
+ this.legendLeft.addGroup(groupId, this.groups[groupId]);
+ }
}
- else if (!this.selected && this.dom.dragRight) {
- // delete drag area
- if (this.dom.dragRight.parentNode) {
- this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
+ else {
+ this.groups[groupId].update(group);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
+ this.legendRight.updateGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
+ this.legendLeft.updateGroup(groupId, this.groups[groupId]);
}
- this.dom.dragRight = null;
}
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
};
- module.exports = RangeItem;
-
-
-/***/ },
-/* 36 */
-/***/ function(module, exports, __webpack_require__) {
-
- var Hammer = __webpack_require__(18);
- var Item = __webpack_require__(33);
- var RangeItem = __webpack_require__(35);
-
- /**
- * @constructor BackgroundItem
- * @extends Item
- * @param {Object} data Object containing parameters start, end
- * content, className.
- * @param {{toScreen: function, toTime: function}} conversion
- * Conversion functions from time to screen and vice versa
- * @param {Object} [options] Configuration options
- * // TODO: describe options
- */
- // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation
- function BackgroundItem (data, conversion, options) {
- this.props = {
- content: {
- width: 0
+ LineGraph.prototype._updateAllGroupData = function () {
+ if (this.itemsData != null) {
+ var groupsContent = {};
+ var groupId;
+ for (groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ groupsContent[groupId] = [];
+ }
}
- };
- this.overflow = false; // if contents can overflow (css styling), this flag is set to true
-
- // validate data
- if (data) {
- if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data.id);
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ item.x = util.convert(item.x,"Date");
+ groupsContent[item.group].push(item);
+ }
}
- if (data.end == undefined) {
- throw new Error('Property "end" missing in item ' + data.id);
+ for (groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].setItems(groupsContent[groupId]);
+ }
}
}
-
- Item.call(this, data, conversion, options);
-
- this.ignoreStacking = true; // this is not used when stacking
- this.emptyContent = false;
- }
-
- BackgroundItem.prototype = new Item (null, null, null);
-
- BackgroundItem.prototype.baseClassName = 'item background';
-
- /**
- * 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
- */
- BackgroundItem.prototype.isVisible = function(range) {
- // determine visibility
- return (this.data.start < range.end) && (this.data.end > range.start);
};
/**
- * Repaint the item
+ * Create or delete the group holding all ungrouped items. This group is used when
+ * there are no groups specified. This anonymous group is called 'graph'.
+ * @protected
*/
- BackgroundItem.prototype.redraw = function() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
-
- // background box
- dom.box = document.createElement('div');
- // className is updated in redraw()
-
- // contents box
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
-
- // attach this item as attribute
- dom.box['timeline-item'] = this;
-
- this.dirty = true;
- }
-
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot redraw item: no parent attached');
- }
- if (!dom.box.parentNode) {
- var background = this.parent.dom.background;
- if (!background) {
- throw new Error('Cannot redraw item: parent has no background container element');
+ LineGraph.prototype._updateUngrouped = function() {
+ if (this.itemsData && this.itemsData != null) {
+ var ungroupedCounter = 0;
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ if (item != undefined) {
+ if (item.hasOwnProperty('group')) {
+ if (item.group === undefined) {
+ item.group = UNGROUPED;
+ }
+ }
+ else {
+ item.group = UNGROUPED;
+ }
+ ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
+ }
+ }
}
- background.appendChild(dom.box);
- }
- this.displayed = true;
-
- // Update DOM when item is marked dirty. An item is marked dirty when:
- // - the item is not yet rendered
- // - the item's data is changed
- // - the item is selected/deselected
- if (this.dirty) {
- this._updateContents(this.dom.content);
- this._updateTitle(this.dom.content);
- this._updateDataAttributes(this.dom.content);
- this._updateStyle(this.dom.box);
-
- // update class
- var className = (this.data.className ? (' ' + this.data.className) : '') +
- (this.selected ? ' selected' : '');
- dom.box.className = this.baseClassName + className;
-
- // determine from css whether this box has overflow
- this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
-
- // recalculate size
- this.props.content.width = this.dom.content.offsetWidth;
- this.height = 0; // set height zero, so this item will be ignored when stacking items
-
- this.dirty = false;
- }
- };
-
- /**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
- */
- BackgroundItem.prototype.show = RangeItem.prototype.show;
-
- /**
- * Hide the item from the DOM (when visible)
- * @return {Boolean} changed
- */
- BackgroundItem.prototype.hide = RangeItem.prototype.hide;
-
- /**
- * Reposition the item horizontally
- * @Override
- */
- BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX;
-
- /**
- * Reposition the item vertically
- * @Override
- */
- BackgroundItem.prototype.repositionY = function(margin) {
- var onTop = this.options.orientation === 'top';
- this.dom.content.style.top = onTop ? '' : '0';
- this.dom.content.style.bottom = onTop ? '0' : '';
- var height;
-
- // special positioning for subgroups
- if (this.data.subgroup !== undefined) {
- var subgroup = this.data.subgroup;
- height = this.parent.subgroups[subgroup].height + margin.item.vertical;
-
-
- // if the orientation is top, we need to take the difference in height into account.
- if (onTop == true) {
- // the first subgroup will have to account for the distance from the top to the first item.
- height += subgroup == 0 ? margin.axis - 0.5*margin.item.vertical : 0;
- var newTop = this.parent.top + (subgroup) * height;
- // the others will have to be offset downwards with this same distance.
- newTop += subgroup != 0 ? margin.axis - 0.5 * margin.item.vertical : 0;
- this.dom.box.style.top = newTop + 'px';
- this.dom.box.style.bottom = '';
+ if (ungroupedCounter == 0) {
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
}
- // and when the orientation is bottom:
else {
- var amountvisibleSubgroups = this.parent.visibleSubgroups - 1;
- this.dom.box.style.top = (amountvisibleSubgroups - subgroup) * height + this.parent.top + 'px';
- this.dom.box.style.bottom = '';
+ var group = {id: UNGROUPED, content: this.options.defaultGroup};
+ this._updateGroup(group, UNGROUPED);
}
}
- // and in the case of no subgroups:
else {
- // we want backgrounds with groups to only show in groups.
- if (this.data.group !== undefined) {
- height = this.parent.height;
- // alignment for items when orientation is top
- if (onTop == true) {
- this.dom.box.style.top = this.parent.top + 'px';
- this.dom.box.style.bottom = '';
- }
- // and when the orientation is bottom
- else {
- this.dom.box.style.top = this.parent.top + 'px';
- this.dom.box.style.bottom = '';
- }
- }
- else {
- // if the item is not in a group:
- height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.centerContainer.height);
- this.dom.box.style.top = onTop ? '0' : '';
- this.dom.box.style.bottom = onTop ? '' : '0';
- }
- }
- this.dom.box.style.height = height + 'px';
- };
-
- module.exports = BackgroundItem;
-
-
-/***/ },
-/* 37 */
-/***/ function(module, exports, __webpack_require__) {
-
- var mousetrap = __webpack_require__(38);
- var Emitter = __webpack_require__(10);
- var Hammer = __webpack_require__(18);
- var util = __webpack_require__(1);
-
- /**
- * Turn an element into an clickToUse element.
- * When not active, the element has a transparent overlay. When the overlay is
- * clicked, the mode is changed to active.
- * When active, the element is displayed with a blue border around it, and
- * the interactive contents of the element can be used. When clicked outside
- * the element, the elements mode is changed to inactive.
- * @param {Element} container
- * @constructor
- */
- function Activator(container) {
- this.active = false;
-
- this.dom = {
- container: container
- };
-
- this.dom.overlay = document.createElement('div');
- this.dom.overlay.className = 'overlay';
-
- this.dom.container.appendChild(this.dom.overlay);
-
- this.hammer = Hammer(this.dom.overlay, {prevent_default: false});
- this.hammer.on('tap', this._onTapOverlay.bind(this));
-
- // block all touch events (except tap)
- var me = this;
- var events = [
- 'touch', 'pinch',
- 'doubletap', 'hold',
- 'dragstart', 'drag', 'dragend',
- 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox
- ];
- events.forEach(function (event) {
- me.hammer.on(event, function (event) {
- event.stopPropagation();
- });
- });
-
- // attach a tap event to the window, in order to deactivate when clicking outside the timeline
- this.windowHammer = Hammer(window, {prevent_default: false});
- this.windowHammer.on('tap', function (event) {
- // deactivate when clicked outside the container
- if (!_hasParent(event.target, container)) {
- me.deactivate();
- }
- });
-
- // mousetrap listener only bounded when active)
- this.escListener = this.deactivate.bind(this);
- }
-
- // turn into an event emitter
- Emitter(Activator.prototype);
-
- // The currently active activator
- Activator.current = null;
-
- /**
- * Destroy the activator. Cleans up all created DOM and event listeners
- */
- Activator.prototype.destroy = function () {
- this.deactivate();
-
- // remove dom
- this.dom.overlay.parentNode.removeChild(this.dom.overlay);
-
- // cleanup hammer instances
- this.hammer = null;
- this.windowHammer = null;
- // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory)
- };
-
- /**
- * Activate the element
- * Overlay is hidden, element is decorated with a blue shadow border
- */
- Activator.prototype.activate = function () {
- // we allow only one active activator at a time
- if (Activator.current) {
- Activator.current.deactivate();
- }
- Activator.current = this;
-
- this.active = true;
- this.dom.overlay.style.display = 'none';
- util.addClassName(this.dom.container, 'vis-active');
-
- this.emit('change');
- this.emit('activate');
-
- // ugly hack: bind ESC after emitting the events, as the Network rebinds all
- // keyboard events on a 'change' event
- mousetrap.bind('esc', this.escListener);
- };
-
- /**
- * Deactivate the element
- * Overlay is displayed on top of the element
- */
- Activator.prototype.deactivate = function () {
- this.active = false;
- this.dom.overlay.style.display = '';
- util.removeClassName(this.dom.container, 'vis-active');
- mousetrap.unbind('esc', this.escListener);
-
- this.emit('change');
- this.emit('deactivate');
- };
-
- /**
- * Handle a tap event: activate the container
- * @param event
- * @private
- */
- Activator.prototype._onTapOverlay = function (event) {
- // activate the container
- this.activate();
- event.stopPropagation();
- };
-
- /**
- * Test whether the element has the requested parent element somewhere in
- * its chain of parent nodes.
- * @param {HTMLElement} element
- * @param {HTMLElement} parent
- * @returns {boolean} Returns true when the parent is found somewhere in the
- * chain of parent nodes.
- * @private
- */
- function _hasParent(element, parent) {
- while (element) {
- if (element === parent) {
- return true
- }
- element = element.parentNode;
- }
- return false;
- }
-
- module.exports = Activator;
-
-
-/***/ },
-/* 38 */
-/***/ function(module, exports, __webpack_require__) {
-
- /**
- * Copyright 2012 Craig Campbell
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * Mousetrap is a simple keyboard shortcut library for Javascript with
- * no external dependencies
- *
- * @version 1.1.2
- * @url craig.is/killing/mice
- */
-
- /**
- * mapping of special keycodes to their corresponding keys
- *
- * everything in this dictionary cannot use keypress events
- * so it has to be here to map to the correct keycodes for
- * keyup/keydown events
- *
- * @type {Object}
- */
- var _MAP = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 16: 'shift',
- 17: 'ctrl',
- 18: 'alt',
- 20: 'capslock',
- 27: 'esc',
- 32: 'space',
- 33: 'pageup',
- 34: 'pagedown',
- 35: 'end',
- 36: 'home',
- 37: 'left',
- 38: 'up',
- 39: 'right',
- 40: 'down',
- 45: 'ins',
- 46: 'del',
- 91: 'meta',
- 93: 'meta',
- 224: 'meta'
- },
-
- /**
- * mapping for special characters so they can support
- *
- * this dictionary is only used incase you want to bind a
- * keyup or keydown event to one of these keys
- *
- * @type {Object}
- */
- _KEYCODE_MAP = {
- 106: '*',
- 107: '+',
- 109: '-',
- 110: '.',
- 111 : '/',
- 186: ';',
- 187: '=',
- 188: ',',
- 189: '-',
- 190: '.',
- 191: '/',
- 192: '`',
- 219: '[',
- 220: '\\',
- 221: ']',
- 222: '\''
- },
-
- /**
- * this is a mapping of keys that require shift on a US keypad
- * back to the non shift equivelents
- *
- * this is so you can use keyup events with these keys
- *
- * note that this will only work reliably on US keyboards
- *
- * @type {Object}
- */
- _SHIFT_MAP = {
- '~': '`',
- '!': '1',
- '@': '2',
- '#': '3',
- '$': '4',
- '%': '5',
- '^': '6',
- '&': '7',
- '*': '8',
- '(': '9',
- ')': '0',
- '_': '-',
- '+': '=',
- ':': ';',
- '\"': '\'',
- '<': ',',
- '>': '.',
- '?': '/',
- '|': '\\'
- },
-
- /**
- * this is a list of special strings you can use to map
- * to modifier keys when you specify your keyboard shortcuts
- *
- * @type {Object}
- */
- _SPECIAL_ALIASES = {
- 'option': 'alt',
- 'command': 'meta',
- 'return': 'enter',
- 'escape': 'esc'
- },
-
- /**
- * variable to store the flipped version of _MAP from above
- * needed to check if we should use keypress or not when no action
- * is specified
- *
- * @type {Object|undefined}
- */
- _REVERSE_MAP,
-
- /**
- * a list of all the callbacks setup via Mousetrap.bind()
- *
- * @type {Object}
- */
- _callbacks = {},
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
+ }
- /**
- * direct map of string combinations to callbacks used for trigger()
- *
- * @type {Object}
- */
- _direct_map = {},
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
+ };
- /**
- * keeps track of what level each sequence is at since multiple
- * sequences can start out with the same sequence
- *
- * @type {Object}
- */
- _sequence_levels = {},
- /**
- * variable to store the setTimeout call
- *
- * @type {null|number}
- */
- _reset_timer,
+ /**
+ * Redraw the component, mandatory function
+ * @return {boolean} Returns true if the component is resized
+ */
+ LineGraph.prototype.redraw = function() {
+ var resized = false;
- /**
- * temporary state where we will ignore the next keyup
- *
- * @type {boolean|string}
- */
- _ignore_next_keyup = false,
+ this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
+ if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
+ resized = true;
+ }
+ // check if this component is resized
+ resized = this._isResized() || resized;
+ // check whether zoomed (in that case we need to re-stack everything)
+ var visibleInterval = this.body.range.end - this.body.range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ this.lastVisibleInterval = visibleInterval;
+ this.lastWidth = this.width;
- /**
- * are we currently inside of a sequence?
- * type of action ("keyup" or "keydown" or "keypress") or false
- *
- * @type {boolean|string}
- */
- _inside_sequence = false;
+ // calculate actual size and position
+ this.width = this.dom.frame.offsetWidth;
- /**
- * loop through the f keys, f1 to f19 and add them to the map
- * programatically
- */
- for (var i = 1; i < 20; ++i) {
- _MAP[111 + i] = 'f' + i;
+ // the svg element is three times as big as the width, this allows for fully dragging left and right
+ // without reloading the graph. the controls for this are bound to events in the constructor
+ if (resized == true) {
+ this.svg.style.width = util.option.asSize(3*this.width);
+ this.svg.style.left = util.option.asSize(-this.width);
}
- /**
- * loop through to map numbers on the numeric keypad
- */
- for (i = 0; i <= 9; ++i) {
- _MAP[i + 96] = i;
+ if (zoomed == true || this.abortedGraphUpdate == true) {
+ this._updateGraph();
}
-
- /**
- * cross browser add event method
- *
- * @param {Element|HTMLDocument} object
- * @param {string} type
- * @param {Function} callback
- * @returns void
- */
- function _addEvent(object, type, callback) {
- if (object.addEventListener) {
- return object.addEventListener(type, callback, false);
+ else {
+ // move the whole svg while dragging
+ if (this.lastStart != 0) {
+ var offset = this.body.range.start - this.lastStart;
+ var range = this.body.range.end - this.body.range.start;
+ if (this.width != 0) {
+ var rangePerPixelInv = this.width/range;
+ var xOffset = offset * rangePerPixelInv;
+ this.svg.style.left = (-this.width - xOffset) + "px";
}
+ }
- object.attachEvent('on' + type, callback);
}
- /**
- * takes the event and returns the key character
- *
- * @param {Event} e
- * @return {string}
- */
- function _characterFromEvent(e) {
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
- // for keypress events we should return the character as is
- if (e.type == 'keypress') {
- return String.fromCharCode(e.which);
+ return resized;
+ };
+
+ /**
+ * Update and redraw the graph.
+ *
+ */
+ LineGraph.prototype._updateGraph = function () {
+ // reset the svg elements
+ DOMutil.prepareElements(this.svgElements);
+ if (this.width != 0 && this.itemsData != null) {
+ var group, i;
+ var preprocessedGroupData = {};
+ var processedGroupData = {};
+ var groupRanges = {};
+ var changeCalled = false;
+
+ // getting group Ids
+ var groupIds = [];
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ group = this.groups[groupId];
+ if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
+ groupIds.push(groupId);
+ }
+ }
+ }
+ if (groupIds.length > 0) {
+ // this is the range of the SVG canvas
+ var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
+ var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
+ var groupsData = {};
+ // fill groups data
+ this._getRelevantData(groupIds, groupsData, minDate, maxDate);
+ // we transform the X coordinates to detect collisions
+ for (i = 0; i < groupIds.length; i++) {
+ preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
}
+ // now all needed data has been collected we start the processing.
+ this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
- // for non keypress events the special maps are needed
- if (_MAP[e.which]) {
- return _MAP[e.which];
+ // update the Y axis first, we use this data to draw at the correct Y points
+ // changeCalled is required to clean the SVG on a change emit.
+ changeCalled = this._updateYAxis(groupIds, groupRanges);
+ if (changeCalled == true) {
+ DOMutil.cleanupElements(this.svgElements);
+ this.abortedGraphUpdate = true;
+ this.body.emitter.emit("change");
+ return;
}
+ this.abortedGraphUpdate = false;
- if (_KEYCODE_MAP[e.which]) {
- return _KEYCODE_MAP[e.which];
+ // With the yAxis scaled correctly, use this to get the Y values of the points.
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
}
- // if it is not in the special map
- return String.fromCharCode(e.which).toLowerCase();
+
+ // draw the groups
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.options.style == 'line') {
+ this._drawLineGraph(processedGroupData[groupIds[i]], group);
+ }
+ }
+ this._drawBarGraphs(groupIds, processedGroupData);
+ }
}
- /**
- * should we stop this event before firing off callbacks
- *
- * @param {Event} e
- * @return {boolean}
- */
- function _stop(e) {
- var element = e.target || e.srcElement,
- tag_name = element.tagName;
+ // cleanup unused svg elements
+ DOMutil.cleanupElements(this.svgElements);
+ };
- // if the element has the class "mousetrap" then no need to stop
- if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
- return false;
+
+ LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
+ // first select and preprocess the data from the datasets.
+ // the groups have their preselection of data, we now loop over this data to see
+ // what data we need to draw. Sorted data is much faster.
+ // more optimization is possible by doing the sampling before and using the binary search
+ // to find the end date to determine the increment.
+ var group, i, j, item;
+ if (groupIds.length > 0) {
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ groupsData[groupIds[i]] = [];
+ var dataContainer = groupsData[groupIds[i]];
+ // optimization for sorted data
+ if (group.options.sort == true) {
+ var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
+ for (j = guess; j < group.itemsData.length; j++) {
+ item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > maxDate) {
+ dataContainer.push(item);
+ break;
+ }
+ else {
+ dataContainer.push(item);
+ }
+ }
+ }
}
+ else {
+ for (j = 0; j < group.itemsData.length; j++) {
+ item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > minDate && item.x < maxDate) {
+ dataContainer.push(item);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ this._applySampling(groupIds, groupsData);
+ };
+
+ LineGraph.prototype._applySampling = function (groupIds, groupsData) {
+ var group;
+ if (groupIds.length > 0) {
+ for (var i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.options.sampling == true) {
+ var dataContainer = groupsData[groupIds[i]];
+ if (dataContainer.length > 0) {
+ var increment = 1;
+ var amountOfPoints = dataContainer.length;
+
+ // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
+ // of width changing of the yAxis.
+ var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
+ var pointsPerPixel = amountOfPoints / xDistance;
+ increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
- // stop for input, select, and textarea
- return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
- }
+ var sampledData = [];
+ for (var j = 0; j < amountOfPoints; j += increment) {
+ sampledData.push(dataContainer[j]);
- /**
- * checks if two arrays are equal
- *
- * @param {Array} modifiers1
- * @param {Array} modifiers2
- * @returns {boolean}
- */
- function _modifiersMatch(modifiers1, modifiers2) {
- return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+ }
+ groupsData[groupIds[i]] = sampledData;
+ }
+ }
+ }
}
+ };
- /**
- * resets all sequence counters except for the ones passed in
- *
- * @param {Object} do_not_reset
- * @returns void
- */
- function _resetSequences(do_not_reset) {
- do_not_reset = do_not_reset || {};
+ LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
+ var groupData, group, i,j;
+ var barCombinedDataLeft = [];
+ var barCombinedDataRight = [];
+ var barCombinedData;
+ if (groupIds.length > 0) {
+ for (i = 0; i < groupIds.length; i++) {
+ groupData = groupsData[groupIds[i]];
+ if (groupData.length > 0) {
+ group = this.groups[groupIds[i]];
+ if (group.options.style == 'line' || group.options.barChart.handleOverlap != "stack") {
+ var yMin = groupData[0].y;
+ var yMax = groupData[0].y;
+ for (j = 0; j < groupData.length; j++) {
+ yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
+ yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
+ }
+ groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation};
+ }
+ else if (group.options.style == 'bar') {
+ if (group.options.yAxisOrientation == 'left') {
+ barCombinedData = barCombinedDataLeft;
+ }
+ else {
+ barCombinedData = barCombinedDataRight;
+ }
- var active_sequences = false,
- key;
+ groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true};
- for (key in _sequence_levels) {
- if (do_not_reset[key]) {
- active_sequences = true;
- continue;
+ // combine data
+ for (j = 0; j < groupData.length; j++) {
+ barCombinedData.push({
+ x: groupData[j].x,
+ y: groupData[j].y,
+ groupId: groupIds[i]
+ });
}
- _sequence_levels[key] = 0;
+ }
}
+ }
- if (!active_sequences) {
- _inside_sequence = false;
- }
+ var intersections;
+ if (barCombinedDataLeft.length > 0) {
+ // sort by time and by group
+ barCombinedDataLeft.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
+ }
+ });
+ intersections = {};
+ this._getDataIntersections(intersections, barCombinedDataLeft);
+ groupRanges["__barchartLeft"] = this._getStackedBarYRange(intersections, barCombinedDataLeft);
+ groupRanges["__barchartLeft"].yAxisOrientation = "left";
+ groupIds.push("__barchartLeft");
+ }
+ if (barCombinedDataRight.length > 0) {
+ // sort by time and by group
+ barCombinedDataRight.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
+ }
+ });
+ intersections = {};
+ this._getDataIntersections(intersections, barCombinedDataRight);
+ groupRanges["__barchartRight"] = this._getStackedBarYRange(intersections, barCombinedDataRight);
+ groupRanges["__barchartRight"].yAxisOrientation = "right";
+ groupIds.push("__barchartRight");
+ }
}
+ };
- /**
- * finds all callbacks that match based on the keycode, modifiers,
- * and action
- *
- * @param {string} character
- * @param {Array} modifiers
- * @param {string} action
- * @param {boolean=} remove - should we remove any matches
- * @param {string=} combination
- * @returns {Array}
- */
- function _getMatches(character, modifiers, action, remove, combination) {
- var i,
- callback,
- matches = [];
+ LineGraph.prototype._getStackedBarYRange = function (intersections, combinedData) {
+ var key;
+ var yMin = combinedData[0].y;
+ var yMax = combinedData[0].y;
+ for (var i = 0; i < combinedData.length; i++) {
+ key = combinedData[i].x;
+ if (intersections[key] === undefined) {
+ yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
+ yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
+ }
+ else {
+ intersections[key].accumulated += combinedData[i].y;
+ }
+ }
+ for (var xpos in intersections) {
+ if (intersections.hasOwnProperty(xpos)) {
+ yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
+ yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
+ }
+ }
- // if there are no events related to this keycode
- if (!_callbacks[character]) {
- return [];
- }
+ return {min: yMin, max: yMax};
+ };
- // if a modifier key is coming up on its own we should allow it
- if (action == 'keyup' && _isModifier(character)) {
- modifiers = [character];
- }
- // loop through all callbacks for the key that was pressed
- // and see if any of them match
- for (i = 0; i < _callbacks[character].length; ++i) {
- callback = _callbacks[character][i];
+ /**
+ * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
+ * @param {Array} groupIds
+ * @param {Object} groupRanges
+ * @private
+ */
+ LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
+ var changeCalled = false;
+ var yAxisLeftUsed = false;
+ var yAxisRightUsed = false;
+ var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
+ // if groups are present
+ if (groupIds.length > 0) {
+ for (var i = 0; i < groupIds.length; i++) {
+ if (groupRanges.hasOwnProperty(groupIds[i])) {
+ if (groupRanges[groupIds[i]].ignore !== true) {
+ minVal = groupRanges[groupIds[i]].min;
+ maxVal = groupRanges[groupIds[i]].max;
- // if this is a sequence but it is not at the right level
- // then move onto the next match
- if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
- continue;
+ if (groupRanges[groupIds[i]].yAxisOrientation == 'left') {
+ yAxisLeftUsed = true;
+ minLeft = minLeft > minVal ? minVal : minLeft;
+ maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
}
-
- // if the action we are looking for doesn't match the action we got
- // then we should keep going
- if (action != callback.action) {
- continue;
+ else {
+ yAxisRightUsed = true;
+ minRight = minRight > minVal ? minVal : minRight;
+ maxRight = maxRight < maxVal ? maxVal : maxRight;
}
+ }
+ }
+ }
- // if this is a keypress event that means that we need to only
- // look at the character, otherwise check the modifiers as
- // well
- if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
-
- // remove is used so if you change your mind and call bind a
- // second time with a new function the first one is overwritten
- if (remove && callback.combo == combination) {
- _callbacks[character].splice(i, 1);
- }
+ if (yAxisLeftUsed == true) {
+ this.yAxisLeft.setRange(minLeft, maxLeft);
+ }
+ if (yAxisRightUsed == true) {
+ this.yAxisRight.setRange(minRight, maxRight);
+ }
+ }
- matches.push(callback);
- }
- }
+ changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
+ changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
- return matches;
+ if (yAxisRightUsed == true && yAxisLeftUsed == true) {
+ this.yAxisLeft.drawIcons = true;
+ this.yAxisRight.drawIcons = true;
+ }
+ else {
+ this.yAxisLeft.drawIcons = false;
+ this.yAxisRight.drawIcons = false;
}
- /**
- * takes a key event and figures out what the modifiers are
- *
- * @param {Event} e
- * @returns {Array}
- */
- function _eventModifiers(e) {
- var modifiers = [];
+ this.yAxisRight.master = !yAxisLeftUsed;
- if (e.shiftKey) {
- modifiers.push('shift');
- }
+ if (this.yAxisRight.master == false) {
+ if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
+ else {this.yAxisLeft.lineOffset = 0;}
- if (e.altKey) {
- modifiers.push('alt');
- }
+ changeCalled = this.yAxisLeft.redraw() || changeCalled;
+ this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
+ }
+ else {
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
+ }
- if (e.ctrlKey) {
- modifiers.push('ctrl');
- }
+ // clean the accumulated lists
+ if (groupIds.indexOf("__barchartLeft") != -1) {
+ groupIds.splice(groupIds.indexOf("__barchartLeft"),1);
+ }
+ if (groupIds.indexOf("__barchartRight") != -1) {
+ groupIds.splice(groupIds.indexOf("__barchartRight"),1);
+ }
- if (e.metaKey) {
- modifiers.push('meta');
- }
+ return changeCalled;
+ };
- return modifiers;
+ /**
+ * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
+ *
+ * @param {boolean} axisUsed
+ * @returns {boolean}
+ * @private
+ * @param axis
+ */
+ LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
+ var changed = false;
+ if (axisUsed == false) {
+ if (axis.dom.frame.parentNode) {
+ axis.hide();
+ changed = true;
+ }
}
+ else {
+ if (!axis.dom.frame.parentNode) {
+ axis.show();
+ changed = true;
+ }
+ }
+ return changed;
+ };
- /**
- * actually calls the callback function
- *
- * if your callback function returns false this will use the jquery
- * convention - prevent default and stop propogation on the event
- *
- * @param {Function} callback
- * @param {Event} e
- * @returns void
- */
- function _fireCallback(callback, e) {
- if (callback(e) === false) {
- if (e.preventDefault) {
- e.preventDefault();
- }
- if (e.stopPropagation) {
- e.stopPropagation();
- }
+ /**
+ * draw a bar graph
+ *
+ * @param groupIds
+ * @param processedGroupData
+ */
+ LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) {
+ var combinedData = [];
+ var intersections = {};
+ var coreDistance;
+ var key, drawData;
+ var group;
+ var i,j;
+ var barPoints = 0;
- e.returnValue = false;
- e.cancelBubble = true;
+ // combine all barchart data
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.options.style == 'bar') {
+ if (group.visible == true && (this.options.groups.visibility[groupIds[i]] === undefined || this.options.groups.visibility[groupIds[i]] == true)) {
+ for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
+ combinedData.push({
+ x: processedGroupData[groupIds[i]][j].x,
+ y: processedGroupData[groupIds[i]][j].y,
+ groupId: groupIds[i]
+ });
+ barPoints += 1;
+ }
}
+ }
}
- /**
- * handles a character key event
- *
- * @param {string} character
- * @param {Event} e
- * @returns void
- */
- function _handleCharacter(character, e) {
-
- // if this event should not happen stop here
- if (_stop(e)) {
- return;
- }
+ if (barPoints == 0) {return;}
- var callbacks = _getMatches(character, _eventModifiers(e), e.type),
- i,
- do_not_reset = {},
- processed_sequence_callback = false;
+ // sort by time and by group
+ combinedData.sort(function (a, b) {
+ if (a.x == b.x) {
+ return a.groupId - b.groupId;
+ } else {
+ return a.x - b.x;
+ }
+ });
- // loop through matching callbacks for this key event
- for (i = 0; i < callbacks.length; ++i) {
+ // get intersections
+ this._getDataIntersections(intersections, combinedData);
- // fire for all sequence callbacks
- // this is because if for example you have multiple sequences
- // bound such as "g i" and "g t" they both need to fire the
- // callback for matching g cause otherwise you can only ever
- // match the first one
- if (callbacks[i].seq) {
- processed_sequence_callback = true;
+ // plot barchart
+ for (i = 0; i < combinedData.length; i++) {
+ group = this.groups[combinedData[i].groupId];
+ var minWidth = 0.1 * group.options.barChart.width;
- // keep a list of which sequences were matches for later
- do_not_reset[callbacks[i].seq] = 1;
- _fireCallback(callbacks[i].callback, e);
- continue;
- }
+ key = combinedData[i].x;
+ var heightOffset = 0;
+ if (intersections[key] === undefined) {
+ if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
+ if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
+ drawData = this._getSafeDrawData(coreDistance, group, minWidth);
+ }
+ else {
+ var nextKey = i + (intersections[key].amount - intersections[key].resolved);
+ var prevKey = i - (intersections[key].resolved + 1);
+ if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
+ if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
+ drawData = this._getSafeDrawData(coreDistance, group, minWidth);
+ intersections[key].resolved += 1;
- // if there were no sequence matches but we are still here
- // that means this is a regular match so we should fire that
- if (!processed_sequence_callback && !_inside_sequence) {
- _fireCallback(callbacks[i].callback, e);
- }
+ if (group.options.barChart.handleOverlap == 'stack') {
+ heightOffset = intersections[key].accumulated;
+ intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
+ }
+ else if (group.options.barChart.handleOverlap == 'sideBySide') {
+ drawData.width = drawData.width / intersections[key].amount;
+ drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
+ if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
+ else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
}
+ }
+ DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg);
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg);
+ }
+ }
+ };
- // if you are inside of a sequence and the key you are pressing
- // is not a modifier key then we should reset all sequences
- // that were not matched by this key event
- if (e.type == _inside_sequence && !_isModifier(character)) {
- _resetSequences(do_not_reset);
+ /**
+ * Fill the intersections object with counters of how many datapoints share the same x coordinates
+ * @param intersections
+ * @param combinedData
+ * @private
+ */
+ LineGraph.prototype._getDataIntersections = function (intersections, combinedData) {
+ // get intersections
+ var coreDistance;
+ for (var i = 0; i < combinedData.length; i++) {
+ if (i + 1 < combinedData.length) {
+ coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
+ }
+ if (i > 0) {
+ coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
+ }
+ if (coreDistance == 0) {
+ if (intersections[combinedData[i].x] === undefined) {
+ intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
}
+ intersections[combinedData[i].x].amount += 1;
+ }
}
+ };
- /**
- * handles a keydown event
- *
- * @param {Event} e
- * @returns void
- */
- function _handleKey(e) {
+ /**
+ * Get the width and offset for bargraphs based on the coredistance between datapoints
+ *
+ * @param coreDistance
+ * @param group
+ * @param minWidth
+ * @returns {{width: Number, offset: Number}}
+ * @private
+ */
+ LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) {
+ var width, offset;
+ if (coreDistance < group.options.barChart.width && coreDistance > 0) {
+ width = coreDistance < minWidth ? minWidth : coreDistance;
- // normalize e.which for key events
- // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
- e.which = typeof e.which == "number" ? e.which : e.keyCode;
+ offset = 0; // recalculate offset with the new width;
+ if (group.options.barChart.align == 'left') {
+ offset -= 0.5 * coreDistance;
+ }
+ else if (group.options.barChart.align == 'right') {
+ offset += 0.5 * coreDistance;
+ }
+ }
+ else {
+ // default settings
+ width = group.options.barChart.width;
+ offset = 0;
+ if (group.options.barChart.align == 'left') {
+ offset -= 0.5 * group.options.barChart.width;
+ }
+ else if (group.options.barChart.align == 'right') {
+ offset += 0.5 * group.options.barChart.width;
+ }
+ }
- var character = _characterFromEvent(e);
+ return {width: width, offset: offset};
+ };
- // no character found then stop
- if (!character) {
- return;
+
+ /**
+ * draw a line graph
+ *
+ * @param dataset
+ * @param group
+ */
+ LineGraph.prototype._drawLineGraph = function (dataset, group) {
+ if (dataset != null) {
+ if (dataset.length > 0) {
+ var path, d;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
+ path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
+ path.setAttributeNS(null, "class", group.className);
+
+ // construct path from dataset
+ if (group.options.catmullRom.enabled == true) {
+ d = this._catmullRom(dataset, group);
+ }
+ else {
+ d = this._linear(dataset);
}
- if (e.type == 'keyup' && _ignore_next_keyup == character) {
- _ignore_next_keyup = false;
- return;
+ // append with points for fill and finalize the path
+ if (group.options.shaded.enabled == true) {
+ var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
+ var dFill;
+ if (group.options.shaded.orientation == 'top') {
+ dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
+ }
+ else {
+ dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
+ }
+ fillPath.setAttributeNS(null, "class", group.className + " fill");
+ fillPath.setAttributeNS(null, "d", dFill);
}
+ // copy properties to path for drawing.
+ path.setAttributeNS(null, "d", "M" + d);
- _handleCharacter(character, e);
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ this._drawPoints(dataset, group, this.svgElements, this.svg);
+ }
+ }
}
+ };
- /**
- * determines if the keycode specified is a modifier key or not
- *
- * @param {string} key
- * @returns {boolean}
- */
- function _isModifier(key) {
- return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+ /**
+ * draw the data points
+ *
+ * @param {Array} dataset
+ * @param {Object} JSONcontainer
+ * @param {Object} svg | SVG DOM element
+ * @param {GraphGroup} group
+ * @param {Number} [offset]
+ */
+ LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
+ if (offset === undefined) {offset = 0;}
+ for (var i = 0; i < dataset.length; i++) {
+ DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
}
+ };
- /**
- * called to set a 1 second timeout on the specified sequence
- *
- * this is so after each key press in the sequence you have 1 second
- * to press the next key before you have to start over
- *
- * @returns void
- */
- function _resetSequenceTimer() {
- clearTimeout(_reset_timer);
- _reset_timer = setTimeout(_resetSequences, 1000);
- }
- /**
- * reverses the map lookup so that we can look for specific keys
- * to see what can and can't use keypress
- *
- * @return {Object}
- */
- function _getReverseMap() {
- if (!_REVERSE_MAP) {
- _REVERSE_MAP = {};
- for (var key in _MAP) {
- // pull out the numeric keypad from here cause keypress should
- // be able to detect the keys from the character
- if (key > 95 && key < 112) {
- continue;
- }
+ /**
+ * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
+ * the yAxis.
+ *
+ * @param datapoints
+ * @returns {Array}
+ * @private
+ */
+ LineGraph.prototype._convertXcoordinates = function (datapoints) {
+ var extractedData = [];
+ var xValue, yValue;
+ var toScreen = this.body.util.toScreen;
- if (_MAP.hasOwnProperty(key)) {
- _REVERSE_MAP[_MAP[key]] = key;
- }
- }
- }
- return _REVERSE_MAP;
+ for (var i = 0; i < datapoints.length; i++) {
+ xValue = toScreen(datapoints[i].x) + this.width;
+ yValue = datapoints[i].y;
+ extractedData.push({x: xValue, y: yValue});
}
- /**
- * picks the best action based on the key combination
- *
- * @param {string} key - character for key
- * @param {Array} modifiers
- * @param {string=} action passed in
- */
- function _pickBestAction(key, modifiers, action) {
+ return extractedData;
+ };
- // if no action was picked in we should try to pick the one
- // that we think would work best for this key
- if (!action) {
- action = _getReverseMap()[key] ? 'keydown' : 'keypress';
- }
- // modifier keys don't work as expected with keypress,
- // switch to keydown
- if (action == 'keypress' && modifiers.length) {
- action = 'keydown';
- }
- return action;
+ /**
+ * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
+ * the yAxis.
+ *
+ * @param datapoints
+ * @returns {Array}
+ * @private
+ */
+ LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
+ var extractedData = [];
+ var xValue, yValue;
+ var toScreen = this.body.util.toScreen;
+ var axis = this.yAxisLeft;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
+ if (group.options.yAxisOrientation == 'right') {
+ axis = this.yAxisRight;
}
- /**
- * binds a key sequence to an event
- *
- * @param {string} combo - combo specified in bind call
- * @param {Array} keys
- * @param {Function} callback
- * @param {string=} action
- * @returns void
- */
- function _bindSequence(combo, keys, callback, action) {
-
- // start off by adding a sequence level record for this combination
- // and setting the level to 0
- _sequence_levels[combo] = 0;
+ for (var i = 0; i < datapoints.length; i++) {
+ xValue = toScreen(datapoints[i].x) + this.width;
+ yValue = Math.round(axis.convertValue(datapoints[i].y));
+ extractedData.push({x: xValue, y: yValue});
+ }
- // if there is no action pick the best one for the first key
- // in the sequence
- if (!action) {
- action = _pickBestAction(keys[0], []);
- }
+ group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
- /**
- * callback to increase the sequence level for this sequence and reset
- * all other sequences that were active
- *
- * @param {Event} e
- * @returns void
- */
- var _increaseSequence = function(e) {
- _inside_sequence = action;
- ++_sequence_levels[combo];
- _resetSequenceTimer();
- },
+ return extractedData;
+ };
- /**
- * wraps the specified callback inside of another function in order
- * to reset all sequence counters as soon as this sequence is done
- *
- * @param {Event} e
- * @returns void
- */
- _callbackAndReset = function(e) {
- _fireCallback(callback, e);
+ /**
+ * This uses an uniform parametrization of the CatmullRom algorithm:
+ * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
+ * @param data
+ * @returns {string}
+ * @private
+ */
+ LineGraph.prototype._catmullRomUniform = function(data) {
+ // catmull rom
+ var p0, p1, p2, p3, bp1, bp2;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var normalization = 1/6;
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- // we should ignore the next key up if the action is key down
- // or keypress. this is so if you finish a sequence and
- // release the key the final key will not trigger a keyup
- if (action !== 'keyup') {
- _ignore_next_keyup = _characterFromEvent(e);
- }
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
- // weird race condition if a sequence ends with the key
- // another sequence begins with
- setTimeout(_resetSequences, 10);
- },
- i;
- // loop through keys one at a time and bind the appropriate callback
- // function. for any key leading up to the final one it should
- // increase the sequence. after the final, it should reset all sequences
- for (i = 0; i < keys.length; ++i) {
- _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
- }
- }
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ // 0 1 0 0
+ // -1/6 1 1/6 0
+ // 0 1/6 1 -1/6
+ // 0 0 1 0
- /**
- * binds a single keyboard combination
- *
- * @param {string} combination
- * @param {Function} callback
- * @param {string=} action
- * @param {string=} sequence_name - name of sequence if part of sequence
- * @param {number=} level - what part of the sequence the command is
- * @returns void
- */
- function _bindSingle(combination, callback, action, sequence_name, level) {
+ // bp0 = { x: p1.x, y: p1.y };
+ bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
+ bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
+ // bp0 = { x: p2.x, y: p2.y };
- // make sure multiple spaces in a row become a single space
- combination = combination.replace(/\s+/g, ' ');
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
+ }
- var sequence = combination.split(' '),
- i,
- key,
- keys,
- modifiers = [];
+ return d;
+ };
- // if this pattern is a sequence of keys then run through this method
- // to reprocess each pattern one key at a time
- if (sequence.length > 1) {
- return _bindSequence(combination, sequence, callback, action);
- }
+ /**
+ * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
+ * By default, the centripetal parameterization is used because this gives the nicest results.
+ * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
+ *
+ * One optimization can be used to reuse distances since this is a sliding window approach.
+ * @param data
+ * @returns {string}
+ * @private
+ */
+ LineGraph.prototype._catmullRom = function(data, group) {
+ var alpha = group.options.catmullRom.alpha;
+ if (alpha == 0 || alpha === undefined) {
+ return this._catmullRomUniform(data);
+ }
+ else {
+ var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
+ var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- // take the keys from this pattern and figure out what the actual
- // pattern is all about
- keys = combination === '+' ? ['+'] : combination.split('+');
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
- for (i = 0; i < keys.length; ++i) {
- key = keys[i];
+ d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
+ d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
+ d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
- // normalize key names
- if (_SPECIAL_ALIASES[key]) {
- key = _SPECIAL_ALIASES[key];
- }
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ //
+ // A = 2d1^2a + 3d1^a * d2^a + d3^2a
+ // B = 2d3^2a + 3d3^a * d2^a + d2^2a
+ //
+ // [ 0 1 0 0 ]
+ // [ -d2^2a/N A/N d1^2a/N 0 ]
+ // [ 0 d3^2a/M B/M -d2^2a/M ]
+ // [ 0 0 1 0 ]
- // if this is not a keypress event then we should
- // be smart about using shift keys
- // this will only work for US keyboards however
- if (action && action != 'keypress' && _SHIFT_MAP[key]) {
- key = _SHIFT_MAP[key];
- modifiers.push('shift');
- }
+ // [ 0 1 0 0 ]
+ // [ -d2pow2a/N A/N d1pow2a/N 0 ]
+ // [ 0 d3pow2a/M B/M -d2pow2a/M ]
+ // [ 0 0 1 0 ]
- // if this key is a modifier then add it to the list of modifiers
- if (_isModifier(key)) {
- modifiers.push(key);
- }
- }
+ d3powA = Math.pow(d3, alpha);
+ d3pow2A = Math.pow(d3,2*alpha);
+ d2powA = Math.pow(d2, alpha);
+ d2pow2A = Math.pow(d2,2*alpha);
+ d1powA = Math.pow(d1, alpha);
+ d1pow2A = Math.pow(d1,2*alpha);
- // depending on what the key combination is
- // we will try to pick the best event for it
- action = _pickBestAction(key, modifiers, action);
+ A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
+ B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
+ N = 3*d1powA * (d1powA + d2powA);
+ if (N > 0) {N = 1 / N;}
+ M = 3*d3powA * (d3powA + d2powA);
+ if (M > 0) {M = 1 / M;}
- // make sure to initialize array if this is the first time
- // a callback is added for this key
- if (!_callbacks[key]) {
- _callbacks[key] = [];
- }
+ bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
+ y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
- // remove an existing match if there is one
- _getMatches(key, modifiers, action, !sequence_name, combination);
+ bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
+ y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
- // add this call back to the array
- // if it is a sequence put it at the beginning
- // if not put it at the end
- //
- // this is important because the way these are processed expects
- // the sequence ones to come first
- _callbacks[key][sequence_name ? 'unshift' : 'push']({
- callback: callback,
- modifiers: modifiers,
- action: action,
- seq: sequence_name,
- level: level,
- combo: combination
- });
+ if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
+ if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
+ }
+
+ return d;
}
+ };
- /**
- * binds multiple combinations to the same callback
- *
- * @param {Array} combinations
- * @param {Function} callback
- * @param {string|undefined} action
- * @returns void
- */
- function _bindMultiple(combinations, callback, action) {
- for (var i = 0; i < combinations.length; ++i) {
- _bindSingle(combinations[i], callback, action);
- }
+ /**
+ * this generates the SVG path for a linear drawing between datapoints.
+ * @param data
+ * @returns {string}
+ * @private
+ */
+ LineGraph.prototype._linear = function(data) {
+ // linear
+ var d = "";
+ for (var i = 0; i < data.length; i++) {
+ if (i == 0) {
+ d += data[i].x + "," + data[i].y;
+ }
+ else {
+ d += " " + data[i].x + "," + data[i].y;
+ }
}
+ return d;
+ };
- // start!
- _addEvent(document, 'keypress', _handleKey);
- _addEvent(document, 'keydown', _handleKey);
- _addEvent(document, 'keyup', _handleKey);
+ module.exports = LineGraph;
- var mousetrap = {
- /**
- * binds an event to mousetrap
- *
- * can be a single key, a combination of keys separated with +,
- * a comma separated list of keys, an array of keys, or
- * a sequence of keys separated by spaces
- *
- * be sure to list the modifier keys first to make sure that the
- * correct key ends up getting bound (the last key in the pattern)
- *
- * @param {string|Array} keys
- * @param {Function} callback
- * @param {string=} action - 'keypress', 'keydown', or 'keyup'
- * @returns void
- */
- bind: function(keys, callback, action) {
- _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
- _direct_map[keys + ':' + action] = callback;
- return this;
- },
+/***/ },
+/* 35 */
+/***/ function(module, exports, __webpack_require__) {
- /**
- * unbinds an event to mousetrap
- *
- * the unbinding sets the callback function of the specified key combo
- * to an empty function and deletes the corresponding key in the
- * _direct_map dict.
- *
- * the keycombo+action has to be exactly the same as
- * it was defined in the bind method
- *
- * TODO: actually remove this from the _callbacks dictionary instead
- * of binding an empty function
- *
- * @param {string|Array} keys
- * @param {string} action
- * @returns void
- */
- unbind: function(keys, action) {
- if (_direct_map[keys + ':' + action]) {
- delete _direct_map[keys + ':' + action];
- this.bind(keys, function() {}, action);
- }
- return this;
- },
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+ var Component = __webpack_require__(22);
+ var DataStep = __webpack_require__(36);
- /**
- * triggers an event that has already been bound
- *
- * @param {string} keys
- * @param {string=} action
- * @returns void
- */
- trigger: function(keys, action) {
- _direct_map[keys + ':' + action]();
- return this;
- },
+ /**
+ * A horizontal time axis
+ * @param {Object} [options] See DataAxis.setOptions for the available
+ * options.
+ * @constructor DataAxis
+ * @extends Component
+ * @param body
+ */
+ function DataAxis (body, options, svg, linegraphOptions) {
+ this.id = util.randomUUID();
+ this.body = body;
- /**
- * resets the library back to its initial state. this is useful
- * if you want to clear out the current keyboard shortcuts and bind
- * new ones - for example if you switch to another page
- *
- * @returns void
- */
- reset: function() {
- _callbacks = {};
- _direct_map = {};
- return this;
- }
+ this.defaultOptions = {
+ orientation: 'left', // supported: 'left', 'right'
+ showMinorLabels: true,
+ showMajorLabels: true,
+ icons: true,
+ majorLinesOffset: 7,
+ minorLinesOffset: 4,
+ labelOffsetX: 10,
+ labelOffsetY: 2,
+ iconWidth: 20,
+ width: '40px',
+ visible: true,
+ customRange: {
+ left: {min:undefined, max:undefined},
+ right: {min:undefined, max:undefined}
+ }
};
- module.exports = mousetrap;
-
-
+ this.linegraphOptions = linegraphOptions;
+ this.linegraphSVG = svg;
+ this.props = {};
+ this.DOMelements = { // dynamic elements
+ lines: {},
+ labels: {}
+ };
-/***/ },
-/* 39 */
-/***/ function(module, exports, __webpack_require__) {
+ this.dom = {};
- var Emitter = __webpack_require__(10);
- var Hammer = __webpack_require__(18);
- var util = __webpack_require__(1);
- var DataSet = __webpack_require__(7);
- var DataView = __webpack_require__(8);
- var Range = __webpack_require__(20);
- var Core = __webpack_require__(24);
- var TimeAxis = __webpack_require__(25);
- var CurrentTime = __webpack_require__(27);
- var CustomTime = __webpack_require__(29);
- var LineGraph = __webpack_require__(40);
+ this.range = {start:0, end:0};
- /**
- * Create a timeline visualization
- * @param {HTMLElement} container
- * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
- * @param {Object} [options] See Graph2d.setOptions for the available options.
- * @constructor
- * @extends Core
- */
- function Graph2d (container, items, groups, options) {
- // if the third element is options, the forth is groups (optionally);
- if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) {
- var forthArgument = options;
- options = groups;
- groups = forthArgument;
- }
+ this.options = util.extend({}, this.defaultOptions);
+ this.conversionFactor = 1;
- var me = this;
- this.defaultOptions = {
- start: null,
- end: null,
+ this.setOptions(options);
+ this.width = Number(('' + this.options.width).replace("px",""));
+ this.minWidth = this.width;
+ this.height = this.linegraphSVG.offsetHeight;
- autoResize: true,
+ this.stepPixels = 25;
+ this.stepPixelsForced = 25;
+ this.lineOffset = 0;
+ this.master = true;
+ this.svgElements = {};
- orientation: 'bottom',
- width: null,
- height: null,
- maxHeight: null,
- minHeight: null
- };
- this.options = util.deepExtend({}, this.defaultOptions);
- // Create the DOM, props, and emitter
- this._create(container);
+ this.groups = {};
+ this.amountOfGroups = 0;
- // all components listed here will be repainted automatically
- this.components = [];
+ // create the HTML DOM
+ this._create();
+ }
- this.body = {
- dom: this.dom,
- domProps: this.props,
- emitter: {
- on: this.on.bind(this),
- off: this.off.bind(this),
- emit: this.emit.bind(this)
- },
- util: {
- snap: null, // will be specified after TimeAxis is created
- toScreen: me._toScreen.bind(me),
- toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
- toTime: me._toTime.bind(me),
- toGlobalTime : me._toGlobalTime.bind(me)
- }
- };
+ DataAxis.prototype = new Component();
- // range
- this.range = new Range(this.body);
- this.components.push(this.range);
- this.body.range = this.range;
- // time axis
- this.timeAxis = new TimeAxis(this.body);
- this.components.push(this.timeAxis);
- this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
- // current time bar
- this.currentTime = new CurrentTime(this.body);
- this.components.push(this.currentTime);
+ DataAxis.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
+ }
+ this.amountOfGroups += 1;
+ };
- // custom time bar
- // Note: time bar will be attached in this.setOptions when selected
- this.customTime = new CustomTime(this.body);
- this.components.push(this.customTime);
+ DataAxis.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+ };
- // item set
- this.linegraph = new LineGraph(this.body);
- this.components.push(this.linegraph);
+ DataAxis.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
+ }
+ };
- this.itemsData = null; // DataSet
- this.groupsData = null; // DataSet
- // apply options
+ DataAxis.prototype.setOptions = function (options) {
if (options) {
- this.setOptions(options);
- }
+ var redraw = false;
+ if (this.options.orientation != options.orientation && options.orientation !== undefined) {
+ redraw = true;
+ }
+ var fields = [
+ 'orientation',
+ 'showMinorLabels',
+ 'showMajorLabels',
+ 'icons',
+ 'majorLinesOffset',
+ 'minorLinesOffset',
+ 'labelOffsetX',
+ 'labelOffsetY',
+ 'iconWidth',
+ 'width',
+ 'visible',
+ 'customRange'
+ ];
+ util.selectiveExtend(fields, this.options, options);
- // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
- if (groups) {
- this.setGroups(groups);
- }
+ this.minWidth = Number(('' + this.options.width).replace("px",""));
- // create itemset
- if (items) {
- this.setItems(items);
- }
- else {
- this.redraw();
+ if (redraw == true && this.dom.frame) {
+ this.hide();
+ this.show();
+ }
}
- }
+ };
- // Extend the functionality from Core
- Graph2d.prototype = new Core();
/**
- * Set items
- * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
+ * Create the HTML DOM for the DataAxis
*/
- Graph2d.prototype.setItems = function(items) {
- var initialLoad = (this.itemsData == null);
+ DataAxis.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.style.width = this.options.width;
+ this.dom.frame.style.height = this.height;
- // convert to type DataSet when needed
- var newDataSet;
- if (!items) {
- newDataSet = null;
- }
- else if (items instanceof DataSet || items instanceof DataView) {
- newDataSet = items;
+ this.dom.lineContainer = document.createElement('div');
+ this.dom.lineContainer.style.width = '100%';
+ this.dom.lineContainer.style.height = this.height;
+
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "absolute";
+ this.svg.style.top = '0px';
+ this.svg.style.height = '100%';
+ this.svg.style.width = '100%';
+ this.svg.style.display = "block";
+ this.dom.frame.appendChild(this.svg);
+ };
+
+ DataAxis.prototype._redrawGroupIcons = function () {
+ DOMutil.prepareElements(this.svgElements);
+
+ var x;
+ var iconWidth = this.options.iconWidth;
+ var iconHeight = 15;
+ var iconOffset = 4;
+ var y = iconOffset + 0.5 * iconHeight;
+
+ if (this.options.orientation == 'left') {
+ x = iconOffset;
}
else {
- // turn an array into a dataset
- newDataSet = new DataSet(items, {
- type: {
- start: 'Date',
- end: 'Date'
- }
- });
+ x = this.width - iconWidth - iconOffset;
}
- // set items
- this.itemsData = newDataSet;
- this.linegraph && this.linegraph.setItems(newDataSet);
-
- if (initialLoad) {
- if (this.options.start != undefined || this.options.end != undefined) {
- var start = this.options.start != undefined ? this.options.start : null;
- var end = this.options.end != undefined ? this.options.end : null;
-
- this.setWindow(start, end, {animate: false});
- }
- else {
- this.fit({animate: false});
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + iconOffset;
+ }
}
}
+
+ DOMutil.cleanupElements(this.svgElements);
};
/**
- * Set groups
- * @param {vis.DataSet | Array | google.visualization.DataTable} groups
+ * Create the HTML DOM for the DataAxis
*/
- Graph2d.prototype.setGroups = function(groups) {
- // convert to type DataSet when needed
- var newDataSet;
- if (!groups) {
- newDataSet = null;
- }
- else if (groups instanceof DataSet || groups instanceof DataView) {
- newDataSet = groups;
- }
- else {
- // turn an array into a dataset
- newDataSet = new DataSet(groups);
+ DataAxis.prototype.show = function() {
+ if (!this.dom.frame.parentNode) {
+ if (this.options.orientation == 'left') {
+ this.body.dom.left.appendChild(this.dom.frame);
+ }
+ else {
+ this.body.dom.right.appendChild(this.dom.frame);
+ }
}
- this.groupsData = newDataSet;
- this.linegraph.setGroups(newDataSet);
+ if (!this.dom.lineContainer.parentNode) {
+ this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
+ }
};
/**
- * Returns an object containing an SVG element with the icon of the group (size determined by iconWidth and iconHeight), the label of the group (content) and the yAxisOrientation of the group (left or right).
- * @param groupId
- * @param width
- * @param height
+ * Create the HTML DOM for the DataAxis
*/
- Graph2d.prototype.getLegend = function(groupId, width, height) {
- if (width === undefined) {width = 15;}
- if (height === undefined) {height = 15;}
- if (this.linegraph.groups[groupId] !== undefined) {
- return this.linegraph.groups[groupId].getLegend(width,height);
+ DataAxis.prototype.hide = function() {
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
}
- else {
- return "cannot find group:" + groupId;
+
+ if (this.dom.lineContainer.parentNode) {
+ this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
}
- }
+ };
/**
- * This checks if the visible option of the supplied group (by ID) is true or false.
- * @param groupId
- * @returns {*}
+ * Set a range (start and end)
+ * @param end
+ * @param start
+ * @param end
*/
- Graph2d.prototype.isGroupVisible = function(groupId) {
- if (this.linegraph.groups[groupId] !== undefined) {
- return (this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true));
- }
- else {
- return false;
- }
- }
-
+ DataAxis.prototype.setRange = function (start, end) {
+ this.range.start = start;
+ this.range.end = end;
+ };
/**
- * Get the data range of the item set.
- * @returns {{min: Date, max: Date}} range A range with a start and end Date.
- * When no minimum is found, min==null
- * When no maximum is found, max==null
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
*/
- Graph2d.prototype.getItemRange = function() {
- var min = null;
- var max = null;
-
- // calculate min from start filed
- for (var groupId in this.linegraph.groups) {
- if (this.linegraph.groups.hasOwnProperty(groupId)) {
- if (this.linegraph.groups[groupId].visible == true) {
- for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) {
- var item = this.linegraph.groups[groupId].itemsData[i];
- var value = util.convert(item.x, 'Date').valueOf();
- min = min == null ? value : min > value ? value : min;
- max = max == null ? value : max < value ? value : max;
- }
+ DataAxis.prototype.redraw = function () {
+ var changeCalled = false;
+ var activeGroups = 0;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ activeGroups++;
}
}
}
+ if (this.amountOfGroups == 0 || activeGroups == 0) {
+ this.hide();
+ }
+ else {
+ this.show();
+ this.height = Number(this.linegraphSVG.style.height.replace("px",""));
+ // svg offsetheight did not work in firefox and explorer...
- return {
- min: (min != null) ? new Date(min) : null,
- max: (max != null) ? new Date(max) : null
- };
- };
-
-
+ this.dom.lineContainer.style.height = this.height + 'px';
+ this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
- module.exports = Graph2d;
+ var props = this.props;
+ var frame = this.dom.frame;
+ // update classname
+ frame.className = 'dataaxis';
-/***/ },
-/* 40 */
-/***/ function(module, exports, __webpack_require__) {
+ // calculate character width and height
+ this._calculateCharSize();
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var DataSet = __webpack_require__(7);
- var DataView = __webpack_require__(8);
- var Component = __webpack_require__(22);
- var DataAxis = __webpack_require__(41);
- var GraphGroup = __webpack_require__(43);
- var Legend = __webpack_require__(44);
+ var orientation = this.options.orientation;
+ var showMinorLabels = this.options.showMinorLabels;
+ var showMajorLabels = this.options.showMajorLabels;
- var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
+ // determine the width and height of the elemens for the axis
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
- /**
- * This is the constructor of the LineGraph. It requires a Timeline body and options.
- *
- * @param body
- * @param options
- * @constructor
- */
- function LineGraph(body, options) {
- this.id = util.randomUUID();
- this.body = body;
+ props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
+ props.minorLineHeight = 1;
+ props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
+ props.majorLineHeight = 1;
- this.defaultOptions = {
- yAxisOrientation: 'left',
- defaultGroup: 'default',
- sort: true,
- sampling: true,
- graphHeight: '400px',
- shaded: {
- enabled: false,
- orientation: 'bottom' // top, bottom
- },
- style: 'line', // line, bar
- barChart: {
- width: 50,
- handleOverlap: 'overlap',
- align: 'center' // left, center, right
- },
- catmullRom: {
- enabled: true,
- parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
- alpha: 0.5
- },
- drawPoints: {
- enabled: true,
- size: 6,
- style: 'square' // square, circle
- },
- dataAxis: {
- showMinorLabels: true,
- showMajorLabels: true,
- icons: false,
- width: '40px',
- visible: true,
- customRange: {
- left: {min:undefined, max:undefined},
- right: {min:undefined, max:undefined}
- }
- },
- legend: {
- enabled: false,
- icons: true,
- left: {
- visible: true,
- position: 'top-left' // top/bottom - left,right
- },
- right: {
- visible: true,
- position: 'top-right' // top/bottom - left,right
- }
- },
- groups: {
- visibility: {}
+ // take frame offline while updating (is almost twice as fast)
+ if (orientation == 'left') {
+ frame.style.top = '0';
+ frame.style.left = '0';
+ frame.style.bottom = '';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
}
- };
-
- // options is shared by this ItemSet and all its items
- this.options = util.extend({}, this.defaultOptions);
- this.dom = {};
- this.props = {};
- this.hammer = null;
- this.groups = {};
- this.abortedGraphUpdate = false;
-
- var me = this;
- this.itemsData = null; // DataSet
- this.groupsData = null; // DataSet
-
- // listeners for the DataSet of the items
- this.itemListeners = {
- 'add': function (event, params, senderId) {
- me._onAdd(params.items);
- },
- 'update': function (event, params, senderId) {
- me._onUpdate(params.items);
- },
- 'remove': function (event, params, senderId) {
- me._onRemove(params.items);
+ else { // right
+ frame.style.top = '';
+ frame.style.bottom = '0';
+ frame.style.left = '0';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
}
- };
-
- // listeners for the DataSet of the groups
- this.groupListeners = {
- 'add': function (event, params, senderId) {
- me._onAddGroups(params.items);
- },
- 'update': function (event, params, senderId) {
- me._onUpdateGroups(params.items);
- },
- 'remove': function (event, params, senderId) {
- me._onRemoveGroups(params.items);
+ changeCalled = this._redrawLabels();
+ if (this.options.icons == true) {
+ this._redrawGroupIcons();
}
- };
-
- this.items = {}; // object with an Item for every data item
- this.selection = []; // list with the ids of all selected nodes
- this.lastStart = this.body.range.start;
- this.touchParams = {}; // stores properties while dragging
-
- this.svgElements = {};
- this.setOptions(options);
- this.groupsUsingDefaultStyles = [0];
-
- this.body.emitter.on("rangechanged", function() {
- me.lastStart = me.body.range.start;
- me.svg.style.left = util.option.asSize(-me.width);
- me._updateGraph.apply(me);
- });
-
- // create the HTML DOM
- this._create();
- this.body.emitter.emit("change");
- }
-
- LineGraph.prototype = new Component();
+ }
+ return changeCalled;
+ };
/**
- * Create the HTML DOM for the ItemSet
+ * Repaint major and minor text labels and vertical grid lines
+ * @private
*/
- LineGraph.prototype._create = function(){
- var frame = document.createElement('div');
- frame.className = 'LineGraph';
- this.dom.frame = frame;
+ DataAxis.prototype._redrawLabels = function () {
+ DOMutil.prepareElements(this.DOMelements.lines);
+ DOMutil.prepareElements(this.DOMelements.labels);
- // create svg element for graph drawing.
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = "relative";
- this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
- this.svg.style.display = "block";
- frame.appendChild(this.svg);
+ var orientation = this.options['orientation'];
- // data axis
- this.options.dataAxis.orientation = 'left';
- this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
+ // calculate range and step (step such that we have space for 7 characters per label)
+ var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
- this.options.dataAxis.orientation = 'right';
- this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
- delete this.options.dataAxis.orientation;
+ var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation]);
+ this.step = step;
+ // get the distance in pixels for a step
+ // dead space is space that is "left over" after a step
+ var stepPixels = (this.dom.frame.offsetHeight - (step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange))) / (((step.marginRange - step.deadSpace) / step.step));
+ this.stepPixels = stepPixels;
- // legends
- this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
- this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
+ var amountOfSteps = this.height / stepPixels;
+ var stepDifference = 0;
- this.show();
- };
+ if (this.master == false) {
+ stepPixels = this.stepPixelsForced;
+ stepDifference = Math.round((this.dom.frame.offsetHeight / stepPixels) - amountOfSteps);
+ for (var i = 0; i < 0.5 * stepDifference; i++) {
+ step.previous();
+ }
+ amountOfSteps = this.height / stepPixels;
+ }
+ else {
+ amountOfSteps += 0.25;
+ }
- /**
- * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
- * @param options
- */
- LineGraph.prototype.setOptions = function(options) {
- if (options) {
- var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
- util.selectiveDeepExtend(fields, this.options, options);
- util.mergeOptions(this.options, options,'catmullRom');
- util.mergeOptions(this.options, options,'drawPoints');
- util.mergeOptions(this.options, options,'shaded');
- util.mergeOptions(this.options, options,'legend');
- if (options.catmullRom) {
- if (typeof options.catmullRom == 'object') {
- if (options.catmullRom.parametrization) {
- if (options.catmullRom.parametrization == 'uniform') {
- this.options.catmullRom.alpha = 0;
- }
- else if (options.catmullRom.parametrization == 'chordal') {
- this.options.catmullRom.alpha = 1.0;
- }
- else {
- this.options.catmullRom.parametrization = 'centripetal';
- this.options.catmullRom.alpha = 0.5;
- }
- }
- }
- }
+ this.valueAtZero = step.marginEnd;
+ var marginStartPos = 0;
- if (this.yAxisLeft) {
- if (options.dataAxis !== undefined) {
- this.yAxisLeft.setOptions(this.options.dataAxis);
- this.yAxisRight.setOptions(this.options.dataAxis);
- }
+ // do not draw the first label
+ var max = 1;
+
+ this.maxLabelSize = 0;
+ var y = 0;
+ while (max < Math.round(amountOfSteps)) {
+ step.next();
+ y = Math.round(max * stepPixels);
+ marginStartPos = max * stepPixels;
+ var isMajor = step.isMajor();
+
+ if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
}
- if (this.legendLeft) {
- if (options.legend !== undefined) {
- this.legendLeft.setOptions(this.options.legend);
- this.legendRight.setOptions(this.options.legend);
+ if (isMajor && this.options['showMajorLabels'] && this.master == true ||
+ this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
+ if (y >= 0) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
}
+ this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
}
-
- if (this.groups.hasOwnProperty(UNGROUPED)) {
- this.groups[UNGROUPED].setOptions(options);
+ else {
+ this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
}
+
+ max++;
}
- if (this.dom.frame) {
- this._updateGraph();
- }
- };
- /**
- * Hide the component from the DOM
- */
- LineGraph.prototype.hide = function() {
- // remove the frame containing the items
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
+ if (this.master == false) {
+ this.conversionFactor = y / (this.valueAtZero - step.current);
+ }
+ else {
+ this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange;
}
- };
- /**
- * Show the component in the DOM (when not already visible).
- * @return {Boolean} changed
- */
- LineGraph.prototype.show = function() {
- // show frame containing the items
- if (!this.dom.frame.parentNode) {
- this.body.dom.center.appendChild(this.dom.frame);
+ var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
+ // this will resize the yAxis to accomodate the labels.
+ if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
+ this.width = this.maxLabelSize + offset;
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ this.redraw();
+ return true;
+ }
+ // this will resize the yAxis if it is too big for the labels.
+ else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
+ this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ this.redraw();
+ return true;
+ }
+ else {
+ DOMutil.cleanupElements(this.DOMelements.lines);
+ DOMutil.cleanupElements(this.DOMelements.labels);
+ return false;
}
};
+ DataAxis.prototype.convertValue = function (value) {
+ var invertedValue = this.valueAtZero - value;
+ var convertedValue = invertedValue * this.conversionFactor;
+ return convertedValue;
+ };
/**
- * Set items
- * @param {vis.DataSet | null} items
+ * Create a label for the axis at position x
+ * @private
+ * @param y
+ * @param text
+ * @param orientation
+ * @param className
+ * @param characterHeight
*/
- LineGraph.prototype.setItems = function(items) {
- var me = this,
- ids,
- oldItemsData = this.itemsData;
-
- // replace the dataset
- if (!items) {
- this.itemsData = null;
- }
- else if (items instanceof DataSet || items instanceof DataView) {
- this.itemsData = items;
+ DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
+ // reuse redundant label
+ var label = DOMutil.getDOMElement('div',this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift();
+ label.className = className;
+ label.innerHTML = text;
+ if (orientation == 'left') {
+ label.style.left = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "right";
}
else {
- throw new TypeError('Data must be an instance of DataSet or DataView');
+ label.style.right = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "left";
}
- if (oldItemsData) {
- // unsubscribe from old dataset
- util.forEach(this.itemListeners, function (callback, event) {
- oldItemsData.off(event, callback);
- });
+ label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
- // remove all drawn items
- ids = oldItemsData.getIds();
- this._onRemove(ids);
+ text += '';
+
+ var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
+ if (this.maxLabelSize < text.length * largestWidth) {
+ this.maxLabelSize = text.length * largestWidth;
}
+ };
- if (this.itemsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.itemListeners, function (callback, event) {
- me.itemsData.on(event, callback, id);
- });
+ /**
+ * Create a minor line for the axis at position y
+ * @param y
+ * @param orientation
+ * @param className
+ * @param offset
+ * @param width
+ */
+ DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
+ if (this.master == true) {
+ var line = DOMutil.getDOMElement('div',this.DOMelements.lines, this.dom.lineContainer);//this.dom.redundant.lines.shift();
+ line.className = className;
+ line.innerHTML = '';
- // add all new items
- ids = this.itemsData.getIds();
- this._onAdd(ids);
+ if (orientation == 'left') {
+ line.style.left = (this.width - offset) + 'px';
+ }
+ else {
+ line.style.right = (this.width - offset) + 'px';
+ }
+
+ line.style.width = width + 'px';
+ line.style.top = y + 'px';
}
- this._updateUngrouped();
- this._updateGraph();
- this.redraw();
};
+
+
+
+
/**
- * Set groups
- * @param {vis.DataSet} groups
+ * 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
*/
- LineGraph.prototype.setGroups = function(groups) {
- var me = this,
- ids;
+ DataAxis.prototype._calculateCharSize = function () {
+ // determine the char width and height on the minor axis
+ if (!('minorCharHeight' in this.props)) {
+ var textMinor = document.createTextNode('0');
+ var measureCharMinor = document.createElement('DIV');
+ measureCharMinor.className = 'yAxis minor measure';
+ measureCharMinor.appendChild(textMinor);
+ this.dom.frame.appendChild(measureCharMinor);
- // unsubscribe from current dataset
- if (this.groupsData) {
- util.forEach(this.groupListeners, function (callback, event) {
- me.groupsData.unsubscribe(event, callback);
- });
+ this.props.minorCharHeight = measureCharMinor.clientHeight;
+ this.props.minorCharWidth = measureCharMinor.clientWidth;
- // remove all drawn groups
- ids = this.groupsData.getIds();
- this.groupsData = null;
- this._onRemoveGroups(ids); // note: this will cause a redraw
+ this.dom.frame.removeChild(measureCharMinor);
}
- // replace the dataset
- if (!groups) {
- this.groupsData = null;
- }
- else if (groups instanceof DataSet || groups instanceof DataView) {
- this.groupsData = groups;
- }
- else {
- throw new TypeError('Data must be an instance of DataSet or DataView');
- }
+ if (!('majorCharHeight' in this.props)) {
+ var textMajor = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'yAxis major measure';
+ measureCharMajor.appendChild(textMajor);
+ this.dom.frame.appendChild(measureCharMajor);
- if (this.groupsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.groupListeners, function (callback, event) {
- me.groupsData.on(event, callback, id);
- });
+ this.props.majorCharHeight = measureCharMajor.clientHeight;
+ this.props.majorCharWidth = measureCharMajor.clientWidth;
- // draw all ms
- ids = this.groupsData.getIds();
- this._onAddGroups(ids);
+ this.dom.frame.removeChild(measureCharMajor);
}
- this._onUpdate();
};
-
/**
- * Update the datapoints
- * @param [ids]
- * @private
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
*/
- LineGraph.prototype._onUpdate = function(ids) {
- this._updateUngrouped();
- this._updateAllGroupData();
- this._updateGraph();
- this.redraw();
+ DataAxis.prototype.snap = function(date) {
+ return this.step.snap(date);
};
- LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
- LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
- LineGraph.prototype._onUpdateGroups = function (groupIds) {
- for (var i = 0; i < groupIds.length; i++) {
- var group = this.groupsData.get(groupIds[i]);
- this._updateGroup(group, groupIds[i]);
- }
- this._updateGraph();
- this.redraw();
- };
- LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
+ module.exports = DataAxis;
- LineGraph.prototype._onRemoveGroups = function (groupIds) {
- for (var i = 0; i < groupIds.length; i++) {
- if (!this.groups.hasOwnProperty(groupIds[i])) {
- if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
- this.yAxisRight.removeGroup(groupIds[i]);
- this.legendRight.removeGroup(groupIds[i]);
- this.legendRight.redraw();
- }
- else {
- this.yAxisLeft.removeGroup(groupIds[i]);
- this.legendLeft.removeGroup(groupIds[i]);
- this.legendLeft.redraw();
- }
- delete this.groups[groupIds[i]];
- }
- }
- this._updateUngrouped();
- this._updateGraph();
- this.redraw();
- };
+
+/***/ },
+/* 36 */
+/***/ function(module, exports, __webpack_require__) {
/**
- * update a group object
+ * @constructor DataStep
+ * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
+ * end data point. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
*
- * @param group
- * @param groupId
- * @private
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ *
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
+ *
+ * Version: 1.2
+ *
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
- LineGraph.prototype._updateGroup = function (group, groupId) {
- if (!this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
- if (this.groups[groupId].options.yAxisOrientation == 'right') {
- this.yAxisRight.addGroup(groupId, this.groups[groupId]);
- this.legendRight.addGroup(groupId, this.groups[groupId]);
- }
- else {
- this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
- this.legendLeft.addGroup(groupId, this.groups[groupId]);
- }
- }
- else {
- this.groups[groupId].update(group);
- if (this.groups[groupId].options.yAxisOrientation == 'right') {
- this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
- this.legendRight.updateGroup(groupId, this.groups[groupId]);
- }
- else {
- this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
- this.legendLeft.updateGroup(groupId, this.groups[groupId]);
- }
+ function DataStep(start, end, minimumStep, containerHeight, customRange) {
+ // variables
+ this.current = 0;
+
+ this.autoScale = true;
+ this.stepIndex = 0;
+ this.step = 1;
+ this.scale = 1;
+
+ this.marginStart;
+ this.marginEnd;
+ this.deadSpace = 0;
+
+ this.majorSteps = [1, 2, 5, 10];
+ this.minorSteps = [0.25, 0.5, 1, 2];
+
+ this.setRange(start, end, minimumStep, containerHeight, customRange);
+ }
+
+
+
+ /**
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Number} [start] The start date and time.
+ * @param {Number} [end] The end date and time.
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ */
+ DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
+ this._start = customRange.min === undefined ? start : customRange.min;
+ this._end = customRange.max === undefined ? end : customRange.max;
+
+ if (this._start == this._end) {
+ this._start -= 0.75;
+ this._end += 1;
}
- this.legendLeft.redraw();
- this.legendRight.redraw();
- };
- LineGraph.prototype._updateAllGroupData = function () {
- if (this.itemsData != null) {
- var groupsContent = {};
- var groupId;
- for (groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- groupsContent[groupId] = [];
- }
- }
- for (var itemId in this.itemsData._data) {
- if (this.itemsData._data.hasOwnProperty(itemId)) {
- var item = this.itemsData._data[itemId];
- item.x = util.convert(item.x,"Date");
- groupsContent[item.group].push(item);
- }
- }
- for (groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- this.groups[groupId].setItems(groupsContent[groupId]);
- }
- }
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep, containerHeight);
}
+ this.setFirst(customRange);
};
/**
- * Create or delete the group holding all ungrouped items. This group is used when
- * there are no groups specified. This anonymous group is called 'graph'.
- * @protected
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
- LineGraph.prototype._updateUngrouped = function() {
- if (this.itemsData && this.itemsData != null) {
- var ungroupedCounter = 0;
- for (var itemId in this.itemsData._data) {
- if (this.itemsData._data.hasOwnProperty(itemId)) {
- var item = this.itemsData._data[itemId];
- if (item != undefined) {
- if (item.hasOwnProperty('group')) {
- if (item.group === undefined) {
- item.group = UNGROUPED;
- }
- }
- else {
- item.group = UNGROUPED;
- }
- ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
- }
+ DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
+ // round to floor
+ var size = this._end - this._start;
+ var safeSize = size * 1.2;
+ var minimumStepValue = minimumStep * (safeSize / containerHeight);
+ var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
+
+ var minorStepIdx = -1;
+ var magnitudefactor = Math.pow(10,orderOfMagnitude);
+
+ var start = 0;
+ if (orderOfMagnitude < 0) {
+ start = orderOfMagnitude;
+ }
+
+ var solutionFound = false;
+ for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
+ magnitudefactor = Math.pow(10,i);
+ for (var j = 0; j < this.minorSteps.length; j++) {
+ var stepSize = magnitudefactor * this.minorSteps[j];
+ if (stepSize >= minimumStepValue) {
+ solutionFound = true;
+ minorStepIdx = j;
+ break;
}
}
-
- if (ungroupedCounter == 0) {
- delete this.groups[UNGROUPED];
- this.legendLeft.removeGroup(UNGROUPED);
- this.legendRight.removeGroup(UNGROUPED);
- this.yAxisLeft.removeGroup(UNGROUPED);
- this.yAxisRight.removeGroup(UNGROUPED);
- }
- else {
- var group = {id: UNGROUPED, content: this.options.defaultGroup};
- this._updateGroup(group, UNGROUPED);
+ if (solutionFound == true) {
+ break;
}
}
- else {
- delete this.groups[UNGROUPED];
- this.legendLeft.removeGroup(UNGROUPED);
- this.legendRight.removeGroup(UNGROUPED);
- this.yAxisLeft.removeGroup(UNGROUPED);
- this.yAxisRight.removeGroup(UNGROUPED);
- }
-
- this.legendLeft.redraw();
- this.legendRight.redraw();
+ this.stepIndex = minorStepIdx;
+ this.scale = magnitudefactor;
+ this.step = magnitudefactor * this.minorSteps[minorStepIdx];
};
+
/**
- * Redraw the component, mandatory function
- * @return {boolean} Returns true if the component is resized
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
*/
- LineGraph.prototype.redraw = function() {
- var resized = false;
-
- this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
- if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
- resized = true;
+ DataStep.prototype.setFirst = function(customRange) {
+ if (customRange === undefined) {
+ customRange = {};
}
- // check if this component is resized
- resized = this._isResized() || resized;
- // check whether zoomed (in that case we need to re-stack everything)
- var visibleInterval = this.body.range.end - this.body.range.start;
- var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
- this.lastVisibleInterval = visibleInterval;
- this.lastWidth = this.width;
+ var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
+ var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
- // calculate actual size and position
- this.width = this.dom.frame.offsetWidth;
+ this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
+ this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
+ this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
+ this.marginRange = this.marginEnd - this.marginStart;
- // the svg element is three times as big as the width, this allows for fully dragging left and right
- // without reloading the graph. the controls for this are bound to events in the constructor
- if (resized == true) {
- this.svg.style.width = util.option.asSize(3*this.width);
- this.svg.style.left = util.option.asSize(-this.width);
- }
+ this.current = this.marginEnd;
- if (zoomed == true || this.abortedGraphUpdate == true) {
- this._updateGraph();
+ };
+
+ DataStep.prototype.roundToMinor = function(value) {
+ var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
+ if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
+ return rounded + (this.scale * this.minorSteps[this.stepIndex]);
}
else {
- // move the whole svg while dragging
- if (this.lastStart != 0) {
- var offset = this.body.range.start - this.lastStart;
- var range = this.body.range.end - this.body.range.start;
- if (this.width != 0) {
- var rangePerPixelInv = this.width/range;
- var xOffset = offset * rangePerPixelInv;
- this.svg.style.left = (-this.width - xOffset) + "px";
- }
- }
-
+ return rounded;
}
+ }
- this.legendLeft.redraw();
- this.legendRight.redraw();
- return resized;
+ /**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+ DataStep.prototype.hasNext = function () {
+ return (this.current >= this.marginStart);
};
/**
- * Update and redraw the graph.
- *
+ * Do the next step
*/
- LineGraph.prototype._updateGraph = function () {
- // reset the svg elements
- DOMutil.prepareElements(this.svgElements);
- if (this.width != 0 && this.itemsData != null) {
- var group, i;
- var preprocessedGroupData = {};
- var processedGroupData = {};
- var groupRanges = {};
- var changeCalled = false;
+ DataStep.prototype.next = function() {
+ var prev = this.current;
+ this.current -= this.step;
- // getting group Ids
- var groupIds = [];
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- group = this.groups[groupId];
- if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
- groupIds.push(groupId);
- }
- }
- }
- if (groupIds.length > 0) {
- // this is the range of the SVG canvas
- var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
- var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
- var groupsData = {};
- // fill groups data
- this._getRelevantData(groupIds, groupsData, minDate, maxDate);
- // we transform the X coordinates to detect collisions
- for (i = 0; i < groupIds.length; i++) {
- preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
- }
- // now all needed data has been collected we start the processing.
- this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current == prev) {
+ this.current = this._end;
+ }
+ };
- // update the Y axis first, we use this data to draw at the correct Y points
- // changeCalled is required to clean the SVG on a change emit.
- changeCalled = this._updateYAxis(groupIds, groupRanges);
- if (changeCalled == true) {
- DOMutil.cleanupElements(this.svgElements);
- this.abortedGraphUpdate = true;
- this.body.emitter.emit("change");
- return;
- }
- this.abortedGraphUpdate = false;
+ /**
+ * Do the next step
+ */
+ DataStep.prototype.previous = function() {
+ this.current += this.step;
+ this.marginEnd += this.step;
+ this.marginRange = this.marginEnd - this.marginStart;
+ };
- // With the yAxis scaled correctly, use this to get the Y values of the points.
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
- }
- // draw the groups
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- if (group.options.style == 'line') {
- this._drawLineGraph(processedGroupData[groupIds[i]], group);
- }
+ /**
+ * Get the current datetime
+ * @return {String} current The current date
+ */
+ DataStep.prototype.getCurrent = function() {
+ var toPrecision = '' + Number(this.current).toPrecision(5);
+ if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) {
+ for (var i = toPrecision.length-1; i > 0; i--) {
+ if (toPrecision[i] == "0") {
+ toPrecision = toPrecision.slice(0,i);
+ }
+ else if (toPrecision[i] == "." || toPrecision[i] == ",") {
+ toPrecision = toPrecision.slice(0,i);
+ break;
+ }
+ else{
+ break;
}
- this._drawBarGraphs(groupIds, processedGroupData);
}
}
- // cleanup unused svg elements
- DOMutil.cleanupElements(this.svgElements);
+ return toPrecision;
};
- LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
- // first select and preprocess the data from the datasets.
- // the groups have their preselection of data, we now loop over this data to see
- // what data we need to draw. Sorted data is much faster.
- // more optimization is possible by doing the sampling before and using the binary search
- // to find the end date to determine the increment.
- var group, i, j, item;
- if (groupIds.length > 0) {
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- groupsData[groupIds[i]] = [];
- var dataContainer = groupsData[groupIds[i]];
- // optimization for sorted data
- if (group.options.sort == true) {
- var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
- for (j = guess; j < group.itemsData.length; j++) {
- item = group.itemsData[j];
- if (item !== undefined) {
- if (item.x > maxDate) {
- dataContainer.push(item);
- break;
- }
- else {
- dataContainer.push(item);
- }
- }
- }
- }
- else {
- for (j = 0; j < group.itemsData.length; j++) {
- item = group.itemsData[j];
- if (item !== undefined) {
- if (item.x > minDate && item.x < maxDate) {
- dataContainer.push(item);
- }
- }
- }
- }
- }
+
+ /**
+ * Snap a date to a rounded value.
+ * The snap intervals are dependent on the current scale and step.
+ * @param {Date} date the date to be snapped.
+ * @return {Date} snappedDate
+ */
+ DataStep.prototype.snap = function(date) {
+
+ };
+
+ /**
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
+ */
+ DataStep.prototype.isMajor = function() {
+ return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
+ };
+
+ module.exports = DataStep;
+
+
+/***/ },
+/* 37 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+
+ /**
+ * @constructor Group
+ * @param {Number | String} groupId
+ * @param {Object} data
+ * @param {ItemSet} itemSet
+ */
+ function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
+ this.id = groupId;
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
+ this.options = util.selectiveBridgeObject(fields,options);
+ this.usingDefaultStyle = group.className === undefined;
+ this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
+ this.zeroPosition = 0;
+ this.update(group);
+ if (this.usingDefaultStyle == true) {
+ this.groupsUsingDefaultStyles[0] += 1;
}
+ this.itemsData = [];
+ this.visible = group.visible === undefined ? true : group.visible;
+ }
- this._applySampling(groupIds, groupsData);
+ GraphGroup.prototype.setItems = function(items) {
+ if (items != null) {
+ this.itemsData = items;
+ if (this.options.sort == true) {
+ this.itemsData.sort(function (a,b) {return a.x - b.x;})
+ }
+ }
+ else {
+ this.itemsData = [];
+ }
};
- LineGraph.prototype._applySampling = function (groupIds, groupsData) {
- var group;
- if (groupIds.length > 0) {
- for (var i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- if (group.options.sampling == true) {
- var dataContainer = groupsData[groupIds[i]];
- if (dataContainer.length > 0) {
- var increment = 1;
- var amountOfPoints = dataContainer.length;
+ GraphGroup.prototype.setZeroPosition = function(pos) {
+ this.zeroPosition = pos;
+ };
- // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
- // of width changing of the yAxis.
- var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
- var pointsPerPixel = amountOfPoints / xDistance;
- increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
+ GraphGroup.prototype.setOptions = function(options) {
+ if (options !== undefined) {
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
+ util.selectiveDeepExtend(fields, this.options, options);
- var sampledData = [];
- for (var j = 0; j < amountOfPoints; j += increment) {
- sampledData.push(dataContainer[j]);
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
}
- groupsData[groupIds[i]] = sampledData;
}
}
}
}
};
- LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
- var groupData, group, i,j;
- var barCombinedDataLeft = [];
- var barCombinedDataRight = [];
- var barCombinedData;
- if (groupIds.length > 0) {
- for (i = 0; i < groupIds.length; i++) {
- groupData = groupsData[groupIds[i]];
- if (groupData.length > 0) {
- group = this.groups[groupIds[i]];
- if (group.options.style == 'line' || group.options.barChart.handleOverlap != "stack") {
- var yMin = groupData[0].y;
- var yMax = groupData[0].y;
- for (j = 0; j < groupData.length; j++) {
- yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
- yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
- }
- groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation};
- }
- else if (group.options.style == 'bar') {
- if (group.options.yAxisOrientation == 'left') {
- barCombinedData = barCombinedDataLeft;
- }
- else {
- barCombinedData = barCombinedDataRight;
- }
+ GraphGroup.prototype.update = function(group) {
+ this.group = group;
+ this.content = group.content || 'graph';
+ this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
+ this.visible = group.visible === undefined ? true : group.visible;
+ this.setOptions(group.options);
+ };
- groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true};
+ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
+ var fillHeight = iconHeight * 0.5;
+ var path, fillPath;
- // combine data
- for (j = 0; j < groupData.length; j++) {
- barCombinedData.push({
- x: groupData[j].x,
- y: groupData[j].y,
- groupId: groupIds[i]
- });
- }
- }
+ var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
+ outline.setAttributeNS(null, "x", x);
+ outline.setAttributeNS(null, "y", y - fillHeight);
+ outline.setAttributeNS(null, "width", iconWidth);
+ outline.setAttributeNS(null, "height", 2*fillHeight);
+ outline.setAttributeNS(null, "class", "outline");
+
+ if (this.options.style == 'line') {
+ path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ path.setAttributeNS(null, "class", this.className);
+ path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
+ if (this.options.shaded.enabled == true) {
+ fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ if (this.options.shaded.orientation == 'top') {
+ fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
+ "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
+ }
+ else {
+ fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
+ "L"+x+"," + (y + fillHeight) + " " +
+ "L"+ (x + iconWidth) + "," + (y + fillHeight) +
+ "L"+ (x + iconWidth) + ","+y);
}
+ fillPath.setAttributeNS(null, "class", this.className + " iconFill");
}
- var intersections;
- if (barCombinedDataLeft.length > 0) {
- // sort by time and by group
- barCombinedDataLeft.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
- }
- });
- intersections = {};
- this._getDataIntersections(intersections, barCombinedDataLeft);
- groupRanges["__barchartLeft"] = this._getStackedBarYRange(intersections, barCombinedDataLeft);
- groupRanges["__barchartLeft"].yAxisOrientation = "left";
- groupIds.push("__barchartLeft");
- }
- if (barCombinedDataRight.length > 0) {
- // sort by time and by group
- barCombinedDataRight.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
- }
- });
- intersections = {};
- this._getDataIntersections(intersections, barCombinedDataRight);
- groupRanges["__barchartRight"] = this._getStackedBarYRange(intersections, barCombinedDataRight);
- groupRanges["__barchartRight"].yAxisOrientation = "right";
- groupIds.push("__barchartRight");
+ if (this.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
}
}
- };
+ else {
+ var barWidth = Math.round(0.3 * iconWidth);
+ var bar1Height = Math.round(0.4 * iconHeight);
+ var bar2Height = Math.round(0.75 * iconHeight);
- LineGraph.prototype._getStackedBarYRange = function (intersections, combinedData) {
- var key;
- var yMin = combinedData[0].y;
- var yMax = combinedData[0].y;
- for (var i = 0; i < combinedData.length; i++) {
- key = combinedData[i].x;
- if (intersections[key] === undefined) {
- yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
- yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
- }
- else {
- intersections[key].accumulated += combinedData[i].y;
- }
- }
- for (var xpos in intersections) {
- if (intersections.hasOwnProperty(xpos)) {
- yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
- yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
- }
- }
+ var offset = Math.round((iconWidth - (2 * barWidth))/3);
- return {min: yMin, max: yMax};
+ DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ }
};
-
/**
- * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
- * @param {Array} groupIds
- * @param {Object} groupRanges
- * @private
+ *
+ * @param iconWidth
+ * @param iconHeight
+ * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
*/
- LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
- var changeCalled = false;
- var yAxisLeftUsed = false;
- var yAxisRightUsed = false;
- var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
- // if groups are present
- if (groupIds.length > 0) {
- for (var i = 0; i < groupIds.length; i++) {
- if (groupRanges.hasOwnProperty(groupIds[i])) {
- if (groupRanges[groupIds[i]].ignore !== true) {
- minVal = groupRanges[groupIds[i]].min;
- maxVal = groupRanges[groupIds[i]].max;
+ GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
+ var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
+ return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
+ }
- if (groupRanges[groupIds[i]].yAxisOrientation == 'left') {
- yAxisLeftUsed = true;
- minLeft = minLeft > minVal ? minVal : minLeft;
- maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
- }
- else {
- yAxisRightUsed = true;
- minRight = minRight > minVal ? minVal : minRight;
- maxRight = maxRight < maxVal ? maxVal : maxRight;
- }
- }
- }
- }
+ module.exports = GraphGroup;
+
+
+/***/ },
+/* 38 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var util = __webpack_require__(1);
+ var DOMutil = __webpack_require__(6);
+ var Component = __webpack_require__(22);
- if (yAxisLeftUsed == true) {
- this.yAxisLeft.setRange(minLeft, maxLeft);
- }
- if (yAxisRightUsed == true) {
- this.yAxisRight.setRange(minRight, maxRight);
+ /**
+ * Legend for Graph2d
+ */
+ function Legend(body, options, side, linegraphOptions) {
+ this.body = body;
+ this.defaultOptions = {
+ enabled: true,
+ icons: true,
+ iconSize: 20,
+ iconSpacing: 6,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ },
+ right: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
}
}
+ this.side = side;
+ this.options = util.extend({},this.defaultOptions);
+ this.linegraphOptions = linegraphOptions;
- changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
- changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
+ this.svgElements = {};
+ this.dom = {};
+ this.groups = {};
+ this.amountOfGroups = 0;
+ this._create();
- if (yAxisRightUsed == true && yAxisLeftUsed == true) {
- this.yAxisLeft.drawIcons = true;
- this.yAxisRight.drawIcons = true;
- }
- else {
- this.yAxisLeft.drawIcons = false;
- this.yAxisRight.drawIcons = false;
- }
+ this.setOptions(options);
+ }
- this.yAxisRight.master = !yAxisLeftUsed;
+ Legend.prototype = new Component();
- if (this.yAxisRight.master == false) {
- if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
- else {this.yAxisLeft.lineOffset = 0;}
- changeCalled = this.yAxisLeft.redraw() || changeCalled;
- this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
- changeCalled = this.yAxisRight.redraw() || changeCalled;
- }
- else {
- changeCalled = this.yAxisRight.redraw() || changeCalled;
+ Legend.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
}
+ this.amountOfGroups += 1;
+ };
- // clean the accumulated lists
- if (groupIds.indexOf("__barchartLeft") != -1) {
- groupIds.splice(groupIds.indexOf("__barchartLeft"),1);
- }
- if (groupIds.indexOf("__barchartRight") != -1) {
- groupIds.splice(groupIds.indexOf("__barchartRight"),1);
+ Legend.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+ };
+
+ Legend.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
}
+ };
- return changeCalled;
+ Legend.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.className = 'legend';
+ this.dom.frame.style.position = "absolute";
+ this.dom.frame.style.top = "10px";
+ this.dom.frame.style.display = "block";
+
+ this.dom.textArea = document.createElement('div');
+ this.dom.textArea.className = 'legendText';
+ this.dom.textArea.style.position = "relative";
+ this.dom.textArea.style.top = "0px";
+
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = 'absolute';
+ this.svg.style.top = 0 +'px';
+ this.svg.style.width = this.options.iconSize + 5 + 'px';
+
+ this.dom.frame.appendChild(this.svg);
+ this.dom.frame.appendChild(this.dom.textArea);
};
/**
- * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
- *
- * @param {boolean} axisUsed
- * @returns {boolean}
- * @private
- * @param axis
+ * Hide the component from the DOM
*/
- LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
- var changed = false;
- if (axisUsed == false) {
- if (axis.dom.frame.parentNode) {
- axis.hide();
- changed = true;
+ Legend.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
+ };
+
+ /**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+ Legend.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
+ }
+ };
+
+ Legend.prototype.setOptions = function(options) {
+ var fields = ['enabled','orientation','icons','left','right'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ };
+
+ Legend.prototype.redraw = function() {
+ var activeGroups = 0;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ activeGroups++;
+ }
}
}
+
+ if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) {
+ this.hide();
+ }
else {
- if (!axis.dom.frame.parentNode) {
- axis.show();
- changed = true;
+ this.show();
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
+ this.dom.frame.style.left = '4px';
+ this.dom.frame.style.textAlign = "left";
+ this.dom.textArea.style.textAlign = "left";
+ this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.right = '';
+ this.svg.style.left = 0 +'px';
+ this.svg.style.right = '';
+ }
+ else {
+ this.dom.frame.style.right = '4px';
+ this.dom.frame.style.textAlign = "right";
+ this.dom.textArea.style.textAlign = "right";
+ this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.left = '';
+ this.svg.style.right = 0 +'px';
+ this.svg.style.left = '';
+ }
+
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
+ this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.bottom = '';
+ }
+ else {
+ this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.top = '';
+ }
+
+ if (this.options.icons == false) {
+ this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
+ this.dom.textArea.style.right = '';
+ this.dom.textArea.style.left = '';
+ this.svg.style.width = '0px';
+ }
+ else {
+ this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
+ this.drawLegendIcons();
+ }
+
+ var content = '';
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ content += this.groups[groupId].content + '
';
+ }
+ }
}
+ this.dom.textArea.innerHTML = content;
+ this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
}
- return changed;
};
+ Legend.prototype.drawLegendIcons = function() {
+ if (this.dom.frame.parentNode) {
+ DOMutil.prepareElements(this.svgElements);
+ var padding = window.getComputedStyle(this.dom.frame).paddingTop;
+ var iconOffset = Number(padding.replace('px',''));
+ var x = iconOffset;
+ var iconWidth = this.options.iconSize;
+ var iconHeight = 0.75 * this.options.iconSize;
+ var y = iconOffset + 0.5 * iconHeight + 3;
- /**
- * draw a bar graph
- *
- * @param groupIds
- * @param processedGroupData
- */
- LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) {
- var combinedData = [];
- var intersections = {};
- var coreDistance;
- var key, drawData;
- var group;
- var i,j;
- var barPoints = 0;
+ this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
- // combine all barchart data
- for (i = 0; i < groupIds.length; i++) {
- group = this.groups[groupIds[i]];
- if (group.options.style == 'bar') {
- if (group.visible == true && (this.options.groups.visibility[groupIds[i]] === undefined || this.options.groups.visibility[groupIds[i]] == true)) {
- for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
- combinedData.push({
- x: processedGroupData[groupIds[i]][j].x,
- y: processedGroupData[groupIds[i]][j].y,
- groupId: groupIds[i]
- });
- barPoints += 1;
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + this.options.iconSpacing;
}
}
}
+
+ DOMutil.cleanupElements(this.svgElements);
}
+ };
+
+ module.exports = Legend;
+
+
+/***/ },
+/* 39 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // Utility functions for ordering and stacking of items
+ var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors
+
+ /**
+ * Order items by their start data
+ * @param {Item[]} items
+ */
+ exports.orderByStart = function(items) {
+ items.sort(function (a, b) {
+ return a.data.start - b.data.start;
+ });
+ };
- if (barPoints == 0) {return;}
+ /**
+ * Order items by their end date. If they have no end date, their start date
+ * is used.
+ * @param {Item[]} items
+ */
+ exports.orderByEnd = function(items) {
+ items.sort(function (a, b) {
+ var aTime = ('end' in a.data) ? a.data.end : a.data.start,
+ bTime = ('end' in b.data) ? b.data.end : b.data.start;
- // sort by time and by group
- combinedData.sort(function (a, b) {
- if (a.x == b.x) {
- return a.groupId - b.groupId;
- } else {
- return a.x - b.x;
- }
+ return aTime - bTime;
});
+ };
- // get intersections
- this._getDataIntersections(intersections, combinedData);
-
- // plot barchart
- for (i = 0; i < combinedData.length; i++) {
- group = this.groups[combinedData[i].groupId];
- var minWidth = 0.1 * group.options.barChart.width;
+ /**
+ * Adjust vertical positions of the items such that they don't overlap each
+ * other.
+ * @param {Item[]} items
+ * All visible items
+ * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
+ * Margins between items and between items and the axis.
+ * @param {boolean} [force=false]
+ * If true, all items will be repositioned. If false (default), only
+ * items having a top===null will be re-stacked
+ */
+ exports.stack = function(items, margin, force) {
+ var i, iMax;
- key = combinedData[i].x;
- var heightOffset = 0;
- if (intersections[key] === undefined) {
- if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
- if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
- drawData = this._getSafeDrawData(coreDistance, group, minWidth);
+ if (force) {
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ items[i].top = null;
}
- else {
- var nextKey = i + (intersections[key].amount - intersections[key].resolved);
- var prevKey = i - (intersections[key].resolved + 1);
- if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
- if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
- drawData = this._getSafeDrawData(coreDistance, group, minWidth);
- intersections[key].resolved += 1;
+ }
- if (group.options.barChart.handleOverlap == 'stack') {
- heightOffset = intersections[key].accumulated;
- intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
- }
- else if (group.options.barChart.handleOverlap == 'sideBySide') {
- drawData.width = drawData.width / intersections[key].amount;
- drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
- if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
- else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
- }
- }
- DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg);
- // draw points
- if (group.options.drawPoints.enabled == true) {
- DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg);
+ // calculate new, non-overlapping positions
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ var item = items[i];
+ if (item.top === null) {
+ // initialize top position
+ item.top = margin.axis;
+
+ do {
+ // TODO: optimize checking for overlap. when there is a gap without items,
+ // you only need to check for items from the next item on, not from zero
+ var collidingItem = null;
+ for (var j = 0, jj = items.length; j < jj; j++) {
+ var other = items[j];
+ if (other.top !== null && other !== item && other.ignoreStacking == false && exports.collision(item, other, margin.item)) {
+ collidingItem = other;
+ break;
+ }
+ }
+
+ if (collidingItem != null) {
+ // There is a collision. Reposition the items above the colliding element
+ item.top = collidingItem.top + collidingItem.height + margin.item.vertical;
+ }
+ } while (collidingItem);
}
}
};
+
/**
- * Fill the intersections object with counters of how many datapoints share the same x coordinates
- * @param intersections
- * @param combinedData
- * @private
+ * Adjust vertical positions of the items without stacking them
+ * @param {Item[]} items
+ * All visible items
+ * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
+ * Margins between items and between items and the axis.
*/
- LineGraph.prototype._getDataIntersections = function (intersections, combinedData) {
- // get intersections
- var coreDistance;
- for (var i = 0; i < combinedData.length; i++) {
- if (i + 1 < combinedData.length) {
- coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
- }
- if (i > 0) {
- coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
- }
- if (coreDistance == 0) {
- if (intersections[combinedData[i].x] === undefined) {
- intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
+ exports.nostack = function(items, margin, subgroups) {
+ var i, iMax, newTop;
+
+ // reset top position of all items
+ for (i = 0, iMax = items.length; i < iMax; i++) {
+ if (items[i].data.subgroup !== undefined) {
+ newTop = margin.axis;
+ for (var subgroupIdx in subgroups) {
+ if (subgroups.hasOwnProperty(subgroupIdx)) {
+ if (subgroups[subgroupIdx].visible == true && subgroupIdx < items[i].data.subgroup) {
+ newTop += subgroups[subgroupIdx].height + margin.item.vertical;
+ }
+ }
}
- intersections[combinedData[i].x].amount += 1;
+ items[i].top = newTop;
+ }
+ else {
+ items[i].top = margin.axis;
}
}
};
/**
- * Get the width and offset for bargraphs based on the coredistance between datapoints
- *
- * @param coreDistance
- * @param group
- * @param minWidth
- * @returns {{width: Number, offset: Number}}
- * @private
+ * Test if the two provided items collide
+ * The items must have parameters left, width, top, and height.
+ * @param {Item} a The first item
+ * @param {Item} b The second item
+ * @param {{horizontal: number, vertical: number}} margin
+ * An object containing a horizontal and vertical
+ * minimum required margin.
+ * @return {boolean} true if a and b collide, else false
*/
- LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) {
- var width, offset;
- if (coreDistance < group.options.barChart.width && coreDistance > 0) {
- width = coreDistance < minWidth ? minWidth : coreDistance;
+ exports.collision = function(a, b, margin) {
+ return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) &&
+ (a.left + a.width + margin.horizontal - EPSILON) > b.left &&
+ (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
+ (a.top + a.height + margin.vertical - EPSILON) > b.top);
+ };
- offset = 0; // recalculate offset with the new width;
- if (group.options.barChart.align == 'left') {
- offset -= 0.5 * coreDistance;
- }
- else if (group.options.barChart.align == 'right') {
- offset += 0.5 * coreDistance;
- }
- }
- else {
- // default settings
- width = group.options.barChart.width;
- offset = 0;
- if (group.options.barChart.align == 'left') {
- offset -= 0.5 * group.options.barChart.width;
- }
- else if (group.options.barChart.align == 'right') {
- offset += 0.5 * group.options.barChart.width;
- }
- }
- return {width: width, offset: offset};
- };
+/***/ },
+/* 40 */
+/***/ function(module, exports, __webpack_require__) {
+ var Hammer = __webpack_require__(18);
+ var util = __webpack_require__(1);
/**
- * draw a line graph
- *
- * @param dataset
- * @param group
+ * @constructor Item
+ * @param {Object} data Object containing (optional) parameters type,
+ * start, end, content, group, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} options Configuration options
+ * // TODO: describe available options
*/
- LineGraph.prototype._drawLineGraph = function (dataset, group) {
- if (dataset != null) {
- if (dataset.length > 0) {
- var path, d;
- var svgHeight = Number(this.svg.style.height.replace("px",""));
- path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
- path.setAttributeNS(null, "class", group.className);
+ function Item (data, conversion, options) {
+ this.id = null;
+ this.parent = null;
+ this.data = data;
+ this.dom = null;
+ this.conversion = conversion || {};
+ this.options = options || {};
- // construct path from dataset
- if (group.options.catmullRom.enabled == true) {
- d = this._catmullRom(dataset, group);
- }
- else {
- d = this._linear(dataset);
- }
+ this.selected = false;
+ this.displayed = false;
+ this.dirty = true;
- // append with points for fill and finalize the path
- if (group.options.shaded.enabled == true) {
- var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
- var dFill;
- if (group.options.shaded.orientation == 'top') {
- dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
- }
- else {
- dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
- }
- fillPath.setAttributeNS(null, "class", group.className + " fill");
- fillPath.setAttributeNS(null, "d", dFill);
- }
- // copy properties to path for drawing.
- path.setAttributeNS(null, "d", "M" + d);
+ this.top = null;
+ this.left = null;
+ this.width = null;
+ this.height = null;
- // draw points
- if (group.options.drawPoints.enabled == true) {
- this._drawPoints(dataset, group, this.svgElements, this.svg);
- }
- }
- }
+ this.ignoreStacking = false;
+ }
+
+ /**
+ * Select current item
+ */
+ Item.prototype.select = function() {
+ this.selected = true;
+ this.dirty = true;
+ if (this.displayed) this.redraw();
+ };
+
+ /**
+ * Unselect current item
+ */
+ Item.prototype.unselect = function() {
+ this.selected = false;
+ this.dirty = true;
+ if (this.displayed) this.redraw();
};
/**
- * draw the data points
- *
- * @param {Array} dataset
- * @param {Object} JSONcontainer
- * @param {Object} svg | SVG DOM element
- * @param {GraphGroup} group
- * @param {Number} [offset]
+ * Set data for the item. Existing data will be updated. The id should not
+ * be changed. When the item is displayed, it will be redrawn immediately.
+ * @param {Object} data
*/
- LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
- if (offset === undefined) {offset = 0;}
- for (var i = 0; i < dataset.length; i++) {
- DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
- }
+ Item.prototype.setData = function(data) {
+ this.data = data;
+ this.dirty = true;
+ if (this.displayed) this.redraw();
};
-
-
/**
- * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
- * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
- * the yAxis.
- *
- * @param datapoints
- * @returns {Array}
- * @private
+ * Set a parent for the item
+ * @param {ItemSet | Group} parent
*/
- LineGraph.prototype._convertXcoordinates = function (datapoints) {
- var extractedData = [];
- var xValue, yValue;
- var toScreen = this.body.util.toScreen;
-
- for (var i = 0; i < datapoints.length; i++) {
- xValue = toScreen(datapoints[i].x) + this.width;
- yValue = datapoints[i].y;
- extractedData.push({x: xValue, y: yValue});
+ Item.prototype.setParent = function(parent) {
+ if (this.displayed) {
+ this.hide();
+ this.parent = parent;
+ if (this.parent) {
+ this.show();
+ }
+ }
+ else {
+ this.parent = parent;
}
-
- return extractedData;
};
-
-
/**
- * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
- * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
- * the yAxis.
- *
- * @param datapoints
- * @returns {Array}
- * @private
+ * 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
*/
- LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
- var extractedData = [];
- var xValue, yValue;
- var toScreen = this.body.util.toScreen;
- var axis = this.yAxisLeft;
- var svgHeight = Number(this.svg.style.height.replace("px",""));
- if (group.options.yAxisOrientation == 'right') {
- axis = this.yAxisRight;
- }
+ Item.prototype.isVisible = function(range) {
+ // Should be implemented by Item implementations
+ return false;
+ };
- for (var i = 0; i < datapoints.length; i++) {
- xValue = toScreen(datapoints[i].x) + this.width;
- yValue = Math.round(axis.convertValue(datapoints[i].y));
- extractedData.push({x: xValue, y: yValue});
- }
+ /**
+ * Show the Item in the DOM (when not already visible)
+ * @return {Boolean} changed
+ */
+ Item.prototype.show = function() {
+ return false;
+ };
- group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
+ /**
+ * Hide the Item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ Item.prototype.hide = function() {
+ return false;
+ };
- return extractedData;
+ /**
+ * Repaint the item
+ */
+ Item.prototype.redraw = function() {
+ // should be implemented by the item
};
/**
- * This uses an uniform parametrization of the CatmullRom algorithm:
- * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
- * @param data
- * @returns {string}
- * @private
+ * Reposition the Item horizontally
*/
- LineGraph.prototype._catmullRomUniform = function(data) {
- // catmull rom
- var p0, p1, p2, p3, bp1, bp2;
- var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
- var normalization = 1/6;
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
+ Item.prototype.repositionX = function() {
+ // should be implemented by the item
+ };
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
+ /**
+ * Reposition the Item vertically
+ */
+ Item.prototype.repositionY = function() {
+ // should be implemented by the item
+ };
+ /**
+ * Repaint a delete button on the top right of the item when the item is selected
+ * @param {HTMLElement} anchor
+ * @protected
+ */
+ Item.prototype._repaintDeleteButton = function (anchor) {
+ if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
+ // create and show button
+ var me = this;
- // Catmull-Rom to Cubic Bezier conversion matrix
- // 0 1 0 0
- // -1/6 1 1/6 0
- // 0 1/6 1 -1/6
- // 0 0 1 0
+ var deleteButton = document.createElement('div');
+ deleteButton.className = 'delete';
+ deleteButton.title = 'Delete this item';
- // bp0 = { x: p1.x, y: p1.y };
- bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
- bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
- // bp0 = { x: p2.x, y: p2.y };
+ Hammer(deleteButton, {
+ preventDefault: true
+ }).on('tap', function (event) {
+ me.parent.removeFromDataSet(me);
+ event.stopPropagation();
+ });
- d += "C" +
- bp1.x + "," +
- bp1.y + " " +
- bp2.x + "," +
- bp2.y + " " +
- p2.x + "," +
- p2.y + " ";
+ 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;
}
-
- return d;
};
/**
- * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
- * By default, the centripetal parameterization is used because this gives the nicest results.
- * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
- *
- * One optimization can be used to reuse distances since this is a sliding window approach.
- * @param data
- * @returns {string}
+ * Set HTML contents for the item
+ * @param {Element} element HTML element to fill with the contents
* @private
*/
- LineGraph.prototype._catmullRom = function(data, group) {
- var alpha = group.options.catmullRom.alpha;
- if (alpha == 0 || alpha === undefined) {
- return this._catmullRomUniform(data);
+ Item.prototype._updateContents = function (element) {
+ var content;
+ if (this.options.template) {
+ var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset
+ content = this.options.template(itemData);
}
else {
- var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
- var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
- var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
- var length = data.length;
- for (var i = 0; i < length - 1; i++) {
-
- p0 = (i == 0) ? data[0] : data[i-1];
- p1 = data[i];
- p2 = data[i+1];
- p3 = (i + 2 < length) ? data[i+2] : p2;
-
- d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
- d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
- d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
-
- // Catmull-Rom to Cubic Bezier conversion matrix
- //
- // A = 2d1^2a + 3d1^a * d2^a + d3^2a
- // B = 2d3^2a + 3d3^a * d2^a + d2^2a
- //
- // [ 0 1 0 0 ]
- // [ -d2^2a/N A/N d1^2a/N 0 ]
- // [ 0 d3^2a/M B/M -d2^2a/M ]
- // [ 0 0 1 0 ]
-
- // [ 0 1 0 0 ]
- // [ -d2pow2a/N A/N d1pow2a/N 0 ]
- // [ 0 d3pow2a/M B/M -d2pow2a/M ]
- // [ 0 0 1 0 ]
-
- d3powA = Math.pow(d3, alpha);
- d3pow2A = Math.pow(d3,2*alpha);
- d2powA = Math.pow(d2, alpha);
- d2pow2A = Math.pow(d2,2*alpha);
- d1powA = Math.pow(d1, alpha);
- d1pow2A = Math.pow(d1,2*alpha);
-
- A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
- B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
- N = 3*d1powA * (d1powA + d2powA);
- if (N > 0) {N = 1 / N;}
- M = 3*d3powA * (d3powA + d2powA);
- if (M > 0) {M = 1 / M;}
-
- bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
- y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
-
- bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
- y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
-
- if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
- if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
- d += "C" +
- bp1.x + "," +
- bp1.y + " " +
- bp2.x + "," +
- bp2.y + " " +
- p2.x + "," +
- p2.y + " ";
+ content = this.data.content;
+ }
+
+ if(content !== this.content) {
+ // only replace the content when changed
+ if (content instanceof Element) {
+ element.innerHTML = '';
+ element.appendChild(content);
+ }
+ else if (content != undefined) {
+ element.innerHTML = content;
+ }
+ else {
+ if (!(this.data.type == 'background' && this.data.content === undefined)) {
+ throw new Error('Property "content" missing in item ' + this.id);
+ }
}
- return d;
+ this.content = content;
}
};
/**
- * this generates the SVG path for a linear drawing between datapoints.
- * @param data
- * @returns {string}
+ * Set HTML contents for the item
+ * @param {Element} element HTML element to fill with the contents
* @private
*/
- LineGraph.prototype._linear = function(data) {
- // linear
- var d = "";
- for (var i = 0; i < data.length; i++) {
- if (i == 0) {
- d += data[i].x + "," + data[i].y;
+ Item.prototype._updateTitle = function (element) {
+ if (this.data.title != null) {
+ element.title = this.data.title || '';
+ }
+ else {
+ element.removeAttribute('title');
+ }
+ };
+
+ /**
+ * Process dataAttributes timeline option and set as data- attributes on dom.content
+ * @param {Element} element HTML element to which the attributes will be attached
+ * @private
+ */
+ Item.prototype._updateDataAttributes = function(element) {
+ if (this.options.dataAttributes && this.options.dataAttributes.length > 0) {
+ var attributes = [];
+
+ if (Array.isArray(this.options.dataAttributes)) {
+ attributes = this.options.dataAttributes;
+ }
+ else if (this.options.dataAttributes == 'all') {
+ attributes = Object.keys(this.data);
}
else {
- d += " " + data[i].x + "," + data[i].y;
+ return;
+ }
+
+ for (var i = 0; i < attributes.length; i++) {
+ var name = attributes[i];
+ var value = this.data[name];
+
+ if (value != null) {
+ element.setAttribute('data-' + name, value);
+ }
+ else {
+ element.removeAttribute('data-' + name);
+ }
}
}
- return d;
};
- module.exports = LineGraph;
+ /**
+ * Update custom styles of the element
+ * @param element
+ * @private
+ */
+ Item.prototype._updateStyle = function(element) {
+ // remove old styles
+ if (this.style) {
+ util.removeCssText(element, this.style);
+ this.style = null;
+ }
+
+ // append new styles
+ if (this.data.style) {
+ util.addCssText(element, this.data.style);
+ this.style = this.data.style;
+ }
+ };
+
+ module.exports = Item;
/***/ },
/* 41 */
/***/ function(module, exports, __webpack_require__) {
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var Component = __webpack_require__(22);
- var DataStep = __webpack_require__(42);
+ var Hammer = __webpack_require__(18);
+ var Item = __webpack_require__(40);
+ var RangeItem = __webpack_require__(42);
/**
- * A horizontal time axis
- * @param {Object} [options] See DataAxis.setOptions for the available
- * options.
- * @constructor DataAxis
- * @extends Component
- * @param body
+ * @constructor BackgroundItem
+ * @extends Item
+ * @param {Object} data Object containing parameters start, end
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe options
*/
- function DataAxis (body, options, svg, linegraphOptions) {
- this.id = util.randomUUID();
- this.body = body;
-
- this.defaultOptions = {
- orientation: 'left', // supported: 'left', 'right'
- showMinorLabels: true,
- showMajorLabels: true,
- icons: true,
- majorLinesOffset: 7,
- minorLinesOffset: 4,
- labelOffsetX: 10,
- labelOffsetY: 2,
- iconWidth: 20,
- width: '40px',
- visible: true,
- customRange: {
- left: {min:undefined, max:undefined},
- right: {min:undefined, max:undefined}
+ // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation
+ function BackgroundItem (data, conversion, options) {
+ this.props = {
+ content: {
+ width: 0
}
};
+ this.overflow = false; // if contents can overflow (css styling), this flag is set to true
- this.linegraphOptions = linegraphOptions;
- this.linegraphSVG = svg;
- this.props = {};
- this.DOMelements = { // dynamic elements
- lines: {},
- labels: {}
- };
-
- this.dom = {};
-
- this.range = {start:0, end: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);
+ }
+ }
- this.options = util.extend({}, this.defaultOptions);
- this.conversionFactor = 1;
+ Item.call(this, data, conversion, options);
- this.setOptions(options);
- this.width = Number(('' + this.options.width).replace("px",""));
- this.minWidth = this.width;
- this.height = this.linegraphSVG.offsetHeight;
+ this.ignoreStacking = true; // this is not used when stacking
+ this.emptyContent = false;
+ }
- this.stepPixels = 25;
- this.stepPixelsForced = 25;
- this.lineOffset = 0;
- this.master = true;
- this.svgElements = {};
+ BackgroundItem.prototype = new Item (null, null, null);
+ BackgroundItem.prototype.baseClassName = 'item background';
- this.groups = {};
- this.amountOfGroups = 0;
+ /**
+ * 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
+ */
+ BackgroundItem.prototype.isVisible = function(range) {
+ // determine visibility
+ return (this.data.start < range.end) && (this.data.end > range.start);
+ };
- // create the HTML DOM
- this._create();
- }
+ /**
+ * Repaint the item
+ */
+ BackgroundItem.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
- DataAxis.prototype = new Component();
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in redraw()
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
- DataAxis.prototype.addGroup = function(label, graphOptions) {
- if (!this.groups.hasOwnProperty(label)) {
- this.groups[label] = graphOptions;
+ this.dirty = true;
}
- this.amountOfGroups += 1;
- };
-
- DataAxis.prototype.updateGroup = function(label, graphOptions) {
- this.groups[label] = graphOptions;
- };
- DataAxis.prototype.removeGroup = function(label) {
- if (this.groups.hasOwnProperty(label)) {
- delete this.groups[label];
- this.amountOfGroups -= 1;
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
}
- };
+ if (!dom.box.parentNode) {
+ var background = this.parent.dom.background;
+ if (!background) {
+ throw new Error('Cannot redraw item: parent has no background container element');
+ }
+ background.appendChild(dom.box);
+ }
+ this.displayed = true;
+ // Update DOM when item is marked dirty. An item is marked dirty when:
+ // - the item is not yet rendered
+ // - the item's data is changed
+ // - the item is selected/deselected
+ if (this.dirty) {
+ this._updateContents(this.dom.content);
+ this._updateTitle(this.dom.content);
+ this._updateDataAttributes(this.dom.content);
+ this._updateStyle(this.dom.box);
- DataAxis.prototype.setOptions = function (options) {
- if (options) {
- var redraw = false;
- if (this.options.orientation != options.orientation && options.orientation !== undefined) {
- redraw = true;
- }
- var fields = [
- 'orientation',
- 'showMinorLabels',
- 'showMajorLabels',
- 'icons',
- 'majorLinesOffset',
- 'minorLinesOffset',
- 'labelOffsetX',
- 'labelOffsetY',
- 'iconWidth',
- 'width',
- 'visible',
- 'customRange'
- ];
- util.selectiveExtend(fields, this.options, options);
+ // update class
+ var className = (this.data.className ? (' ' + this.data.className) : '') +
+ (this.selected ? ' selected' : '');
+ dom.box.className = this.baseClassName + className;
- this.minWidth = Number(('' + this.options.width).replace("px",""));
+ // determine from css whether this box has overflow
+ this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
- if (redraw == true && this.dom.frame) {
- this.hide();
- this.show();
- }
+ // recalculate size
+ this.props.content.width = this.dom.content.offsetWidth;
+ this.height = 0; // set height zero, so this item will be ignored when stacking items
+
+ this.dirty = false;
}
};
-
/**
- * Create the HTML DOM for the DataAxis
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
*/
- DataAxis.prototype._create = function() {
- this.dom.frame = document.createElement('div');
- this.dom.frame.style.width = this.options.width;
- this.dom.frame.style.height = this.height;
+ BackgroundItem.prototype.show = RangeItem.prototype.show;
- this.dom.lineContainer = document.createElement('div');
- this.dom.lineContainer.style.width = '100%';
- this.dom.lineContainer.style.height = this.height;
+ /**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ BackgroundItem.prototype.hide = RangeItem.prototype.hide;
- // create svg element for graph drawing.
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = "absolute";
- this.svg.style.top = '0px';
- this.svg.style.height = '100%';
- this.svg.style.width = '100%';
- this.svg.style.display = "block";
- this.dom.frame.appendChild(this.svg);
- };
+ /**
+ * Reposition the item horizontally
+ * @Override
+ */
+ BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX;
- DataAxis.prototype._redrawGroupIcons = function () {
- DOMutil.prepareElements(this.svgElements);
+ /**
+ * Reposition the item vertically
+ * @Override
+ */
+ BackgroundItem.prototype.repositionY = function(margin) {
+ var onTop = this.options.orientation === 'top';
+ this.dom.content.style.top = onTop ? '' : '0';
+ this.dom.content.style.bottom = onTop ? '0' : '';
+ var height;
- var x;
- var iconWidth = this.options.iconWidth;
- var iconHeight = 15;
- var iconOffset = 4;
- var y = iconOffset + 0.5 * iconHeight;
+ // special positioning for subgroups
+ if (this.data.subgroup !== undefined) {
+ var subgroup = this.data.subgroup;
+ var subgroups = this.parent.subgroups;
+ // if the orientation is top, we need to take the difference in height into account.
+ if (onTop == true) {
+ // the first subgroup will have to account for the distance from the top to the first item.
+ height = this.parent.subgroups[subgroup].height + margin.item.vertical;
+ height += subgroup == 0 ? margin.axis - 0.5*margin.item.vertical : 0;
+ var newTop = this.parent.top;
+ for (var subgroupIdx in subgroups) {
+ if (subgroups.hasOwnProperty(subgroupIdx)) {
+ if (subgroups[subgroupIdx].visible == true && subgroupIdx < subgroup) {
+ newTop += subgroups[subgroupIdx].height + margin.item.vertical;
+ }
+ }
+ }
- if (this.options.orientation == 'left') {
- x = iconOffset;
+ // the others will have to be offset downwards with this same distance.
+ newTop += subgroup != 0 ? margin.axis - 0.5 * margin.item.vertical : 0;
+ this.dom.box.style.top = newTop + 'px';
+ this.dom.box.style.bottom = '';
+ }
+ // and when the orientation is bottom:
+ else {
+ var newTop = this.parent.top;
+ for (var subgroupIdx in subgroups) {
+ if (subgroups.hasOwnProperty(subgroupIdx)) {
+ if (subgroups[subgroupIdx].visible == true && subgroupIdx > subgroup) {
+ newTop += subgroups[subgroupIdx].height + margin.item.vertical;
+ }
+ }
+ }
+ height = this.parent.subgroups[subgroup].height + margin.item.vertical;
+ this.dom.box.style.top = newTop + 'px';
+ this.dom.box.style.bottom = '';
+ }
}
+ // and in the case of no subgroups:
else {
- x = this.width - iconWidth - iconOffset;
- }
-
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
- y += iconHeight + iconOffset;
- }
+ // we want backgrounds with groups to only show in groups.
+ if (this.data.group !== undefined) {
+ height = this.parent.height;
+ // same alignment for items when orientation is top or bottom
+ this.dom.box.style.top = this.parent.top + 'px';
+ this.dom.box.style.bottom = '';
+ }
+ else {
+ // if the item is not in a group:
+ height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.centerContainer.height);
+ this.dom.box.style.top = onTop ? '0' : '';
+ this.dom.box.style.bottom = onTop ? '' : '0';
}
}
-
- DOMutil.cleanupElements(this.svgElements);
+ this.dom.box.style.height = height + 'px';
};
+ module.exports = BackgroundItem;
+
+
+/***/ },
+/* 42 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Hammer = __webpack_require__(18);
+ var Item = __webpack_require__(40);
+
/**
- * Create the HTML DOM for the DataAxis
+ * @constructor RangeItem
+ * @extends Item
+ * @param {Object} data Object containing parameters start, end
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe options
*/
- DataAxis.prototype.show = function() {
- if (!this.dom.frame.parentNode) {
- if (this.options.orientation == 'left') {
- this.body.dom.left.appendChild(this.dom.frame);
+ function RangeItem (data, conversion, options) {
+ this.props = {
+ content: {
+ width: 0
}
- else {
- this.body.dom.right.appendChild(this.dom.frame);
+ };
+ this.overflow = false; // if contents can overflow (css styling), this flag is set to true
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data.id);
+ }
+ if (data.end == undefined) {
+ throw new Error('Property "end" missing in item ' + data.id);
}
}
- if (!this.dom.lineContainer.parentNode) {
- this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
- }
- };
+ Item.call(this, data, conversion, options);
+ }
- /**
- * Create the HTML DOM for the DataAxis
- */
- DataAxis.prototype.hide = function() {
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
- }
+ RangeItem.prototype = new Item (null, null, null);
- if (this.dom.lineContainer.parentNode) {
- this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
- }
- };
+ RangeItem.prototype.baseClassName = 'item range';
/**
- * Set a range (start and end)
- * @param end
- * @param start
- * @param end
+ * 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
*/
- DataAxis.prototype.setRange = function (start, end) {
- this.range.start = start;
- this.range.end = end;
+ RangeItem.prototype.isVisible = function(range) {
+ // determine visibility
+ return (this.data.start < range.end) && (this.data.end > range.start);
};
/**
- * Repaint the component
- * @return {boolean} Returns true if the component is resized
+ * Repaint the item
*/
- DataAxis.prototype.redraw = function () {
- var changeCalled = false;
- var activeGroups = 0;
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- activeGroups++;
- }
- }
+ RangeItem.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
+
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in redraw()
+
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
+
+ this.dirty = true;
}
- if (this.amountOfGroups == 0 || activeGroups == 0) {
- this.hide();
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
}
- else {
- this.show();
- this.height = Number(this.linegraphSVG.style.height.replace("px",""));
- // svg offsetheight did not work in firefox and explorer...
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) {
+ throw new Error('Cannot redraw item: parent has no foreground container element');
+ }
+ foreground.appendChild(dom.box);
+ }
+ this.displayed = true;
- this.dom.lineContainer.style.height = this.height + 'px';
- this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
+ // Update DOM when item is marked dirty. An item is marked dirty when:
+ // - the item is not yet rendered
+ // - the item's data is changed
+ // - the item is selected/deselected
+ if (this.dirty) {
+ this._updateContents(this.dom.content);
+ this._updateTitle(this.dom.box);
+ this._updateDataAttributes(this.dom.box);
+ this._updateStyle(this.dom.box);
- var props = this.props;
- var frame = this.dom.frame;
+ // update class
+ var className = (this.data.className ? (' ' + this.data.className) : '') +
+ (this.selected ? ' selected' : '');
+ dom.box.className = this.baseClassName + className;
- // update classname
- frame.className = 'dataaxis';
+ // determine from css whether this box has overflow
+ this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
+
+ // recalculate size
+ this.props.content.width = this.dom.content.offsetWidth;
+ this.height = this.dom.box.offsetHeight;
- // calculate character width and height
- this._calculateCharSize();
+ this.dirty = false;
+ }
- var orientation = this.options.orientation;
- var showMinorLabels = this.options.showMinorLabels;
- var showMajorLabels = this.options.showMajorLabels;
+ this._repaintDeleteButton(dom.box);
+ this._repaintDragLeft();
+ this._repaintDragRight();
+ };
- // determine the width and height of the elemens for the axis
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+ /**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ */
+ RangeItem.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
+ }
+ };
- props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
- props.minorLineHeight = 1;
- props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
- props.majorLineHeight = 1;
+ /**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ RangeItem.prototype.hide = function() {
+ if (this.displayed) {
+ var box = this.dom.box;
- // take frame offline while updating (is almost twice as fast)
- if (orientation == 'left') {
- frame.style.top = '0';
- frame.style.left = '0';
- frame.style.bottom = '';
- frame.style.width = this.width + 'px';
- frame.style.height = this.height + "px";
- }
- else { // right
- frame.style.top = '';
- frame.style.bottom = '0';
- frame.style.left = '0';
- frame.style.width = this.width + 'px';
- frame.style.height = this.height + "px";
- }
- changeCalled = this._redrawLabels();
- if (this.options.icons == true) {
- this._redrawGroupIcons();
+ if (box.parentNode) {
+ box.parentNode.removeChild(box);
}
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
}
- return changeCalled;
};
/**
- * Repaint major and minor text labels and vertical grid lines
- * @private
+ * Reposition the item horizontally
+ * @Override
*/
- DataAxis.prototype._redrawLabels = function () {
- DOMutil.prepareElements(this.DOMelements.lines);
- DOMutil.prepareElements(this.DOMelements.labels);
-
- var orientation = this.options['orientation'];
-
- // calculate range and step (step such that we have space for 7 characters per label)
- var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
+ RangeItem.prototype.repositionX = function() {
+ var parentWidth = this.parent.width;
+ var start = this.conversion.toScreen(this.data.start);
+ var end = this.conversion.toScreen(this.data.end);
+ var contentLeft;
+ var contentWidth;
- var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation]);
- this.step = step;
- // get the distance in pixels for a step
- // dead space is space that is "left over" after a step
- var stepPixels = (this.dom.frame.offsetHeight - (step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange))) / (((step.marginRange - step.deadSpace) / step.step));
- this.stepPixels = stepPixels;
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
+ }
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
+ }
+ var boxWidth = Math.max(end - start, 1);
- var amountOfSteps = this.height / stepPixels;
- var stepDifference = 0;
+ if (this.overflow) {
+ this.left = start;
+ this.width = boxWidth + this.props.content.width;
+ contentWidth = this.props.content.width;
- if (this.master == false) {
- stepPixels = this.stepPixelsForced;
- stepDifference = Math.round((this.dom.frame.offsetHeight / stepPixels) - amountOfSteps);
- for (var i = 0; i < 0.5 * stepDifference; i++) {
- step.previous();
- }
- amountOfSteps = this.height / stepPixels;
+ // Note: The calculation of width is an optimistic calculation, giving
+ // a width which will not change when moving the Timeline
+ // So no re-stacking needed, which is nicer for the eye;
}
else {
- amountOfSteps += 0.25;
+ this.left = start;
+ this.width = boxWidth;
+ contentWidth = Math.min(end - start, this.props.content.width);
}
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = boxWidth + 'px';
- this.valueAtZero = step.marginEnd;
- var marginStartPos = 0;
-
- // do not draw the first label
- var max = 1;
+ switch (this.options.align) {
+ case 'left':
+ this.dom.content.style.left = '0';
+ break;
- this.maxLabelSize = 0;
- var y = 0;
- while (max < Math.round(amountOfSteps)) {
- step.next();
- y = Math.round(max * stepPixels);
- marginStartPos = max * stepPixels;
- var isMajor = step.isMajor();
+ case 'right':
+ this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding), 0) + 'px';
+ break;
- if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
- this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
- }
+ case 'center':
+ this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + 'px';
+ break;
- if (isMajor && this.options['showMajorLabels'] && this.master == true ||
- this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
- if (y >= 0) {
- this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
+ default: // 'auto'
+ if (this.overflow) {
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ contentLeft = Math.max(-start, 0);
}
- this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
- }
- else {
- this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
- }
-
- max++;
+ else {
+ // 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 - this.props.content.width - 2 * this.options.padding));
+ // TODO: remove the need for options.padding. it's terrible.
+ }
+ else {
+ contentLeft = 0;
+ }
+ }
+ this.dom.content.style.left = contentLeft + 'px';
}
+ };
- if (this.master == false) {
- this.conversionFactor = y / (this.valueAtZero - step.current);
- }
- else {
- this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange;
- }
+ /**
+ * Reposition the item vertically
+ * @Override
+ */
+ RangeItem.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ box = this.dom.box;
- var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
- // this will resize the yAxis to accomodate the labels.
- if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
- this.width = this.maxLabelSize + offset;
- this.options.width = this.width + "px";
- DOMutil.cleanupElements(this.DOMelements.lines);
- DOMutil.cleanupElements(this.DOMelements.labels);
- this.redraw();
- return true;
- }
- // this will resize the yAxis if it is too big for the labels.
- else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
- this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
- this.options.width = this.width + "px";
- DOMutil.cleanupElements(this.DOMelements.lines);
- DOMutil.cleanupElements(this.DOMelements.labels);
- this.redraw();
- return true;
+ if (orientation == 'top') {
+ box.style.top = this.top + 'px';
}
else {
- DOMutil.cleanupElements(this.DOMelements.lines);
- DOMutil.cleanupElements(this.DOMelements.labels);
- return false;
+ box.style.top = (this.parent.height - this.top - this.height) + 'px';
}
};
- DataAxis.prototype.convertValue = function (value) {
- var invertedValue = this.valueAtZero - value;
- var convertedValue = invertedValue * this.conversionFactor;
- return convertedValue;
- };
-
/**
- * Create a label for the axis at position x
- * @private
- * @param y
- * @param text
- * @param orientation
- * @param className
- * @param characterHeight
+ * Repaint a drag area on the left side of the range when the range is selected
+ * @protected
*/
- DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
- // reuse redundant label
- var label = DOMutil.getDOMElement('div',this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift();
- label.className = className;
- label.innerHTML = text;
- if (orientation == 'left') {
- label.style.left = '-' + this.options.labelOffsetX + 'px';
- label.style.textAlign = "right";
- }
- else {
- label.style.right = '-' + this.options.labelOffsetX + 'px';
- label.style.textAlign = "left";
- }
-
- label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
+ RangeItem.prototype._repaintDragLeft = function () {
+ if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
+ // create and show drag area
+ var dragLeft = document.createElement('div');
+ dragLeft.className = 'drag-left';
+ dragLeft.dragLeftItem = this;
- text += '';
+ // TODO: this should be redundant?
+ Hammer(dragLeft, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag left')
+ });
- var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
- if (this.maxLabelSize < text.length * largestWidth) {
- this.maxLabelSize = text.length * largestWidth;
+ 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;
}
};
/**
- * Create a minor line for the axis at position y
- * @param y
- * @param orientation
- * @param className
- * @param offset
- * @param width
+ * Repaint a drag area on the right side of the range when the range is selected
+ * @protected
*/
- DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
- if (this.master == true) {
- var line = DOMutil.getDOMElement('div',this.DOMelements.lines, this.dom.lineContainer);//this.dom.redundant.lines.shift();
- line.className = className;
- line.innerHTML = '';
+ RangeItem.prototype._repaintDragRight = function () {
+ if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
+ // create and show drag area
+ var dragRight = document.createElement('div');
+ dragRight.className = 'drag-right';
+ dragRight.dragRightItem = this;
- if (orientation == 'left') {
- line.style.left = (this.width - offset) + 'px';
- }
- else {
- line.style.right = (this.width - offset) + 'px';
- }
+ // TODO: this should be redundant?
+ Hammer(dragRight, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag right')
+ });
- line.style.width = width + 'px';
- line.style.top = y + 'px';
+ 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;
}
};
+ module.exports = RangeItem;
+/***/ },
+/* 43 */
+/***/ function(module, exports, __webpack_require__) {
+ var Item = __webpack_require__(40);
+ var util = __webpack_require__(1);
/**
- * 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
+ * @constructor BoxItem
+ * @extends Item
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe available options
*/
- DataAxis.prototype._calculateCharSize = function () {
- // determine the char width and height on the minor axis
- if (!('minorCharHeight' in this.props)) {
- var textMinor = document.createTextNode('0');
- var measureCharMinor = document.createElement('DIV');
- measureCharMinor.className = 'yAxis minor measure';
- measureCharMinor.appendChild(textMinor);
- this.dom.frame.appendChild(measureCharMinor);
-
- this.props.minorCharHeight = measureCharMinor.clientHeight;
- this.props.minorCharWidth = measureCharMinor.clientWidth;
+ function BoxItem (data, conversion, options) {
+ this.props = {
+ dot: {
+ width: 0,
+ height: 0
+ },
+ line: {
+ width: 0,
+ height: 0
+ }
+ };
- this.dom.frame.removeChild(measureCharMinor);
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
}
- if (!('majorCharHeight' in this.props)) {
- var textMajor = document.createTextNode('0');
- var measureCharMajor = document.createElement('DIV');
- measureCharMajor.className = 'yAxis major measure';
- measureCharMajor.appendChild(textMajor);
- this.dom.frame.appendChild(measureCharMajor);
+ Item.call(this, data, conversion, options);
+ }
- this.props.majorCharHeight = measureCharMajor.clientHeight;
- this.props.majorCharWidth = measureCharMajor.clientWidth;
+ BoxItem.prototype = new Item (null, null, null);
- this.dom.frame.removeChild(measureCharMajor);
- }
+ /**
+ * 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
+ */
+ BoxItem.prototype.isVisible = function(range) {
+ // determine visibility
+ // TODO: account for the real width of the item. Right now we just add 1/4 to the window
+ var interval = (range.end - range.start) / 4;
+ return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
};
/**
- * 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
+ * Repaint the item
*/
- DataAxis.prototype.snap = function(date) {
- return this.step.snap(date);
- };
+ BoxItem.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
- module.exports = DataAxis;
+ // 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);
-/***/ },
-/* 42 */
-/***/ function(module, exports, __webpack_require__) {
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
- /**
- * @constructor DataStep
- * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
- * end data point. The class itself determines the best scale (step size) based on the
- * provided start Date, end Date, and minimumStep.
- *
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- *
- * Alternatively, you can set a scale by hand.
- * After creation, you can initialize the class by executing first(). Then you
- * can iterate from the start date to the end date via next(). You can check if
- * the end date is reached with the function hasNext(). After each step, you can
- * retrieve the current date via getCurrent().
- * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
- * days, to years.
- *
- * Version: 1.2
- *
- * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
- * or new Date(2010, 9, 21, 23, 45, 00)
- * @param {Date} [end] The end date
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
- */
- function DataStep(start, end, minimumStep, containerHeight, customRange) {
- // variables
- this.current = 0;
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
- this.autoScale = true;
- this.stepIndex = 0;
- this.step = 1;
- this.scale = 1;
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
- this.marginStart;
- this.marginEnd;
- this.deadSpace = 0;
+ this.dirty = true;
+ }
- this.majorSteps = [1, 2, 5, 10];
- this.minorSteps = [0.25, 0.5, 1, 2];
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) throw new Error('Cannot redraw item: parent has no foreground container element');
+ foreground.appendChild(dom.box);
+ }
+ if (!dom.line.parentNode) {
+ var background = this.parent.dom.background;
+ if (!background) throw new Error('Cannot redraw item: parent has no background container element');
+ background.appendChild(dom.line);
+ }
+ if (!dom.dot.parentNode) {
+ var axis = this.parent.dom.axis;
+ if (!background) throw new Error('Cannot redraw item: parent has no axis container element');
+ axis.appendChild(dom.dot);
+ }
+ this.displayed = true;
- this.setRange(start, end, minimumStep, containerHeight, customRange);
- }
+ // Update DOM when item is marked dirty. An item is marked dirty when:
+ // - the item is not yet rendered
+ // - the item's data is changed
+ // - the item is selected/deselected
+ if (this.dirty) {
+ this._updateContents(this.dom.content);
+ this._updateTitle(this.dom.box);
+ this._updateDataAttributes(this.dom.box);
+ this._updateStyle(this.dom.box);
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ dom.box.className = 'item box' + className;
+ dom.line.className = 'item line' + className;
+ dom.dot.className = 'item dot' + className;
+
+ // recalculate size
+ 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.
+ */
+ BoxItem.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
+ }
+ };
+
+ /**
+ * Hide the item from the DOM (when visible)
+ */
+ BoxItem.prototype.hide = function() {
+ 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;
+ }
+ };
/**
- * Set a new range
- * If minimumStep is provided, the step size is chosen as close as possible
- * to the minimumStep but larger than minimumStep. If minimumStep is not
- * provided, the scale is set to 1 DAY.
- * The minimumStep should correspond with the onscreen size of about 6 characters
- * @param {Number} [start] The start date and time.
- * @param {Number} [end] The end date and time.
- * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
+ * Reposition the item horizontally
+ * @Override
*/
- DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, customRange) {
- this._start = customRange.min === undefined ? start : customRange.min;
- this._end = customRange.max === undefined ? end : customRange.max;
+ BoxItem.prototype.repositionX = function() {
+ var start = this.conversion.toScreen(this.data.start);
+ var align = this.options.align;
+ var left;
+ var box = this.dom.box;
+ var line = this.dom.line;
+ var dot = this.dom.dot;
- if (this._start == this._end) {
- this._start -= 0.75;
- this._end += 1;
+ // calculate left position of the box
+ if (align == 'right') {
+ this.left = start - this.width;
}
-
- if (this.autoScale) {
- this.setMinimumStep(minimumStep, containerHeight);
+ else if (align == 'left') {
+ this.left = start;
}
- this.setFirst(customRange);
+ 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';
};
/**
- * Automatically determine the scale that bests fits the provided minimum step
- * @param {Number} [minimumStep] The minimum step size in milliseconds
+ * Reposition the item vertically
+ * @Override
*/
- DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
- // round to floor
- var size = this._end - this._start;
- var safeSize = size * 1.2;
- var minimumStepValue = minimumStep * (safeSize / containerHeight);
- var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
+ BoxItem.prototype.repositionY = function() {
+ var orientation = this.options.orientation;
+ var box = this.dom.box;
+ var line = this.dom.line;
+ var dot = this.dom.dot;
- var minorStepIdx = -1;
- var magnitudefactor = Math.pow(10,orderOfMagnitude);
+ if (orientation == 'top') {
+ box.style.top = (this.top || 0) + 'px';
- var start = 0;
- if (orderOfMagnitude < 0) {
- start = orderOfMagnitude;
+ line.style.top = '0';
+ line.style.height = (this.parent.top + this.top + 1) + 'px';
+ line.style.bottom = '';
}
+ else { // orientation 'bottom'
+ var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
+ var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
- var solutionFound = false;
- for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
- magnitudefactor = Math.pow(10,i);
- for (var j = 0; j < this.minorSteps.length; j++) {
- var stepSize = magnitudefactor * this.minorSteps[j];
- if (stepSize >= minimumStepValue) {
- solutionFound = true;
- minorStepIdx = j;
- break;
- }
- }
- if (solutionFound == true) {
- break;
- }
+ box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
+ line.style.top = (itemSetHeight - lineHeight) + 'px';
+ line.style.bottom = '0';
}
- this.stepIndex = minorStepIdx;
- this.scale = magnitudefactor;
- this.step = magnitudefactor * this.minorSteps[minorStepIdx];
- };
+ dot.style.top = (-this.props.dot.height / 2) + 'px';
+ };
+ module.exports = BoxItem;
- /**
- * Round the current date to the first minor date value
- * This must be executed once when the current date is set to start Date
- */
- DataStep.prototype.setFirst = function(customRange) {
- if (customRange === undefined) {
- customRange = {};
- }
- var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min;
- var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max;
- this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max;
- this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min;
- this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart;
- this.marginRange = this.marginEnd - this.marginStart;
+/***/ },
+/* 44 */
+/***/ function(module, exports, __webpack_require__) {
- this.current = this.marginEnd;
+ var Item = __webpack_require__(40);
- };
+ /**
+ * @constructor PointItem
+ * @extends Item
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe available options
+ */
+ function PointItem (data, conversion, options) {
+ this.props = {
+ dot: {
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ content: {
+ height: 0,
+ marginLeft: 0
+ }
+ };
- DataStep.prototype.roundToMinor = function(value) {
- var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
- if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
- return rounded + (this.scale * this.minorSteps[this.stepIndex]);
- }
- else {
- return rounded;
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
}
+
+ Item.call(this, data, conversion, options);
}
+ PointItem.prototype = new Item (null, null, null);
/**
- * Check if the there is a next step
- * @return {boolean} true if the current date has not passed the end date
+ * 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
*/
- DataStep.prototype.hasNext = function () {
- return (this.current >= this.marginStart);
+ PointItem.prototype.isVisible = function(range) {
+ // determine visibility
+ // TODO: account for the real width of the item. Right now we just add 1/4 to the window
+ var interval = (range.end - range.start) / 4;
+ return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
};
/**
- * Do the next step
+ * Repaint the item
*/
- DataStep.prototype.next = function() {
- var prev = this.current;
- this.current -= this.step;
+ PointItem.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
- // safety mechanism: if current time is still unchanged, move to the end
- if (this.current == prev) {
- this.current = this._end;
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in redraw()
+
+ // contents box, right from the dot
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.point.appendChild(dom.content);
+
+ // dot at start
+ dom.dot = document.createElement('div');
+ dom.point.appendChild(dom.dot);
+
+ // attach this item as attribute
+ dom.point['timeline-item'] = this;
+
+ this.dirty = true;
+ }
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
+ }
+ if (!dom.point.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) {
+ throw new Error('Cannot redraw item: parent has no foreground container element');
+ }
+ foreground.appendChild(dom.point);
+ }
+ this.displayed = true;
+
+ // Update DOM when item is marked dirty. An item is marked dirty when:
+ // - the item is not yet rendered
+ // - the item's data is changed
+ // - the item is selected/deselected
+ if (this.dirty) {
+ this._updateContents(this.dom.content);
+ this._updateTitle(this.dom.point);
+ this._updateDataAttributes(this.dom.point);
+ this._updateStyle(this.dom.point);
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ dom.point.className = 'item point' + className;
+ dom.dot.className = 'item dot' + className;
+
+ // recalculate size
+ this.width = dom.point.offsetWidth;
+ this.height = dom.point.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.content.height = dom.content.offsetHeight;
+
+ // resize contents
+ dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
+ //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
+
+ dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
+ dom.dot.style.left = (this.props.dot.width / 2) + 'px';
+
+ this.dirty = false;
}
+
+ this._repaintDeleteButton(dom.point);
};
/**
- * Do the next step
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
*/
- DataStep.prototype.previous = function() {
- this.current += this.step;
- this.marginEnd += this.step;
- this.marginRange = this.marginEnd - this.marginStart;
+ PointItem.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
+ }
};
-
-
/**
- * Get the current datetime
- * @return {String} current The current date
+ * Hide the item from the DOM (when visible)
*/
- DataStep.prototype.getCurrent = function() {
- var toPrecision = '' + Number(this.current).toPrecision(5);
- if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) {
- for (var i = toPrecision.length-1; i > 0; i--) {
- if (toPrecision[i] == "0") {
- toPrecision = toPrecision.slice(0,i);
- }
- else if (toPrecision[i] == "." || toPrecision[i] == ",") {
- toPrecision = toPrecision.slice(0,i);
- break;
- }
- else{
- break;
- }
+ PointItem.prototype.hide = function() {
+ if (this.displayed) {
+ if (this.dom.point.parentNode) {
+ this.dom.point.parentNode.removeChild(this.dom.point);
}
- }
-
- return toPrecision;
- };
+ this.top = null;
+ this.left = null;
+ this.displayed = false;
+ }
+ };
/**
- * Snap a date to a rounded value.
- * The snap intervals are dependent on the current scale and step.
- * @param {Date} date the date to be snapped.
- * @return {Date} snappedDate
+ * Reposition the item horizontally
+ * @Override
*/
- DataStep.prototype.snap = function(date) {
+ PointItem.prototype.repositionX = function() {
+ var start = this.conversion.toScreen(this.data.start);
+
+ this.left = start - this.props.dot.width;
+ // reposition point
+ this.dom.point.style.left = this.left + 'px';
};
/**
- * 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.
+ * Reposition the item vertically
+ * @Override
*/
- DataStep.prototype.isMajor = function() {
- return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
+ PointItem.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ point = this.dom.point;
+
+ if (orientation == 'top') {
+ point.style.top = this.top + 'px';
+ }
+ else {
+ point.style.top = (this.parent.height - this.top - this.height) + 'px';
+ }
};
- module.exports = DataStep;
+ module.exports = PointItem;
/***/ },
-/* 43 */
+/* 45 */
/***/ function(module, exports, __webpack_require__) {
var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
+ var stack = __webpack_require__(39);
+ var RangeItem = __webpack_require__(42);
+ var DateUtil = __webpack_require__(23);
/**
* @constructor Group
@@ -21510,456 +21563,460 @@ return /******/ (function(modules) { // webpackBootstrap
* @param {Object} data
* @param {ItemSet} itemSet
*/
- function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
- this.id = groupId;
- var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
- this.options = util.selectiveBridgeObject(fields,options);
- this.usingDefaultStyle = group.className === undefined;
- this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
- this.zeroPosition = 0;
- this.update(group);
- if (this.usingDefaultStyle == true) {
- this.groupsUsingDefaultStyles[0] += 1;
- }
- this.itemsData = [];
- this.visible = group.visible === undefined ? true : group.visible;
- }
+ function Group (groupId, data, itemSet) {
+ this.groupId = groupId;
+ this.subgroups = {};
+ this.visibleSubgroups = 0;
+ this.itemSet = itemSet;
- GraphGroup.prototype.setItems = function(items) {
- if (items != null) {
- this.itemsData = items;
- if (this.options.sort == true) {
- this.itemsData.sort(function (a,b) {return a.x - b.x;})
+ this.dom = {};
+ this.props = {
+ label: {
+ width: 0,
+ height: 0
}
- }
- else {
- this.itemsData = [];
- }
- };
+ };
+ this.className = null;
- GraphGroup.prototype.setZeroPosition = function(pos) {
- this.zeroPosition = pos;
- };
+ this.items = {}; // items filtered by groupId of this group
+ this.visibleItems = []; // items currently visible in window
+ this.orderedItems = { // items sorted by start and by end
+ byStart: [],
+ byEnd: []
+ };
- GraphGroup.prototype.setOptions = function(options) {
- if (options !== undefined) {
- var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
- util.selectiveDeepExtend(fields, this.options, options);
+ this._create();
- util.mergeOptions(this.options, options,'catmullRom');
- util.mergeOptions(this.options, options,'drawPoints');
- util.mergeOptions(this.options, options,'shaded');
+ this.setData(data);
+ }
- if (options.catmullRom) {
- if (typeof options.catmullRom == 'object') {
- if (options.catmullRom.parametrization) {
- if (options.catmullRom.parametrization == 'uniform') {
- this.options.catmullRom.alpha = 0;
- }
- else if (options.catmullRom.parametrization == 'chordal') {
- this.options.catmullRom.alpha = 1.0;
- }
- else {
- this.options.catmullRom.parametrization = 'centripetal';
- this.options.catmullRom.alpha = 0.5;
- }
- }
- }
- }
- }
- };
+ /**
+ * Create DOM elements for the group
+ * @private
+ */
+ Group.prototype._create = function() {
+ var label = document.createElement('div');
+ label.className = 'vlabel';
+ this.dom.label = label;
- GraphGroup.prototype.update = function(group) {
- this.group = group;
- this.content = group.content || 'graph';
- this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
- this.visible = group.visible === undefined ? true : group.visible;
- this.setOptions(group.options);
- };
+ var inner = document.createElement('div');
+ inner.className = 'inner';
+ label.appendChild(inner);
+ this.dom.inner = inner;
- GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
- var fillHeight = iconHeight * 0.5;
- var path, fillPath;
+ var foreground = document.createElement('div');
+ foreground.className = 'group';
+ foreground['timeline-group'] = this;
+ this.dom.foreground = foreground;
- var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
- outline.setAttributeNS(null, "x", x);
- outline.setAttributeNS(null, "y", y - fillHeight);
- outline.setAttributeNS(null, "width", iconWidth);
- outline.setAttributeNS(null, "height", 2*fillHeight);
- outline.setAttributeNS(null, "class", "outline");
+ this.dom.background = document.createElement('div');
+ this.dom.background.className = 'group';
- if (this.options.style == 'line') {
- path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
- path.setAttributeNS(null, "class", this.className);
- path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
- if (this.options.shaded.enabled == true) {
- fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
- if (this.options.shaded.orientation == 'top') {
- fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
- "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
- }
- else {
- fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
- "L"+x+"," + (y + fillHeight) + " " +
- "L"+ (x + iconWidth) + "," + (y + fillHeight) +
- "L"+ (x + iconWidth) + ","+y);
- }
- fillPath.setAttributeNS(null, "class", this.className + " iconFill");
- }
+ this.dom.axis = document.createElement('div');
+ this.dom.axis.className = 'group';
- if (this.options.drawPoints.enabled == true) {
- DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
- }
+ // create a hidden marker to detect when the Timelines container is attached
+ // to the DOM, or the style of a parent of the Timeline is changed from
+ // display:none is changed to visible.
+ this.dom.marker = document.createElement('div');
+ this.dom.marker.style.visibility = 'hidden'; // TODO: ask jos why this is not none?
+ this.dom.marker.innerHTML = '?';
+ this.dom.background.appendChild(this.dom.marker);
+ };
+
+ /**
+ * Set the group data for this group
+ * @param {Object} data Group data, can contain properties content and className
+ */
+ Group.prototype.setData = function(data) {
+ // update contents
+ var content = data && data.content;
+ if (content instanceof Element) {
+ this.dom.inner.appendChild(content);
+ }
+ else if (content !== undefined && content !== null) {
+ this.dom.inner.innerHTML = content;
}
else {
- var barWidth = Math.round(0.3 * iconWidth);
- var bar1Height = Math.round(0.4 * iconHeight);
- var bar2Height = Math.round(0.75 * iconHeight);
+ this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null
+ }
- var offset = Math.round((iconWidth - (2 * barWidth))/3);
+ // update title
+ this.dom.label.title = data && data.title || '';
- DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
- DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ if (!this.dom.inner.firstChild) {
+ util.addClassName(this.dom.inner, 'hidden');
+ }
+ else {
+ util.removeClassName(this.dom.inner, 'hidden');
+ }
+
+ // update className
+ var className = data && data.className || null;
+ if (className != this.className) {
+ if (this.className) {
+ util.removeClassName(this.dom.label, this.className);
+ util.removeClassName(this.dom.foreground, this.className);
+ util.removeClassName(this.dom.background, this.className);
+ util.removeClassName(this.dom.axis, this.className);
+ }
+ util.addClassName(this.dom.label, className);
+ util.addClassName(this.dom.foreground, className);
+ util.addClassName(this.dom.background, className);
+ util.addClassName(this.dom.axis, className);
+ this.className = className;
+ }
+
+ // update style
+ if (this.style) {
+ util.removeCssText(this.dom.label, this.style);
+ this.style = null;
+ }
+ if (data && data.style) {
+ util.addCssText(this.dom.label, data.style);
+ this.style = data.style;
}
};
/**
- *
- * @param iconWidth
- * @param iconHeight
- * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}}
+ * Get the width of the group label
+ * @return {number} width
*/
- GraphGroup.prototype.getLegend = function(iconWidth, iconHeight) {
- var svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.drawIcon(0,0.5*iconHeight,[],svg,iconWidth,iconHeight);
- return {icon: svg, label: this.content, orientation:this.options.yAxisOrientation};
- }
-
- module.exports = GraphGroup;
-
-
-/***/ },
-/* 44 */
-/***/ function(module, exports, __webpack_require__) {
+ Group.prototype.getLabelWidth = function() {
+ return this.props.label.width;
+ };
- var util = __webpack_require__(1);
- var DOMutil = __webpack_require__(6);
- var Component = __webpack_require__(22);
/**
- * Legend for Graph2d
+ * Repaint this group
+ * @param {{start: number, end: number}} range
+ * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
+ * @param {boolean} [restack=false] Force restacking of all items
+ * @return {boolean} Returns true if the group is resized
*/
- function Legend(body, options, side, linegraphOptions) {
- this.body = body;
- this.defaultOptions = {
- enabled: true,
- icons: true,
- iconSize: 20,
- iconSpacing: 6,
- left: {
- visible: true,
- position: 'top-left' // top/bottom - left,center,right
- },
- right: {
- visible: true,
- position: 'top-left' // top/bottom - left,center,right
- }
- }
- this.side = side;
- this.options = util.extend({},this.defaultOptions);
- this.linegraphOptions = linegraphOptions;
-
- this.svgElements = {};
- this.dom = {};
- this.groups = {};
- this.amountOfGroups = 0;
- this._create();
+ Group.prototype.redraw = function(range, margin, restack) {
+ var resized = false;
- this.setOptions(options);
- }
+ this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
- Legend.prototype = new Component();
+ // force recalculation of the height of the items when the marker height changed
+ // (due to the Timeline being attached to the DOM or changed from display:none to visible)
+ var markerHeight = this.dom.marker.clientHeight;
+ if (markerHeight != this.lastMarkerHeight) {
+ this.lastMarkerHeight = markerHeight;
+ util.forEach(this.items, function (item) {
+ item.dirty = true;
+ if (item.displayed) item.redraw();
+ });
- Legend.prototype.addGroup = function(label, graphOptions) {
- if (!this.groups.hasOwnProperty(label)) {
- this.groups[label] = graphOptions;
+ restack = true;
}
- this.amountOfGroups += 1;
- };
- Legend.prototype.updateGroup = function(label, graphOptions) {
- this.groups[label] = graphOptions;
- };
+ // reposition visible items vertically
+ if (this.itemSet.options.stack) { // TODO: ugly way to access options...
+ stack.stack(this.visibleItems, margin, restack);
+ }
+ else { // no stacking
+ stack.nostack(this.visibleItems, margin, this.subgroups);
+ }
- Legend.prototype.removeGroup = function(label) {
- if (this.groups.hasOwnProperty(label)) {
- delete this.groups[label];
- this.amountOfGroups -= 1;
+ // recalculate the height of the group
+ var height;
+ var visibleItems = this.visibleItems;
+ //var visibleSubgroups = [];
+ //this.visibleSubgroups = 0;
+ this.resetSubgroups();
+ var me = this;
+ if (visibleItems.length) {
+ var min = visibleItems[0].top;
+ var max = visibleItems[0].top + visibleItems[0].height;
+ util.forEach(visibleItems, function (item) {
+ min = Math.min(min, item.top);
+ max = Math.max(max, (item.top + item.height));
+ if (item.data.subgroup !== undefined) {
+ me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height,item.height);
+ me.subgroups[item.data.subgroup].visible = true;
+ //if (visibleSubgroups.indexOf(item.data.subgroup) == -1){
+ // visibleSubgroups.push(item.data.subgroup);
+ // me.visibleSubgroups += 1;
+ //}
+ }
+ });
+ if (min > margin.axis) {
+ // there is an empty gap between the lowest item and the axis
+ var offset = min - margin.axis;
+ max -= offset;
+ util.forEach(visibleItems, function (item) {
+ item.top -= offset;
+ });
+ }
+ height = max + margin.item.vertical / 2;
}
- };
+ else {
+ height = margin.axis + margin.item.vertical;
+ }
+ height = Math.max(height, this.props.label.height);
- Legend.prototype._create = function() {
- this.dom.frame = document.createElement('div');
- this.dom.frame.className = 'legend';
- this.dom.frame.style.position = "absolute";
- this.dom.frame.style.top = "10px";
- this.dom.frame.style.display = "block";
+ // calculate actual size and position
+ var foreground = this.dom.foreground;
+ this.top = foreground.offsetTop;
+ this.left = foreground.offsetLeft;
+ this.width = foreground.offsetWidth;
+ resized = util.updateProperty(this, 'height', height) || resized;
- this.dom.textArea = document.createElement('div');
- this.dom.textArea.className = 'legendText';
- this.dom.textArea.style.position = "relative";
- this.dom.textArea.style.top = "0px";
+ // recalculate size of label
+ resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
+ resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
- this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
- this.svg.style.position = 'absolute';
- this.svg.style.top = 0 +'px';
- this.svg.style.width = this.options.iconSize + 5 + 'px';
+ // apply new height
+ this.dom.background.style.height = height + 'px';
+ this.dom.foreground.style.height = height + 'px';
+ this.dom.label.style.height = height + 'px';
- this.dom.frame.appendChild(this.svg);
- this.dom.frame.appendChild(this.dom.textArea);
+ // update vertical position of items after they are re-stacked and the height of the group is calculated
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ var item = this.visibleItems[i];
+ item.repositionY(margin);
+ }
+
+ return resized;
};
/**
- * Hide the component from the DOM
+ * Show this group: attach to the DOM
*/
- Legend.prototype.hide = function() {
- // remove the frame containing the items
- if (this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
+ Group.prototype.show = function() {
+ if (!this.dom.label.parentNode) {
+ this.itemSet.dom.labelSet.appendChild(this.dom.label);
}
- };
- /**
- * Show the component in the DOM (when not already visible).
- * @return {Boolean} changed
- */
- Legend.prototype.show = function() {
- // show frame containing the items
- if (!this.dom.frame.parentNode) {
- this.body.dom.center.appendChild(this.dom.frame);
+ if (!this.dom.foreground.parentNode) {
+ this.itemSet.dom.foreground.appendChild(this.dom.foreground);
}
- };
- Legend.prototype.setOptions = function(options) {
- var fields = ['enabled','orientation','icons','left','right'];
- util.selectiveDeepExtend(fields, this.options, options);
- };
+ if (!this.dom.background.parentNode) {
+ this.itemSet.dom.background.appendChild(this.dom.background);
+ }
- Legend.prototype.redraw = function() {
- var activeGroups = 0;
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- activeGroups++;
- }
- }
+ if (!this.dom.axis.parentNode) {
+ this.itemSet.dom.axis.appendChild(this.dom.axis);
}
+ };
- if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) {
- this.hide();
+ /**
+ * Hide this group: remove from the DOM
+ */
+ Group.prototype.hide = function() {
+ var label = this.dom.label;
+ if (label.parentNode) {
+ label.parentNode.removeChild(label);
}
- else {
- this.show();
- if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
- this.dom.frame.style.left = '4px';
- this.dom.frame.style.textAlign = "left";
- this.dom.textArea.style.textAlign = "left";
- this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
- this.dom.textArea.style.right = '';
- this.svg.style.left = 0 +'px';
- this.svg.style.right = '';
- }
- else {
- this.dom.frame.style.right = '4px';
- this.dom.frame.style.textAlign = "right";
- this.dom.textArea.style.textAlign = "right";
- this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
- this.dom.textArea.style.left = '';
- this.svg.style.right = 0 +'px';
- this.svg.style.left = '';
- }
- if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
- this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
- this.dom.frame.style.bottom = '';
- }
- else {
- this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
- this.dom.frame.style.top = '';
- }
+ var foreground = this.dom.foreground;
+ if (foreground.parentNode) {
+ foreground.parentNode.removeChild(foreground);
+ }
- if (this.options.icons == false) {
- this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
- this.dom.textArea.style.right = '';
- this.dom.textArea.style.left = '';
- this.svg.style.width = '0px';
- }
- else {
- this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
- this.drawLegendIcons();
- }
+ var background = this.dom.background;
+ if (background.parentNode) {
+ background.parentNode.removeChild(background);
+ }
- var content = '';
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- content += this.groups[groupId].content + '
';
- }
- }
- }
- this.dom.textArea.innerHTML = content;
- this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
+ var axis = this.dom.axis;
+ if (axis.parentNode) {
+ axis.parentNode.removeChild(axis);
}
};
- Legend.prototype.drawLegendIcons = function() {
- if (this.dom.frame.parentNode) {
- DOMutil.prepareElements(this.svgElements);
- var padding = window.getComputedStyle(this.dom.frame).paddingTop;
- var iconOffset = Number(padding.replace('px',''));
- var x = iconOffset;
- var iconWidth = this.options.iconSize;
- var iconHeight = 0.75 * this.options.iconSize;
- var y = iconOffset + 0.5 * iconHeight + 3;
-
- this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
+ /**
+ * Add an item to the group
+ * @param {Item} item
+ */
+ Group.prototype.add = function(item) {
+ this.items[item.id] = item;
+ item.setParent(this);
- for (var groupId in this.groups) {
- if (this.groups.hasOwnProperty(groupId)) {
- if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) {
- this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
- y += iconHeight + this.options.iconSpacing;
- }
- }
+ // add to
+ if (item.data.subgroup !== undefined) {
+ if (this.subgroups[item.data.subgroup] === undefined) {
+ this.subgroups[item.data.subgroup] = {height:0, visible: false};
}
+ }
- DOMutil.cleanupElements(this.svgElements);
+ if (this.visibleItems.indexOf(item) == -1) {
+ var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
+ this._checkIfVisible(item, this.visibleItems, range);
}
};
- module.exports = Legend;
+ Group.prototype.resetSubgroups = function() {
+ for (var subgroup in this.subgroups) {
+ if (this.subgroups.hasOwnProperty(subgroup)) {
+ this.subgroups[subgroup].visible = false;
+ }
+ }
+ }
+ /**
+ * Remove an item from the group
+ * @param {Item} item
+ */
+ Group.prototype.remove = function(item) {
+ delete this.items[item.id];
+ item.setParent(this.itemSet);
-/***/ },
-/* 45 */
-/***/ function(module, exports, __webpack_require__) {
+ // remove from visible items
+ var index = this.visibleItems.indexOf(item);
+ if (index != -1) this.visibleItems.splice(index, 1);
- // Utility functions for ordering and stacking of items
- var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors
+ // TODO: also remove from ordered items?
+ };
/**
- * Order items by their start data
- * @param {Item[]} items
+ * Remove an item from the corresponding DataSet
+ * @param {Item} item
*/
- exports.orderByStart = function(items) {
- items.sort(function (a, b) {
- return a.data.start - b.data.start;
- });
+ Group.prototype.removeFromDataSet = function(item) {
+ this.itemSet.removeItem(item.id);
};
/**
- * Order items by their end date. If they have no end date, their start date
- * is used.
- * @param {Item[]} items
+ * Reorder the items
*/
- exports.orderByEnd = function(items) {
- items.sort(function (a, b) {
- var aTime = ('end' in a.data) ? a.data.end : a.data.start,
- bTime = ('end' in b.data) ? b.data.end : b.data.start;
+ Group.prototype.order = function() {
+ var array = util.toArray(this.items);
+ this.orderedItems.byStart = array;
+ this.orderedItems.byEnd = this._constructByEndArray(array);
- return aTime - bTime;
- });
+ stack.orderByStart(this.orderedItems.byStart);
+ stack.orderByEnd(this.orderedItems.byEnd);
};
/**
- * Adjust vertical positions of the items such that they don't overlap each
- * other.
- * @param {Item[]} items
- * All visible items
- * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
- * Margins between items and between items and the axis.
- * @param {boolean} [force=false]
- * If true, all items will be repositioned. If false (default), only
- * items having a top===null will be re-stacked
+ * Create an array containing all items being a range (having an end date)
+ * @param {Item[]} array
+ * @returns {RangeItem[]}
+ * @private
*/
- exports.stack = function(items, margin, force) {
- var i, iMax;
+ Group.prototype._constructByEndArray = function(array) {
+ var endArray = [];
- if (force) {
- // reset top position of all items
- for (i = 0, iMax = items.length; i < iMax; i++) {
- items[i].top = null;
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] instanceof RangeItem) {
+ endArray.push(array[i]);
}
}
+ return endArray;
+ };
- // calculate new, non-overlapping positions
- for (i = 0, iMax = items.length; i < iMax; i++) {
- var item = items[i];
- if (item.top === null) {
- // initialize top position
- item.top = margin.axis;
+ /**
+ * Update the visible items
+ * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
+ * @param {Item[]} visibleItems The previously visible items.
+ * @param {{start: number, end: number}} range Visible range
+ * @return {Item[]} visibleItems The new visible items.
+ * @private
+ */
+ Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
+ var initialPosByStart,
+ newVisibleItems = [],
+ i;
- 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 && other.ignoreStacking == false && exports.collision(item, other, margin.item)) {
- collidingItem = other;
- break;
- }
- }
+ // first check if the items that were in view previously are still in view.
+ // this handles the case for the RangeItem that is both before and after the current one.
+ if (visibleItems.length > 0) {
+ for (i = 0; i < visibleItems.length; i++) {
+ this._checkIfVisible(visibleItems[i], newVisibleItems, range);
+ }
+ }
- if (collidingItem != null) {
- // There is a collision. Reposition the items above the colliding element
- item.top = collidingItem.top + collidingItem.height + margin.item.vertical;
- }
- } while (collidingItem);
+ // If there were no visible items previously, use binarySearch to find a visible PointItem or RangeItem (based on startTime)
+ if (newVisibleItems.length == 0) {
+ initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
+ }
+ else {
+ initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
+ }
+
+ // use visible search to find a visible RangeItem (only based on endTime)
+ var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
+
+ // if we found a initial ID to use, trace it up and down until we meet an invisible item.
+ if (initialPosByStart != -1) {
+ for (i = initialPosByStart; i >= 0; i--) {
+ if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
+ }
+ for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
+ if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
+ }
+ }
+
+ // if we found a initial ID to use, trace it up and down until we meet an invisible item.
+ if (initialPosByEnd != -1) {
+ for (i = initialPosByEnd; i >= 0; i--) {
+ if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
+ }
+ for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
+ if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
}
}
+
+ return newVisibleItems;
};
+
/**
- * Adjust vertical positions of the items without stacking them
- * @param {Item[]} items
- * All visible items
- * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin
- * Margins between items and between items and the axis.
+ * this function checks if an item is invisible. If it is NOT we make it visible
+ * and add it to the global visible items. If it is, return true.
+ *
+ * @param {Item} item
+ * @param {Item[]} visibleItems
+ * @param {{start:number, end:number}} range
+ * @returns {boolean}
+ * @private
*/
- exports.nostack = function(items, margin, subgroups) {
- var i, iMax;
-
- // reset top position of all items
- for (i = 0, iMax = items.length; i < iMax; i++) {
- if (items[i].data.subgroup !== undefined) {
- items[i].top = margin.axis + (subgroups[items[i].data.subgroup].height + margin.item.vertical) * items[i].data.subgroup;
+ Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
+ if (item.isVisible(range)) {
+ if (!item.displayed) item.show();
+ item.repositionX();
+ if (visibleItems.indexOf(item) == -1) {
+ visibleItems.push(item);
+ }
+ return false;
}
else {
- items[i].top = margin.axis;
+ if (item.displayed) item.hide();
+ return true;
}
- }
};
/**
- * Test if the two provided items collide
- * The items must have parameters left, width, top, and height.
- * @param {Item} a The first item
- * @param {Item} b The second item
- * @param {{horizontal: number, vertical: number}} margin
- * An object containing a horizontal and vertical
- * minimum required margin.
- * @return {boolean} true if a and b collide, else false
+ * this function is very similar to the _checkIfInvisible() but it does not
+ * return booleans, hides the item if it should not be seen and always adds to
+ * the visibleItems.
+ * this one is for brute forcing and hiding.
+ *
+ * @param {Item} item
+ * @param {Array} visibleItems
+ * @param {{start:number, end:number}} range
+ * @private
*/
- exports.collision = function(a, b, margin) {
- return ((a.left - margin.horizontal + EPSILON) < (b.left + b.width) &&
- (a.left + a.width + margin.horizontal - EPSILON) > b.left &&
- (a.top - margin.vertical + EPSILON) < (b.top + b.height) &&
- (a.top + a.height + margin.vertical - EPSILON) > b.top);
+ Group.prototype._checkIfVisible = function(item, visibleItems, range) {
+ if (item.isVisible(range)) {
+ if (!item.displayed) item.show();
+ // reposition item horizontally
+ item.repositionX();
+ visibleItems.push(item);
+ }
+ else {
+ if (item.displayed) item.hide();
+ }
};
+ module.exports = Group;
+
/***/ },
/* 46 */
@@ -21967,7 +22024,7 @@ return /******/ (function(modules) { // webpackBootstrap
var Emitter = __webpack_require__(10);
var Hammer = __webpack_require__(18);
- var mousetrap = __webpack_require__(38);
+ var mousetrap = __webpack_require__(32);
var util = __webpack_require__(1);
var hammerUtil = __webpack_require__(21);
var DataSet = __webpack_require__(7);
@@ -21980,7 +22037,7 @@ return /******/ (function(modules) { // webpackBootstrap
var Edge = __webpack_require__(52);
var Popup = __webpack_require__(53);
var MixinLoader = __webpack_require__(54);
- var Activator = __webpack_require__(37);
+ var Activator = __webpack_require__(31);
var locales = __webpack_require__(65);
// Load custom shapes into CanvasRenderingContext2D
diff --git a/examples/timeline/29_hiding_times.html b/examples/timeline/29_hiding_times.html
index 3bcd3579..4c76db86 100644
--- a/examples/timeline/29_hiding_times.html
+++ b/examples/timeline/29_hiding_times.html
@@ -44,11 +44,14 @@
},
start: '2014-04-17',
end: '2014-05-01',
- height: '200px'
+ height: '200px',
+ editable: true,
+ showCustomTime: true
};
// Create a Timeline
var timeline = new vis.Timeline(container, items, options);
+ timeline.setCustomTime("2014-04-18 13:00:00");