diff --git a/HISTORY.md b/HISTORY.md index e59a824a..d4493ddb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -26,6 +26,7 @@ http://visjs.org - Added getDataset to all manipulation functions. Thanks @ericvandever! - Fixed #1039, icon now returns correct distance to border - Added blurEdge and hoverEdge events. +- Added labelHighlightBold option to edges and nodes. ### Graph2d diff --git a/dist/vis.js b/dist/vis.js index 4a1f9e44..5b662a3a 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -84,65 +84,65 @@ return /******/ (function(modules) { // webpackBootstrap // utils 'use strict'; - exports.util = __webpack_require__(9); - exports.DOMutil = __webpack_require__(14); + exports.util = __webpack_require__(14); + exports.DOMutil = __webpack_require__(19); // data - exports.DataSet = __webpack_require__(15); - exports.DataView = __webpack_require__(17); - exports.Queue = __webpack_require__(16); + exports.DataSet = __webpack_require__(20); + exports.DataView = __webpack_require__(22); + exports.Queue = __webpack_require__(21); // Graph3d - exports.Graph3d = __webpack_require__(18); + exports.Graph3d = __webpack_require__(23); exports.graph3d = { - Camera: __webpack_require__(22), - Filter: __webpack_require__(23), - Point2d: __webpack_require__(19), - Point3d: __webpack_require__(21), - Slider: __webpack_require__(24), - StepNumber: __webpack_require__(25) + Camera: __webpack_require__(27), + Filter: __webpack_require__(28), + Point2d: __webpack_require__(24), + Point3d: __webpack_require__(26), + Slider: __webpack_require__(29), + StepNumber: __webpack_require__(30) }; // Timeline - exports.Timeline = __webpack_require__(26); - exports.Graph2d = __webpack_require__(50); + exports.Timeline = __webpack_require__(31); + exports.Graph2d = __webpack_require__(55); exports.timeline = { - DateUtil: __webpack_require__(32), - DataStep: __webpack_require__(53), - Range: __webpack_require__(30), - stack: __webpack_require__(36), - TimeStep: __webpack_require__(38), + DateUtil: __webpack_require__(37), + DataStep: __webpack_require__(58), + Range: __webpack_require__(35), + stack: __webpack_require__(41), + TimeStep: __webpack_require__(43), components: { items: { - Item: __webpack_require__(4), - BackgroundItem: __webpack_require__(41), - BoxItem: __webpack_require__(40), - PointItem: __webpack_require__(2), - RangeItem: __webpack_require__(37) + Item: __webpack_require__(8), + BackgroundItem: __webpack_require__(46), + BoxItem: __webpack_require__(45), + PointItem: __webpack_require__(6), + RangeItem: __webpack_require__(42) }, - Component: __webpack_require__(28), - CurrentTime: __webpack_require__(27), - CustomTime: __webpack_require__(45), - DataAxis: __webpack_require__(52), - GraphGroup: __webpack_require__(54), - Group: __webpack_require__(35), - BackgroundGroup: __webpack_require__(39), - ItemSet: __webpack_require__(34), - Legend: __webpack_require__(58), - LineGraph: __webpack_require__(51), - TimeAxis: __webpack_require__(42) + Component: __webpack_require__(33), + CurrentTime: __webpack_require__(32), + CustomTime: __webpack_require__(50), + DataAxis: __webpack_require__(57), + GraphGroup: __webpack_require__(59), + Group: __webpack_require__(40), + BackgroundGroup: __webpack_require__(44), + ItemSet: __webpack_require__(39), + Legend: __webpack_require__(63), + LineGraph: __webpack_require__(56), + TimeAxis: __webpack_require__(47) } }; // Network - exports.Network = __webpack_require__(60); + exports.Network = __webpack_require__(65); exports.network = { Images: __webpack_require__(112), dotparser: __webpack_require__(110), gephiParser: __webpack_require__(111), - allOptions: __webpack_require__(108) + allOptions: __webpack_require__(7) }; exports.network.convertDot = function (input) { return exports.network.dotparser.DOTToGraph(input); @@ -157,10 +157,10 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(10); - exports.hammer = __webpack_require__(5); // TODO: deprecate exports.hammer some day - exports.Hammer = __webpack_require__(5); - exports.keycharm = __webpack_require__(44); + exports.moment = __webpack_require__(15); + exports.hammer = __webpack_require__(10); // TODO: deprecate exports.hammer some day + exports.Hammer = __webpack_require__(10); + exports.keycharm = __webpack_require__(49); /***/ }, /* 1 */ @@ -181,941 +181,1042 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var Item = __webpack_require__(4); - - /** - * @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 - } - }; - - // 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 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; - }; + Object.defineProperty(exports, '__esModule', { + value: true + }); - /** - * Repaint the item - */ - PointItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - // background box - dom.point = document.createElement('div'); - // className is updated in redraw() + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'vis-item-content'; - dom.point.appendChild(dom.content); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - // dot at start - dom.dot = document.createElement('div'); - dom.point.appendChild(dom.dot); + var _componentsNode = __webpack_require__(67); - // attach this item as attribute - dom.point['timeline-item'] = this; + var _componentsNode2 = _interopRequireDefault(_componentsNode); - this.dirty = true; - } + var _componentsSharedLabel = __webpack_require__(4); - // 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; + var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - // 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); + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); - var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; + var NodesHandler = (function () { + function NodesHandler(body, images, groups, layoutEngine) { + var _this = this; - // update class - var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); - dom.point.className = 'vis-item vis-point' + className; - dom.dot.className = 'vis-item vis-dot' + className; + _classCallCheck(this, NodesHandler); - // recalculate size of dot and contents - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; + this.body = body; + this.images = images; + this.groups = groups; + this.layoutEngine = layoutEngine; - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + // create the node API in the body container + this.body.functions.createNode = this.create.bind(this); - dom.dot.style.top = (this.height - this.props.dot.height) / 2 + 'px'; - dom.dot.style.left = this.props.dot.width / 2 + 'px'; + this.nodesListeners = { + add: function add(event, params) { + _this.add(params.items); + }, + update: function update(event, params) { + _this.update(params.items, params.data); + }, + remove: function remove(event, params) { + _this.remove(params.items); + } + }; - // recalculate size - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; + this.options = {}; + this.defaultOptions = { + borderWidth: 1, + borderWidthSelected: 2, + brokenImage: undefined, + color: { + border: '#2B7CE9', + background: '#97C2FC', + highlight: { + border: '#2B7CE9', + background: '#D2E5FF' + }, + hover: { + border: '#2B7CE9', + background: '#D2E5FF' + } + }, + fixed: { + x: false, + y: false + }, + font: { + color: '#343434', + size: 14, // px + face: 'arial', + background: 'none', + strokeWidth: 0, // px + strokeColor: '#ffffff', + align: 'horizontal' + }, + group: undefined, + hidden: false, + icon: { + face: 'FontAwesome', //'FontAwesome', + code: undefined, //'\uf007', + size: 50, //50, + color: '#2B7CE9' //'#aa00ff' + }, + image: undefined, // --> URL + label: undefined, + labelHighlightBold: true, + level: undefined, + mass: 1, + physics: true, + scaling: { + min: 10, + max: 30, + label: { + enabled: false, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 5 + }, + customScalingFunction: function customScalingFunction(min, max, total, value) { + if (max === min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + } + }, + shadow: { + enabled: false, + size: 10, + x: 5, + y: 5 + }, + shape: 'ellipse', + size: 25, + title: undefined, + value: undefined, + x: undefined, + y: undefined + }; + util.extend(this.options, this.defaultOptions); - this.dirty = false; + this.bindEventListeners(); } - this._repaintDeleteButton(dom.point); - }; - - /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - */ - PointItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); - } - }; + _createClass(NodesHandler, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this2 = this; - /** - * 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); + // refresh the nodes. Used when reverting from hierarchical layout + this.body.emitter.on('refreshNodes', this.refresh.bind(this)); + this.body.emitter.on('refresh', this.refresh.bind(this)); + this.body.emitter.on('destroy', function () { + delete _this2.body.functions.createNode; + delete _this2.nodesListeners.add; + delete _this2.nodesListeners.update; + delete _this2.nodesListeners.remove; + delete _this2.nodesListeners; + }); } + }, { + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + _componentsNode2['default'].parseOptions(this.options, options); - this.displayed = false; - } - }; - - /** - * Reposition the item horizontally - * @Override - */ - 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'; - }; - - /** - * Reposition the item vertically - * @Override - */ - PointItem.prototype.repositionY = function () { - var orientation = this.options.orientation.item; - var point = this.dom.point; + // update the shape in all nodes + if (options.shape !== undefined) { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].updateShape(); + } + } + } - if (orientation == 'top') { - point.style.top = this.top + 'px'; - } else { - point.style.top = this.parent.height - this.top - this.height + 'px'; - } - }; + // update the shape size in all nodes + if (options.font !== undefined) { + _componentsSharedLabel2['default'].parseOptions(this.options.font, options); + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].updateLabelModule(); + this.body.nodes[nodeId]._reset(); + } + } + } - /** - * Return the width of the item left from its start date - * @return {number} - */ - PointItem.prototype.getWidthLeft = function () { - return this.props.dot.width; - }; + // update the shape size in all nodes + if (options.size !== undefined) { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId]._reset(); + } + } + } - /** - * Return the width of the item right from its start date - * @return {number} - */ - PointItem.prototype.getWidthRight = function () { - return this.width - this.props.dot.width; - }; + // update the state of the letiables if needed + if (options.hidden !== undefined || options.physics !== undefined) { + this.body.emitter.emit('_dataChanged'); + } + } + } + }, { + key: 'setData', - module.exports = PointItem; + /** + * Set a data set with nodes for the network + * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * @private + */ + value: function setData(nodes) { + var _this3 = this; -/***/ }, -/* 3 */ -/***/ function(module, exports, __webpack_require__) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - "use strict"; + var oldNodesData = this.body.data.nodes; - Object.defineProperty(exports, "__esModule", { - value: true - }); + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.body.data.nodes = nodes; + } else if (Array.isArray(nodes)) { + this.body.data.nodes = new DataSet(); + this.body.data.nodes.add(nodes); + } else if (!nodes) { + this.body.data.nodes = new DataSet(); + } else { + throw new TypeError('Array or DataSet expected'); + } - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.off(event, callback); + }); + } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + // remove drawn nodes + this.body.nodes = {}; - var Node = __webpack_require__(63); - var Edge = __webpack_require__(83); - var util = __webpack_require__(9); + if (this.body.data.nodes) { + (function () { + // subscribe to new dataset + var me = _this3; + util.forEach(_this3.nodesListeners, function (callback, event) { + me.body.data.nodes.on(event, callback); + }); - var SelectionHandler = (function () { - function SelectionHandler(body, canvas) { - var _this = this; + // draw all new nodes + var ids = _this3.body.data.nodes.getIds(); + _this3.add(ids, true); + })(); + } - _classCallCheck(this, SelectionHandler); + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); + } + } + }, { + key: 'add', - this.body = body; - this.canvas = canvas; - this.selectionObj = { nodes: [], edges: [] }; - this.hoverObj = { nodes: {}, edges: {} }; + /** + * Add nodes + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - this.options = {}; - this.defaultOptions = { - multiselect: false, - selectable: true, - selectConnectedEdges: true, - hoverConnectedEdges: true - }; - util.extend(this.options, this.defaultOptions); + var id = undefined; + var newNodes = []; + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + var properties = this.body.data.nodes.get(id); + var node = this.create(properties); + newNodes.push(node); + this.body.nodes[id] = node; // note: this may replace an existing node + } - this.body.emitter.on("_dataChanged", function () { - _this.updateSelection(); - }); - } + this.layoutEngine.positionInitially(newNodes); - _createClass(SelectionHandler, [{ - key: "setOptions", - value: function setOptions(options) { - if (options !== undefined) { - var fields = ["multiselect", "hoverConnectedEdges", "selectable", "selectConnectedEdges"]; - util.selectiveDeepExtend(fields, this.options, options); + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); } } }, { - key: "selectOnPoint", + key: 'update', /** - * handles the selection part of the tap; - * - * @param {Object} pointer + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids * @private */ - value: function selectOnPoint(pointer) { - var selected = false; - if (this.options.selectable === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); - - // unselect after getting the objects in order to restore width and height. - this.unselectAll(); - - if (obj !== undefined) { - selected = this.selectObject(obj); + value: function update(ids, changedData) { + var nodes = this.body.nodes; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = changedData[i]; + if (node !== undefined) { + // update node + dataChanged = node.setOptions(data); + } else { + dataChanged = true; + // create node + node = this.create(data); + nodes[id] = node; } - this.body.emitter.emit("_requestRedraw"); } - return selected; + if (dataChanged === true) { + this.body.emitter.emit('_dataChanged'); + } else { + this.body.emitter.emit('_dataUpdated'); + } } }, { - key: "selectAdditionalOnPoint", - value: function selectAdditionalOnPoint(pointer) { - var selectionChanged = false; - if (this.options.selectable === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); + key: 'remove', - if (obj !== undefined) { - selectionChanged = true; - if (obj.isSelected() === true) { - this.deselectObject(obj); - } else { - this.selectObject(obj); - } + /** + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var nodes = this.body.nodes; - this.body.emitter.emit("_requestRedraw"); - } + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + delete nodes[id]; } - return selectionChanged; + + this.body.emitter.emit('_dataChanged'); } }, { - key: "_generateClickEvent", - value: function _generateClickEvent(eventType, event, pointer, oldSelection) { - var emptySelection = arguments[4] === undefined ? false : arguments[4]; + key: 'create', - var properties = undefined; - if (emptySelection === true) { - properties = { nodes: [], edges: [] }; - } else { - properties = this.getSelection(); - } - properties["pointer"] = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: this.canvas.DOMtoCanvas(pointer) - }; - properties["event"] = event; + /** + * create a node + * @param properties + * @param constructorClass + */ + value: function create(properties) { + var constructorClass = arguments[1] === undefined ? _componentsNode2['default'] : arguments[1]; - if (oldSelection !== undefined) { - properties["previousSelection"] = oldSelection; - } - this.body.emitter.emit(eventType, properties); + return new constructorClass(properties, this.body, this.images, this.groups, this.options); } }, { - key: "selectObject", - value: function selectObject(obj) { - var highlightEdges = arguments[1] === undefined ? this.options.selectConnectedEdges : arguments[1]; - - if (obj !== undefined) { - if (obj instanceof Node) { - if (highlightEdges === true) { - this._selectConnectedEdges(obj); - } + key: 'refresh', + value: function refresh() { + var nodes = this.body.nodes; + for (var nodeId in nodes) { + var node = undefined; + if (nodes.hasOwnProperty(nodeId)) { + node = nodes[nodeId]; + } + var data = this.body.data.nodes._data[nodeId]; + if (node !== undefined && data !== undefined) { + node.setOptions({ fixed: false }); + node.setOptions(data); } - obj.select(); - this._addToSelection(obj); - return true; } - return false; } }, { - key: "deselectObject", - value: function deselectObject(obj) { - if (obj.isSelected() === true) { - obj.selected = false; - this._removeFromSelection(obj); + key: 'getPositions', + + /** + * Returns the positions of the nodes. + * @param ids --> optional, can be array of nodeIds, can be string + * @returns {{}} + */ + value: function getPositions(ids) { + var dataArray = {}; + if (ids !== undefined) { + if (Array.isArray(ids) === true) { + for (var i = 0; i < ids.length; i++) { + if (this.body.nodes[ids[i]] !== undefined) { + var node = this.body.nodes[ids[i]]; + dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } else { + if (this.body.nodes[ids] !== undefined) { + var node = this.body.nodes[ids]; + dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } } + return dataArray; } }, { - key: "_getAllNodesOverlappingWith", + key: 'storePositions', /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private + * Load the XY positions of the nodes into the dataset. */ - value: function _getAllNodesOverlappingWith(object) { - var overlappingNodes = []; - var nodes = this.body.nodes; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); + value: function storePositions() { + // todo: add support for clusters and hierarchical. + var dataArray = []; + var dataset = this.body.data.nodes.getDataSet(); + + for (var nodeId in dataset._data) { + if (dataset._data.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) { + dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) }); + } } } - return overlappingNodes; + dataset.update(dataArray); } }, { - key: "_pointerToPositionObject", + key: 'getBoundingBox', /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private + * get the bounding box of a node. + * @param nodeId + * @returns {j|*} */ - value: function _pointerToPositionObject(pointer) { - var canvasPos = this.canvas.DOMtoCanvas(pointer); - return { - left: canvasPos.x - 1, - top: canvasPos.y + 1, - right: canvasPos.x + 1, - bottom: canvasPos.y - 1 - }; + value: function getBoundingBox(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].shape.boundingBox; + } } }, { - key: "getNodeAt", + key: 'getConnectedNodes', /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | undefined} node - * @private + * Get the Ids of nodes connected to this node. + * @param nodeId + * @returns {Array} */ - value: function getNodeAt(pointer) { - var returnNode = arguments[1] === undefined ? true : arguments[1]; - - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - if (returnNode === true) { - return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } else { - return overlappingNodes[overlappingNodes.length - 1]; + value: function getConnectedNodes(nodeId) { + var nodeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + var nodeObj = {}; // used to quickly check if node already exists + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + if (edge.toId == nodeId) { + // these are double equals since ids can be numeric or string + if (nodeObj[edge.fromId] === undefined) { + nodeList.push(edge.fromId); + nodeObj[edge.fromId] = true; + } + } else if (edge.fromId == nodeId) { + // these are double equals since ids can be numeric or string + if (nodeObj[edge.toId] === undefined) { + nodeList.push(edge.toId); + nodeObj[edge.toId] = true; + } + } } - } else { - return undefined; } + return nodeList; } }, { - key: "_getEdgesOverlappingWith", + key: 'getConnectedEdges', /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private + * Get the ids of the edges connected to this node. + * @param nodeId + * @returns {*} */ - value: function _getEdgesOverlappingWith(object, overlappingEdges) { - var edges = this.body.edges; - for (var i = 0; i < this.body.edgeIndices.length; i++) { - var edgeId = this.body.edgeIndices[i]; - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); + value: function getConnectedEdges(nodeId) { + var edgeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + for (var i = 0; i < node.edges.length; i++) { + edgeList.push(node.edges[i].id); } + } else { + console.log('NodeId provided for getConnectedEdges does not exist. Provided: ', nodeId); } + return edgeList; } - }, { - key: "_getAllEdgesOverlappingWith", + }]); - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getAllEdgesOverlappingWith(object) { - var overlappingEdges = []; - this._getEdgesOverlappingWith(object, overlappingEdges); - return overlappingEdges; + return NodesHandler; + })(); + + exports['default'] = NodesHandler; + module.exports = exports['default']; + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _sharedLabel = __webpack_require__(4); + + var _sharedLabel2 = _interopRequireDefault(_sharedLabel); + + var _edgesBezierEdgeDynamic = __webpack_require__(85); + + var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); + + var _edgesBezierEdgeStatic = __webpack_require__(88); + + var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); + + var _edgesStraightEdge = __webpack_require__(89); + + var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); + + var util = __webpack_require__(14); + + /** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with options. Must contain + * At least options from and to. + * Available options: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color + */ + + var Edge = (function () { + function Edge(options, body, globalOptions) { + _classCallCheck(this, Edge); + + if (body === undefined) { + throw 'No body provided'; } - }, { - key: "getEdgeAt", + this.options = util.bridgeObject(globalOptions); + this.body = body; + + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.selected = false; + this.hover = false; + this.labelDirty = true; + this.colorDirty = true; + + this.baseWidth = this.options.width; + this.baseFontSize = this.options.font.size; + + this.from = undefined; // a node + this.to = undefined; // a node + + this.edgeType = undefined; + + this.connected = false; + + this.labelModule = new _sharedLabel2['default'](this.body, this.options); + + this.setOptions(options); + } + + _createClass(Edge, [{ + key: 'setOptions', /** - * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call - * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. - * - * @param pointer - * @returns {undefined} - * @private + * Set or overwrite options for the edge + * @param {Object} options an object with options + * @param doNotEmit */ - value: function getEdgeAt(pointer) { - var returnEdge = arguments[1] === undefined ? true : arguments[1]; + value: function setOptions(options) { + if (!options) { + return; + } + this.colorDirty = true; - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + Edge.parseOptions(this.options, options, true); - if (overlappingEdges.length > 0) { - if (returnEdge === true) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; - } else { - return overlappingEdges[overlappingEdges.length - 1]; - } - } else { - return undefined; + if (options.id !== undefined) { + this.id = options.id; + } + if (options.from !== undefined) { + this.fromId = options.from; + } + if (options.to !== undefined) { + this.toId = options.to; + } + if (options.title !== undefined) { + this.title = options.title; + } + if (options.value !== undefined) { + options.value = parseFloat(options.value); + } + + // update label Module + this.updateLabelModule(); + + var dataChanged = this.updateEdgeType(); + + // if anything has been updates, reset the selection width and the hover width + this._setInteractionWidths(); + + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); + + if (options.hidden !== undefined || options.physics !== undefined) { + dataChanged = true; } + + return dataChanged; } }, { - key: "_addToSelection", + key: 'updateLabelModule', /** - * Add object to the selection array. - * - * @param obj - * @private + * update the options in the label module */ - value: function _addToSelection(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } else { - this.selectionObj.edges[obj.id] = obj; + value: function updateLabelModule() { + this.labelModule.setOptions(this.options, true); + if (this.labelModule.baseSize !== undefined) { + this.baseFontSize = this.labelModule.baseSize; } } }, { - key: "_addToHover", + key: 'updateEdgeType', /** - * Add object to the selection array. - * - * @param obj - * @private + * update the edge type, set the options + * @returns {boolean} */ - value: function _addToHover(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; + value: function updateEdgeType() { + var dataChanged = false; + var changeInType = true; + if (this.edgeType !== undefined) { + if (this.edgeType instanceof _edgesBezierEdgeDynamic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type === 'dynamic') { + changeInType = false; + } + if (this.edgeType instanceof _edgesBezierEdgeStatic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type !== 'dynamic') { + changeInType = false; + } + if (this.edgeType instanceof _edgesStraightEdge2['default'] && this.options.smooth.enabled === false) { + changeInType = false; + } + + if (changeInType === true) { + dataChanged = this.edgeType.cleanup(); + } + } + + if (changeInType === true) { + if (this.options.smooth.enabled === true) { + if (this.options.smooth.type === 'dynamic') { + dataChanged = true; + this.edgeType = new _edgesBezierEdgeDynamic2['default'](this.options, this.body, this.labelModule); + } else { + this.edgeType = new _edgesBezierEdgeStatic2['default'](this.options, this.body, this.labelModule); + } + } else { + this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); + } } else { - this.hoverObj.edges[obj.id] = obj; + // if nothing changes, we just set the options. + this.edgeType.setOptions(this.options); } + + return dataChanged; } }, { - key: "_removeFromSelection", + key: 'togglePhysics', /** - * Remove a single option from selection. - * - * @param {Object} obj - * @private + * Enable or disable the physics. + * @param status */ - value: function _removeFromSelection(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } else { - delete this.selectionObj.edges[obj.id]; - } + value: function togglePhysics(status) { + this.options.physics = status; + this.edgeType.togglePhysics(status); } }, { - key: "unselectAll", + key: 'connect', /** - * Unselect all. The selectionObj is useful for this. - * - * @private + * Connect an edge to its nodes */ - value: function unselectAll() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); + value: function connect() { + this.disconnect(); + + this.from = this.body.nodes[this.fromId] || undefined; + this.to = this.body.nodes[this.toId] || undefined; + this.connected = this.from !== undefined && this.to !== undefined; + + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); + if (this.to) { + this.to.detachEdge(this); } } - this.selectionObj = { nodes: {}, edges: {} }; + this.edgeType.connect(); } }, { - key: "_getSelectedNodeCount", + key: 'disconnect', /** - * return the number of selected nodes - * - * @returns {number} - * @private + * Disconnect an edge from its nodes */ - value: function _getSelectedNodeCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } + value: function disconnect() { + if (this.from) { + this.from.detachEdge(this); + this.from = undefined; } - return count; + if (this.to) { + this.to.detachEdge(this); + this.to = undefined; + } + + this.connected = false; } }, { - key: "_getSelectedNode", + key: 'getTitle', /** - * return the selected node - * - * @returns {number} - * @private + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. */ - value: function _getSelectedNode() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } - } - return undefined; + value: function getTitle() { + return this.title; } }, { - key: "_getSelectedEdge", + key: 'isSelected', /** - * return the selected edge - * - * @returns {number} - * @private + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false */ - value: function _getSelectedEdge() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; - } - } - return undefined; + value: function isSelected() { + return this.selected; } }, { - key: "_getSelectedEdgeCount", + key: 'getValue', /** - * return the number of selected edges - * - * @returns {number} - * @private + * Retrieve the value of the edge. Can be undefined + * @return {Number} value */ - value: function _getSelectedEdgeCount() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } - } - return count; + value: function getValue() { + return this.options.value; } }, { - key: "_getSelectedObjectCount", + key: 'setValueRange', /** - * return the number of selected objects. - * - * @returns {number} - * @private + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + * @param total */ - value: function _getSelectedObjectCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; + value: function setValueRange(min, max, total) { + if (this.options.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); + var widthDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled === true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; } + this.options.width = this.options.scaling.min + scale * widthDiff; + } else { + this.options.width = this.baseWidth; + this.options.font.size = this.baseFontSize; } - return count; - } - }, { - key: "_selectionIsEmpty", - /** - * Check if anything is selected - * - * @returns {boolean} - * @private - */ - value: function _selectionIsEmpty() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; - } - } - return true; + this._setInteractionWidths(); } }, { - key: "_clusterInSelection", - - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - value: function _clusterInSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; - } - } + key: '_setInteractionWidths', + value: function _setInteractionWidths() { + if (typeof this.options.hoverWidth === 'function') { + this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width); + } else { + this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width; } - return false; - } - }, { - key: "_selectConnectedEdges", - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _selectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.select(); - this._addToSelection(edge); + if (typeof this.options.selectionWidth === 'function') { + this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width); + } else { + this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width; } } }, { - key: "_hoverConnectedEdges", + key: 'draw', /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx */ - value: function _hoverConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.hover = true; - this._addToHover(edge); - } + value: function draw(ctx) { + var via = this.edgeType.drawLine(ctx, this.selected, this.hover); + this.drawArrows(ctx, via); + this.drawLabel(ctx, via); } }, { - key: "_unselectConnectedEdges", - - /** - * unselect the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _unselectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.unselect(); - this._removeFromSelection(edge); + key: 'drawArrows', + value: function drawArrows(ctx, viaNode) { + if (this.options.arrows.from.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'from', viaNode, this.selected, this.hover); } - } - }, { - key: "blurObject", - - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - value: function blurObject(object) { - if (object.hover === true) { - object.hover = false; - if (object instanceof Node) { - this.body.emitter.emit("blurNode", { node: object.id }); - } else { - this.body.emitter.emit("blurEdge", { edge: object.id }); - } + if (this.options.arrows.middle.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'middle', viaNode, this.selected, this.hover); + } + if (this.options.arrows.to.enabled === true) { + this.edgeType.drawArrowHead(ctx, 'to', viaNode, this.selected, this.hover); } } }, { - key: "hoverObject", + key: 'drawLabel', + value: function drawLabel(ctx, viaNode) { + if (this.options.label !== undefined) { + // set style + var node1 = this.from; + var node2 = this.to; + var selected = this.from.selected || this.to.selected || this.selected; + if (node1.id != node2.id) { + this.labelModule.pointToSelf = false; + var point = this.edgeType.getPoint(0.5, viaNode); + ctx.save(); - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - value: function hoverObject(object) { - var hoverChanged = false; - // remove all node hover highlights - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (object === undefined) { - this.blurObject(this.hoverObj.nodes[nodeId]); - hoverChanged = true; - } else if (object instanceof Node && object.id != nodeId || object instanceof Edge || object === undefined) { - this.blurObject(this.hoverObj.nodes[nodeId]); - hoverChanged = true; - delete this.hoverObj.nodes[nodeId]; + // if the label has to be rotated: + if (this.options.font.align !== 'horizontal') { + this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); + ctx.translate(point.x, this.labelModule.size.yLine); + this._rotateForLabelAlignment(ctx); } - } - } - - // removing all edge hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } - } - if (object !== undefined) { - if (object.hover === false) { - object.hover = true; - this._addToHover(object); - hoverChanged = true; - if (object instanceof Node) { - this.body.emitter.emit("hoverNode", { node: object.id }); + // draw the label + this.labelModule.draw(ctx, point.x, point.y, selected); + ctx.restore(); + } else { + // Ignore the orientations. + this.labelModule.pointToSelf = true; + var x, y; + var radius = this.options.selfReferenceSize; + if (node1.shape.width > node1.shape.height) { + x = node1.x + node1.shape.width * 0.5; + y = node1.y - radius; } else { - this.body.emitter.emit("hoverEdge", { edge: object.id }); + x = node1.x + radius; + y = node1.y - node1.shape.height * 0.5; } + point = this._pointOnCircle(x, y, radius, 0.125); + this.labelModule.draw(ctx, point.x, point.y, selected); } - if (object instanceof Node && this.options.hoverConnectedEdges === true) { - this._hoverConnectedEdges(object); - } - } - - if (hoverChanged === true) { - this.body.emitter.emit("_requestRedraw"); } } }, { - key: "getSelection", + key: 'isOverlappingWith', /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge */ - value: function getSelection() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return { nodes: nodeIds, edges: edgeIds }; - } - }, { - key: "getSelectedNodes", + value: function isOverlappingWith(obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. - */ - value: function getSelectedNodes() { - var idArray = []; - if (this.options.selectable === true) { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); - } - } + var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return dist < distMax; + } else { + return false; } - return idArray; } }, { - key: "getSelectedEdges", + key: '_rotateForLabelAlignment', /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx + * @private */ - value: function getSelectedEdges() { - var idArray = []; - if (this.options.selectable === true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } - } + value: function _rotateForLabelAlignment(ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); + + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; } - return idArray; + + ctx.rotate(angleInDegrees); } }, { - key: "selectNodes", + key: '_pointOnCircle', /** - * select zero or more nodes with the option to highlight edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - * @param {boolean} [highlightEdges] + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private */ - value: function selectNodes(selection) { - var highlightEdges = arguments[1] === undefined ? true : arguments[1]; - - var i = undefined, - id = undefined; - - if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; - - // first unselect any selected node - this.unselectAll(); - - for (i = 0; i < selection.length; i++) { - id = selection[i]; - - var node = this.body.nodes[id]; - if (!node) { - throw new RangeError("Node with id \"" + id + "\" not found"); - } - this.selectObject(node, highlightEdges); - } - this.body.emitter.emit("_requestRedraw"); + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; } }, { - key: "selectEdges", + key: 'select', + value: function select() { + this.selected = true; + } + }, { + key: 'unselect', + value: function unselect() { + this.selected = false; + } + }], [{ + key: 'parseOptions', + value: function parseOptions(parentOptions, newOptions) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - value: function selectEdges(selection) { - var i = undefined, - id = undefined; + var fields = ['id', 'from', 'hidden', 'hoverWidth', 'label', 'labelHighlightBold', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; - if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; + // only deep extend the items in the field array. These do not have shorthand. + util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); - // first unselect any selected objects - this.unselectAll(); + util.mergeOptions(parentOptions, newOptions, 'smooth'); + util.mergeOptions(parentOptions, newOptions, 'shadow'); - for (i = 0; i < selection.length; i++) { - id = selection[i]; + if (newOptions.dashes !== undefined && newOptions.dashes !== null) { + parentOptions.dashes = newOptions.dashes; + } else if (allowDeletion === true && newOptions.dashes === null) { + parentOptions.dashes = undefined; + delete parentOptions.dashes; + } - var edge = this.body.edges[id]; - if (!edge) { - throw new RangeError("Edge with id \"" + id + "\" not found"); + // set the scaling newOptions + if (newOptions.scaling !== undefined && newOptions.scaling !== null) { + if (newOptions.scaling.min !== undefined) { + parentOptions.scaling.min = newOptions.scaling.min; } - this.selectObject(edge); + if (newOptions.scaling.max !== undefined) { + parentOptions.scaling.max = newOptions.scaling.max; + } + util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); + } else if (allowDeletion === true && newOptions.scaling === null) { + parentOptions.scaling = undefined; + delete parentOptions.scaling; } - this.body.emitter.emit("_requestRedraw"); - } - }, { - key: "updateSelection", - /** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ - value: function updateSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.body.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; + // hanlde multiple input cases for arrows + if (newOptions.arrows !== undefined && newOptions.arrows !== null) { + if (typeof newOptions.arrows === 'string') { + var arrows = newOptions.arrows.toLowerCase(); + if (arrows.indexOf('to') != -1) { + parentOptions.arrows.to.enabled = true; + } + if (arrows.indexOf('middle') != -1) { + parentOptions.arrows.middle.enabled = true; + } + if (arrows.indexOf('from') != -1) { + parentOptions.arrows.from.enabled = true; } + } else if (typeof newOptions.arrows === 'object') { + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to'); + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle'); + util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from'); + } else { + throw new Error('The arrow newOptions can only be an object or a string. Refer to the documentation. You used:' + JSON.stringify(newOptions.arrows)); } + } else if (allowDeletion === true && newOptions.arrows === null) { + parentOptions.arrows = undefined; + delete parentOptions.arrows; } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.body.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; + + // hanlde multiple input cases for color + if (newOptions.color !== undefined && newOptions.color !== null) { + if (util.isString(newOptions.color)) { + parentOptions.color.color = newOptions.color; + parentOptions.color.highlight = newOptions.color; + parentOptions.color.hover = newOptions.color; + parentOptions.color.inherit = false; + } else { + var colorsDefined = false; + if (newOptions.color.color !== undefined) { + parentOptions.color.color = newOptions.color.color;colorsDefined = true; + } + if (newOptions.color.highlight !== undefined) { + parentOptions.color.highlight = newOptions.color.highlight;colorsDefined = true; + } + if (newOptions.color.hover !== undefined) { + parentOptions.color.hover = newOptions.color.hover;colorsDefined = true; + } + if (newOptions.color.inherit !== undefined) { + parentOptions.color.inherit = newOptions.color.inherit; + } + if (newOptions.color.opacity !== undefined) { + parentOptions.color.opacity = Math.min(1, Math.max(0, newOptions.color.opacity)); + } + + if (newOptions.color.inherit === undefined && colorsDefined === true) { + parentOptions.color.inherit = false; } } + } else if (allowDeletion === true && newOptions.color === null) { + parentOptions.color = undefined; + delete parentOptions.color; + } + + // handle the font settings + if (newOptions.font !== undefined) { + _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); } } }]); - return SelectionHandler; + return Edge; })(); - exports["default"] = SelectionHandler; - module.exports = exports["default"]; + exports['default'] = Edge; + module.exports = exports['default']; /***/ }, /* 4 */ @@ -1123,19032 +1224,17886 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var Hammer = __webpack_require__(5); - var util = __webpack_require__(9); + Object.defineProperty(exports, '__esModule', { + value: true + }); - /** - * @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 || {}; - - this.selected = false; - this.displayed = false; - this.dirty = true; + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - this.top = null; - this.left = null; - this.width = null; - this.height = null; + function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } - this.editable = null; - if (this.data && this.data.hasOwnProperty('editable') && typeof this.data.editable === 'boolean') { - this.editable = data.editable; - } - } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - Item.prototype.stack = true; + var util = __webpack_require__(14); - /** - * Select current item - */ - Item.prototype.select = function () { - this.selected = true; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + var Label = (function () { + function Label(body, options) { + _classCallCheck(this, Label); - /** - * Unselect current item - */ - Item.prototype.unselect = function () { - this.selected = false; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + this.body = body; - /** - * 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) { - var groupChanged = data.group != undefined && this.data.group != data.group; - if (groupChanged) { - this.parent.itemSet._moveToGroup(this, data.group); + this.pointToSelf = false; + this.baseSize = undefined; + this.setOptions(options); + this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached } - if (data.hasOwnProperty('editable') && typeof data.editable === 'boolean') { - this.editable = data.editable; - } + _createClass(Label, [{ + key: 'setOptions', + value: function setOptions(options) { + var allowDeletion = arguments[1] === undefined ? false : arguments[1]; - this.data = data; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + this.options = options; - /** - * 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(); + if (options.label !== undefined) { + this.labelDirty = true; + } + + if (options.font !== undefined) { + Label.parseOptions(this.options.font, options, allowDeletion); + if (typeof options.font === 'string') { + this.baseSize = this.options.font.size; + } else if (typeof options.font === 'object') { + if (options.font.size !== undefined) { + this.baseSize = options.font.size; + } + } + } } - } else { - this.parent = parent; - } - }; + }, { + key: 'draw', - /** - * 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; - }; + /** + * Main function. This is called from anything that wants to draw a label. + * @param ctx + * @param x + * @param y + * @param selected + * @param baseline + */ + value: function draw(ctx, x, y, selected) { + var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed - */ - Item.prototype.show = function () { - return false; - }; + // if no label, return + if (this.options.label === undefined) return; - /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed - */ - Item.prototype.hide = function () { - return false; - }; + // check if we have to render the label + var viewFontSize = this.options.font.size * this.body.view.scale; + if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) return; - /** - * Repaint the item - */ - Item.prototype.redraw = function () {}; + // update the size cache if required + this.calculateLabelSize(ctx, selected, x, y, baseline); - /** - * Reposition the Item horizontally - */ - Item.prototype.repositionX = function () {}; + // create the fontfill background + this._drawBackground(ctx); + // draw text + this._drawText(ctx, selected, x, y, baseline); + } + }, { + key: '_drawBackground', - /** - * Reposition the Item vertically - */ - Item.prototype.repositionY = function () {}; + /** + * Draws the label background + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawBackground(ctx) { + if (this.options.font.background !== undefined && this.options.font.background !== 'none') { + ctx.fillStyle = this.options.font.background; - /** - * 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) { - var editable = (this.options.editable.remove || this.data.editable === true) && this.data.editable !== false; + var lineMargin = 2; - if (this.selected && editable && !this.dom.deleteButton) { - // create and show button - var me = this; + switch (this.options.font.align) { + case 'middle': + ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); + break; + case 'top': + ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); + break; + case 'bottom': + ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); + break; + default: + ctx.fillRect(this.size.left, this.size.top - 0.5 * lineMargin, this.size.width, this.size.height); + break; + } + } + } + }, { + key: '_drawText', - var deleteButton = document.createElement('div'); - deleteButton.className = 'vis-delete'; - deleteButton.title = 'Delete this item'; + /** + * + * @param ctx + * @param x + * @param baseline + * @private + */ + value: function _drawText(ctx, selected, x, y) { + var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - // TODO: be able to destroy the delete button - new Hammer(deleteButton).on('tap', function (event) { - event.stopPropagation(); - me.parent.removeFromDataSet(me); - }); + var fontSize = this.options.font.size; + var viewFontSize = fontSize * this.body.view.scale; + // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) + if (viewFontSize >= this.options.scaling.label.maxVisible) { + fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; + } - 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; - } - }; + var yLine = this.size.yLine; - /** - * 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; - } + var _getColor2 = this._getColor(viewFontSize); - var changed = this._contentToString(this.content) !== this._contentToString(content); - if (changed) { - // 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); - } - } + var _getColor22 = _slicedToArray(_getColor2, 2); - this.content = content; - } - }; + var fontColor = _getColor22[0]; + var strokeColor = _getColor22[1]; - /** - * 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 || ''; - } else { - element.removeAttribute('vis-title'); - } - }; + var _setAlignment2 = this._setAlignment(ctx, x, yLine, baseline); - /** - * 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 = []; + var _setAlignment22 = _slicedToArray(_setAlignment2, 2); - if (Array.isArray(this.options.dataAttributes)) { - attributes = this.options.dataAttributes; - } else if (this.options.dataAttributes == 'all') { - attributes = Object.keys(this.data); - } else { - return; - } + x = _setAlignment22[0]; + yLine = _setAlignment22[1]; - for (var i = 0; i < attributes.length; i++) { - var name = attributes[i]; - var value = this.data[name]; + // configure context for drawing the text + ctx.font = (selected && this.options.labelHighlightBold ? 'bold ' : '') + fontSize + 'px ' + this.options.font.face; + ctx.fillStyle = fontColor; + ctx.textAlign = 'center'; - if (value != null) { - element.setAttribute('data-' + name, value); + // set the strokeWidth + if (this.options.font.strokeWidth > 0) { + ctx.lineWidth = this.options.font.strokeWidth; + ctx.strokeStyle = strokeColor; + ctx.lineJoin = 'round'; + } + + // draw the text + for (var i = 0; i < this.lineCount; i++) { + if (this.options.font.strokeWidth > 0) { + ctx.strokeText(this.lines[i], x, yLine); + } + ctx.fillText(this.lines[i], x, yLine); + yLine += fontSize; + } + } + }, { + key: '_setAlignment', + value: function _setAlignment(ctx, x, yLine, baseline) { + // check for label alignment (for edges) + // TODO: make alignment for nodes + if (this.options.font.align !== 'horizontal' && this.pointToSelf === false) { + x = 0; + yLine = 0; + + var lineMargin = 2; + if (this.options.font.align === 'top') { + ctx.textBaseline = 'alphabetic'; + yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers + } else if (this.options.font.align === 'bottom') { + ctx.textBaseline = 'hanging'; + yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers + } else { + ctx.textBaseline = 'middle'; + } } else { - element.removeAttribute('data-' + name); + ctx.textBaseline = baseline; } + + return [x, yLine]; } - } - }; + }, { + key: '_getColor', - /** - * 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; - } + /** + * fade in when relative scale is between threshold and threshold - 1. + * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. + * + * @param viewFontSize + * @returns {*[]} + * @private + */ + value: function _getColor(viewFontSize) { + var fontColor = this.options.font.color || '#000000'; + var strokeColor = this.options.font.strokeColor || '#ffffff'; + if (viewFontSize <= this.options.scaling.label.drawThreshold) { + var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); + fontColor = util.overrideOpacity(fontColor, opacity); + strokeColor = util.overrideOpacity(strokeColor, opacity); + } + return [fontColor, strokeColor]; + } + }, { + key: 'getTextSize', - // append new styles - if (this.data.style) { - util.addCssText(element, this.data.style); - this.style = this.data.style; - } - }; + /** + * + * @param ctx + * @param selected + * @returns {{width: number, height: number}} + */ + value: function getTextSize(ctx) { + var selected = arguments[1] === undefined ? false : arguments[1]; - /** - * Stringify the items contents - * @param {string | Element | undefined} content - * @returns {string | undefined} - * @private - */ - Item.prototype._contentToString = function (content) { - if (typeof content === 'string') return content; - if (content && 'outerHTML' in content) return content.outerHTML; - return content; - }; + var size = { + width: this._processLabel(ctx, selected), + height: this.options.font.size * this.lineCount, + lineCount: this.lineCount + }; + return size; + } + }, { + key: 'calculateLabelSize', - /** - * Return the width of the item left from its start date - * @return {number} - */ - Item.prototype.getWidthLeft = function () { - return 0; - }; + /** + * + * @param ctx + * @param selected + * @param x + * @param y + * @param baseline + */ + value: function calculateLabelSize(ctx, selected) { + var x = arguments[2] === undefined ? 0 : arguments[2]; + var y = arguments[3] === undefined ? 0 : arguments[3]; + var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; - /** - * Return the width of the item right from the max of its start and end date - * @return {number} - */ - Item.prototype.getWidthRight = function () { - return 0; - }; + if (this.labelDirty === true) { + this.size.width = this._processLabel(ctx, selected); + } + this.size.height = this.options.font.size * this.lineCount; + this.size.left = x - this.size.width * 0.5; + this.size.top = y - this.size.height * 0.5; + this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.options.font.size; + if (baseline === 'hanging') { + this.size.top += 0.5 * this.options.font.size; + this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers + this.size.yLine += 4; // distance from node + } - module.exports = Item; + this.labelDirty = false; + } + }, { + key: '_processLabel', - // should be implemented by the item + /** + * This calculates the width as well as explodes the label string and calculates the amount of lines. + * @param ctx + * @param selected + * @returns {number} + * @private + */ + value: function _processLabel(ctx, selected) { + var width = 0; + var lines = ['']; + var lineCount = 0; + if (this.options.label !== undefined) { + lines = String(this.options.label).split('\n'); + lineCount = lines.length; + ctx.font = (selected && this.options.labelHighlightBold ? 'bold ' : '') + this.options.font.size + 'px ' + this.options.font.face; + width = ctx.measureText(lines[0]).width; + for (var i = 1; i < lineCount; i++) { + var lineWidth = ctx.measureText(lines[i]).width; + width = lineWidth > width ? lineWidth : width; + } + } + this.lines = lines; + this.lineCount = lineCount; - // should be implemented by the item + return width; + } + }], [{ + key: 'parseOptions', + value: function parseOptions(parentOptions, newOptions) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - // should be implemented by the item + if (typeof newOptions.font === 'string') { + var newOptionsArray = newOptions.font.split(' '); + parentOptions.size = newOptionsArray[0].replace('px', ''); + parentOptions.face = newOptionsArray[1]; + parentOptions.color = newOptionsArray[2]; + } else if (typeof newOptions.font === 'object') { + util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); + } + parentOptions.size = Number(parentOptions.size); + } + }]); + + return Label; + })(); + + exports['default'] = Label; + module.exports = exports['default']; /***/ }, /* 5 */ /***/ function(module, exports, __webpack_require__) { - // Only load hammer.js when in a browser environment - // (loading hammer.js in a node.js environment gives errors) - 'use strict'; + "use strict"; - if (typeof window !== 'undefined') { - var propagating = __webpack_require__(6); - var Hammer = window['Hammer'] || __webpack_require__(7); - module.exports = propagating(Hammer, { - preventDefault: 'mouse' - }); - } else { - module.exports = function () { - throw Error('hammer.js is only available in a browser, not in node.js.'); - }; - } + Object.defineProperty(exports, "__esModule", { + value: true + }); -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - (function (factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - window.propagating = factory(); - } - }(function () { - var _firstTarget = null; // singleton, will contain the target element where the touch event started - var _processing = false; // singleton, true when a touch event is being handled + var Node = __webpack_require__(67); + var Edge = __webpack_require__(3); + var util = __webpack_require__(14); - /** - * Extend an Hammer.js instance with event propagation. - * - * Features: - * - Events emitted by hammer will propagate in order from child to parent - * elements. - * - Events are extended with a function `event.stopPropagation()` to stop - * propagation to parent elements. - * - An option `preventDefault` to stop all default browser behavior. - * - * Usage: - * var hammer = propagatingHammer(new Hammer(element)); - * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true}); - * - * @param {Hammer.Manager} hammer An hammer instance. - * @param {Object} [options] Available options: - * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`. - * Enforce preventing the default browser behavior. - * Cannot be set to `false`. - * @return {Hammer.Manager} Returns the same hammer instance with extended - * functionality - */ - return function propagating(hammer, options) { - var _options = options || { - preventDefault: false + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { + var _this = this; + + _classCallCheck(this, SelectionHandler); + + this.body = body; + this.canvas = canvas; + this.selectionObj = { nodes: [], edges: [] }; + this.hoverObj = { nodes: {}, edges: {} }; + + this.options = {}; + this.defaultOptions = { + multiselect: false, + selectable: true, + selectConnectedEdges: true, + hoverConnectedEdges: true }; + util.extend(this.options, this.defaultOptions); - if (hammer.Manager) { - // This looks like the Hammer constructor. - // Overload the constructors with our own. - var Hammer = hammer; + this.body.emitter.on("_dataChanged", function () { + _this.updateSelection(); + }); + } - var PropagatingHammer = function(element, options) { - var o = Object.create(_options); - if (options) Hammer.extend(o, options); - return propagating(new Hammer(element, o), o); - }; - Hammer.extend(PropagatingHammer, Hammer); + _createClass(SelectionHandler, [{ + key: "setOptions", + value: function setOptions(options) { + if (options !== undefined) { + var fields = ["multiselect", "hoverConnectedEdges", "selectable", "selectConnectedEdges"]; + util.selectiveDeepExtend(fields, this.options, options); + } + } + }, { + key: "selectOnPoint", - PropagatingHammer.Manager = function (element, options) { - var o = Object.create(_options); - if (options) Hammer.extend(o, options); - return propagating(new Hammer.Manager(element, o), o); - }; + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + var selected = false; + if (this.options.selectable === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); - return PropagatingHammer; + // unselect after getting the objects in order to restore width and height. + this.unselectAll(); + + if (obj !== undefined) { + selected = this.selectObject(obj); + } + this.body.emitter.emit("_requestRedraw"); + } + return selected; } + }, { + key: "selectAdditionalOnPoint", + value: function selectAdditionalOnPoint(pointer) { + var selectionChanged = false; + if (this.options.selectable === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer); - // create a wrapper object which will override the functions - // `on`, `off`, `destroy`, and `emit` of the hammer instance - var wrapper = Object.create(hammer); + if (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); + } else { + this.selectObject(obj); + } - // attach to DOM element - var element = hammer.element; - element.hammer = wrapper; + this.body.emitter.emit("_requestRedraw"); + } + } + return selectionChanged; + } + }, { + key: "_generateClickEvent", + value: function _generateClickEvent(eventType, event, pointer, oldSelection) { + var emptySelection = arguments[4] === undefined ? false : arguments[4]; - // register an event to catch the start of a gesture and store the - // target in a singleton - hammer.on('hammer.input', function (event) { - if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) { - event.preventDefault(); + var properties = undefined; + if (emptySelection === true) { + properties = { nodes: [], edges: [] }; + } else { + properties = this.getSelection(); } - if (event.isFirst) { - _firstTarget = event.target; + properties["pointer"] = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: this.canvas.DOMtoCanvas(pointer) + }; + properties["event"] = event; + + if (oldSelection !== undefined) { + properties["previousSelection"] = oldSelection; } - }); + this.body.emitter.emit(eventType, properties); + } + }, { + key: "selectObject", + value: function selectObject(obj) { + var highlightEdges = arguments[1] === undefined ? this.options.selectConnectedEdges : arguments[1]; - /** @type {Object.>} */ - wrapper._handlers = {}; + if (obj !== undefined) { + if (obj instanceof Node) { + if (highlightEdges === true) { + this._selectConnectedEdges(obj); + } + } + obj.select(); + this._addToSelection(obj); + return true; + } + return false; + } + }, { + key: "deselectObject", + value: function deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); + } + } + }, { + key: "_getAllNodesOverlappingWith", /** - * Register a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} handler A callback function, called as handler(event) - * @returns {Hammer.Manager} Returns the hammer instance + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private */ - wrapper.on = function (events, handler) { - // register the handler - split(events).forEach(function (event) { - var _handlers = wrapper._handlers[event]; - if (!_handlers) { - wrapper._handlers[event] = _handlers = []; - - // register the static, propagated handler - hammer.on(event, propagatedHandler); + value: function _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; + var nodes = this.body.nodes; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); } - _handlers.push(handler); - }); + } + return overlappingNodes; + } + }, { + key: "_pointerToPositionObject", - return wrapper; - }; + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + value: function _pointerToPositionObject(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x - 1, + top: canvasPos.y + 1, + right: canvasPos.x + 1, + bottom: canvasPos.y - 1 + }; + } + }, { + key: "getNodeAt", /** - * Unregister a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} [handler] Optional. The registered handler. If not - * provided, all handlers for given events - * are removed. - * @returns {Hammer.Manager} Returns the hammer instance + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | undefined} node + * @private */ - wrapper.off = function (events, handler) { - // unregister the handler - split(events).forEach(function (event) { - var _handlers = wrapper._handlers[event]; - if (_handlers) { - _handlers = handler ? _handlers.filter(function (h) { - return h !== handler; - }) : []; + value: function getNodeAt(pointer) { + var returnNode = arguments[1] === undefined ? true : arguments[1]; - if (_handlers.length > 0) { - wrapper._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer.off(event, propagatedHandler); - delete wrapper._handlers[event]; - } + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + if (returnNode === true) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } else { + return overlappingNodes[overlappingNodes.length - 1]; } - }); + } else { + return undefined; + } + } + }, { + key: "_getEdgesOverlappingWith", - return wrapper; - }; + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var i = 0; i < this.body.edgeIndices.length; i++) { + var edgeId = this.body.edgeIndices[i]; + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + } + }, { + key: "_getAllEdgesOverlappingWith", /** - * Emit to the event listeners - * @param {string} eventType - * @param {Event} event + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private */ - wrapper.emit = function(eventType, event) { - _firstTarget = event.target; - hammer.emit(eventType, event); - }; + value: function _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; + } + }, { + key: "getEdgeAt", - wrapper.destroy = function () { - // Detach from DOM element - delete hammer.element.hammer; + /** + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {undefined} + * @private + */ + value: function getEdgeAt(pointer) { + var returnEdge = arguments[1] === undefined ? true : arguments[1]; - // clear all handlers - wrapper._handlers = {}; + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - // call original hammer destroy - hammer.destroy(); - }; + if (overlappingEdges.length > 0) { + if (returnEdge === true) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return overlappingEdges[overlappingEdges.length - 1]; + } + } else { + return undefined; + } + } + }, { + key: "_addToSelection", - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + value: function _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; + } } + }, { + key: "_addToHover", /** - * A static event handler, applying event propagation. - * @param {Object} event + * Add object to the selection array. + * + * @param obj + * @private */ - function propagatedHandler(event) { - // let only a single hammer instance handle this event - if (event.type !== 'hammer.input') { - // it is possible that the same srcEvent is used with multiple hammer events, - // we keep track on which events are handled in an object _handled - if (!event.srcEvent._handled) { - event.srcEvent._handled = {}; - } + value: function _addToHover(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; + } + } + }, { + key: "_removeFromSelection", - if (event.srcEvent._handled[event.type]) { - return; + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + value: function _removeFromSelection(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } else { + delete this.selectionObj.edges[obj.id]; + } + } + }, { + key: "unselectAll", + + /** + * Unselect all. The selectionObj is useful for this. + * + * @private + */ + value: function unselectAll() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); } - else { - event.srcEvent._handled[event.type] = true; + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); } } - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; - - // attach firstTarget property to the event - event.firstTarget = _firstTarget; + this.selectionObj = { nodes: {}, edges: {} }; + } + }, { + key: "_getSelectedNodeCount", - // propagate over all elements (until stopped) - var elem = _firstTarget; - while (elem && !stopped) { - var _handlers = elem.hammer && elem.hammer._handlers[event.type]; - if (_handlers) { - for (var i = 0; i < _handlers.length && !stopped; i++) { - _handlers[i](event); - } + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + value: function _getSelectedNodeCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; } - - elem = elem.parentNode; } + return count; } + }, { + key: "_getSelectedNode", - return wrapper; - }; - })); + /** + * return the selected node + * + * @returns {number} + * @private + */ + value: function _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return undefined; + } + }, { + key: "_getSelectedEdge", + /** + * return the selected edge + * + * @returns {number} + * @private + */ + value: function _getSelectedEdge() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return undefined; + } + }, { + key: "_getSelectedEdgeCount", -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { + /** + * return the number of selected edges + * + * @returns {number} + * @private + */ + value: function _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + } + }, { + key: "_getSelectedObjectCount", - var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ - * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ - (function(window, document, exportName, undefined) { - 'use strict'; + /** + * return the number of selected objects. + * + * @returns {number} + * @private + */ + value: function _getSelectedObjectCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + } + }, { + key: "_selectionIsEmpty", - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } + } + return true; + } + }, { + key: "_clusterInSelection", - var TYPE_FUNCTION = 'function'; + /** + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private + */ + value: function _clusterInSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } + } + } + return false; + } + }, { + key: "_selectConnectedEdges", - var round = Math.round; - var abs = Math.abs; - var now = Date.now; + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); + } + } + }, { + key: "_hoverConnectedEdges", - /** - * set a timeout with a given scope - * @param {Function} fn - * @param {Number} timeout - * @param {Object} context - * @returns {number} - */ - function setTimeoutContext(fn, timeout, context) { - return setTimeout(bindFn(fn, context), timeout); - } + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } + } + }, { + key: "_unselectConnectedEdges", - /** - * if the argument is an array, we want to execute the fn on each entry - * if it aint an array we don't want to do a thing. - * this is used by all the methods that accept a single and array argument. - * @param {*|Array} arg - * @param {String} fn - * @param {Object} [context] - * @returns {Boolean} - */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; + /** + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _unselectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); + } } - return false; - } + }, { + key: "blurObject", - /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context - */ - function each(obj, iterator, context) { - var i; + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function blurObject(object) { + if (object.hover === true) { + object.hover = false; + if (object instanceof Node) { + this.body.emitter.emit("blurNode", { node: object.id }); + } else { + this.body.emitter.emit("blurEdge", { edge: object.id }); + } + } + } + }, { + key: "hoverObject", - if (!obj) { - return; + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function hoverObject(object) { + var hoverChanged = false; + // remove all node hover highlights + for (var nodeId in this.hoverObj.nodes) { + if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + if (object === undefined) { + this.blurObject(this.hoverObj.nodes[nodeId]); + hoverChanged = true; + } else if (object instanceof Node && object.id != nodeId || object instanceof Edge || object === undefined) { + this.blurObject(this.hoverObj.nodes[nodeId]); + hoverChanged = true; + delete this.hoverObj.nodes[nodeId]; + } + } + } + + // removing all edge hover highlights + for (var edgeId in this.hoverObj.edges) { + if (this.hoverObj.edges.hasOwnProperty(edgeId)) { + this.hoverObj.edges[edgeId].hover = false; + delete this.hoverObj.edges[edgeId]; + } + } + + if (object !== undefined) { + if (object.hover === false) { + object.hover = true; + this._addToHover(object); + hoverChanged = true; + if (object instanceof Node) { + this.body.emitter.emit("hoverNode", { node: object.id }); + } else { + this.body.emitter.emit("hoverEdge", { edge: object.id }); + } + } + if (object instanceof Node && this.options.hoverConnectedEdges === true) { + this._hoverConnectedEdges(object); + } + } + + if (hoverChanged === true) { + this.body.emitter.emit("_requestRedraw"); + } } + }, { + key: "getSelection", - if (obj.forEach) { - obj.forEach(iterator, context); - } else if (obj.length !== undefined) { - i = 0; - while (i < obj.length) { - iterator.call(context, obj[i], i, obj); - i++; + /** + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection + */ + value: function getSelection() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; + } + }, { + key: "getSelectedNodes", + + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedNodes() { + var idArray = []; + if (this.options.selectable === true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } } - } else { - for (i in obj) { - obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); + } + return idArray; + } + }, { + key: "getSelectedEdges", + + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedEdges() { + var idArray = []; + if (this.options.selectable === true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } } + } + return idArray; } - } + }, { + key: "selectNodes", - /** - * extend object. - * means that properties in dest will be overwritten by the ones in src. - * @param {Object} dest - * @param {Object} src - * @param {Boolean} [merge] - * @returns {Object} dest - */ - function extend(dest, src, merge) { - var keys = Object.keys(src); - var i = 0; - while (i < keys.length) { - if (!merge || (merge && dest[keys[i]] === undefined)) { - dest[keys[i]] = src[keys[i]]; + /** + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] + */ + value: function selectNodes(selection) { + var highlightEdges = arguments[1] === undefined ? true : arguments[1]; + + var i = undefined, + id = undefined; + + if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; + + // first unselect any selected node + this.unselectAll(); + + for (i = 0; i < selection.length; i++) { + id = selection[i]; + + var node = this.body.nodes[id]; + if (!node) { + throw new RangeError("Node with id \"" + id + "\" not found"); } - i++; + this.selectObject(node, highlightEdges); + } + this.body.emitter.emit("_requestRedraw"); } - return dest; - } + }, { + key: "selectEdges", - /** - * merge the values from src in the dest. - * means that properties that exist in dest will not be overwritten by src - * @param {Object} dest - * @param {Object} src - * @returns {Object} dest - */ - function merge(dest, src) { - return extend(dest, src, true); - } + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + value: function selectEdges(selection) { + var i = undefined, + id = undefined; - /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] - */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; + if (!selection || selection.length === undefined) throw "Selection must be an array with ids"; - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; + // first unselect any selected objects + this.unselectAll(); - if (properties) { - extend(childP, properties); + for (i = 0; i < selection.length; i++) { + id = selection[i]; + + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError("Edge with id \"" + id + "\" not found"); + } + this.selectObject(edge); + } + this.body.emitter.emit("_requestRedraw"); } - } + }, { + key: "updateSelection", - /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} - */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } + /** + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ + value: function updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } + } + } + }]); + + return SelectionHandler; + })(); + + exports["default"] = SelectionHandler; + module.exports = exports["default"]; + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var Item = __webpack_require__(8); /** - * let a boolean value also be a function that must return a boolean - * this first item in args will be used as the context - * @param {Boolean|Function} val - * @param {Array} [args] - * @returns {Boolean} + * @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 boolOrFn(val, args) { - if (typeof val == TYPE_FUNCTION) { - return val.apply(args ? args[0] || undefined : undefined, args); + function PointItem(data, conversion, options) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 } - return val; - } + }; - /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} - */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; + // 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); + /** - * addEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * 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 */ - function addEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.addEventListener(type, handler, false); - }); - } + 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; + }; /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler + * Repaint the item */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } - - /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found - */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; - } - - /** - * small indexOf wrapper - * @param {String} str - * @param {String} find - * @returns {Boolean} found - */ - function inStr(str, find) { - return str.indexOf(find) > -1; - } - - /** - * split string on whitespace - * @param {String} str - * @returns {Array} words - */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } - - /** - * find if a array contains the object using indexOf or a simple polyFill - * @param {Array} src - * @param {String} find - * @param {String} [findByKey] - * @return {Boolean|Number} false when not found, or the index - */ - function inArray(src, find, findByKey) { - if (src.indexOf && !findByKey) { - return src.indexOf(find); - } else { - var i = 0; - while (i < src.length) { - if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { - return i; - } - i++; - } - return -1; - } - } - - /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} - */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } - - /** - * unique array with objects based on a key (like 'id') or just by the array's value - * @param {Array} src [{id:1},{id:2},{id:1}] - * @param {String} [key] - * @param {Boolean} [sort=False] - * @returns {Array} [{id:1},{id:2}] - */ - function uniqueArray(src, key, sort) { - var results = []; - var values = []; - var i = 0; + PointItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - while (i < src.length) { - var val = key ? src[i][key] : src[i]; - if (inArray(values, val) < 0) { - results.push(src[i]); - } - values[i] = val; - i++; - } + // background box + dom.point = document.createElement('div'); + // className is updated in redraw() - if (sort) { - if (!key) { - results = results.sort(); - } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); - } - } + // contents box, right from the dot + dom.content = document.createElement('div'); + dom.content.className = 'vis-item-content'; + dom.point.appendChild(dom.content); - return results; - } + // dot at start + dom.dot = document.createElement('div'); + dom.point.appendChild(dom.dot); - /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed - */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); + // attach this item as attribute + dom.point['timeline-item'] = this; - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; + this.dirty = true; + } - if (prop in obj) { - return prop; - } - i++; + // 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'); } - return undefined; - } - - /** - * get a unique id - * @returns {number} uniqueId - */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } + foreground.appendChild(dom.point); + } + this.displayed = true; - /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} - */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } + // 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); - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; - var SUPPORT_TOUCH = ('ontouchstart' in window); - var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; - var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); + // update class + var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); + dom.point.className = 'vis-item vis-point' + className; + dom.dot.className = 'vis-item vis-dot' + className; - var INPUT_TYPE_TOUCH = 'touch'; - var INPUT_TYPE_PEN = 'pen'; - var INPUT_TYPE_MOUSE = 'mouse'; - var INPUT_TYPE_KINECT = 'kinect'; + // recalculate size of dot and contents + this.props.dot.width = dom.dot.offsetWidth; + this.props.dot.height = dom.dot.offsetHeight; + this.props.content.height = dom.content.offsetHeight; - var COMPUTE_INTERVAL = 25; + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - var INPUT_START = 1; - var INPUT_MOVE = 2; - var INPUT_END = 4; - var INPUT_CANCEL = 8; + dom.dot.style.top = (this.height - this.props.dot.height) / 2 + 'px'; + dom.dot.style.left = this.props.dot.width / 2 + 'px'; - var DIRECTION_NONE = 1; - var DIRECTION_LEFT = 2; - var DIRECTION_RIGHT = 4; - var DIRECTION_UP = 8; - var DIRECTION_DOWN = 16; + // recalculate size + this.width = dom.point.offsetWidth; + this.height = dom.point.offsetHeight; - var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; - var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; - var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + this.dirty = false; + } - var PROPS_XY = ['x', 'y']; - var PROPS_CLIENT_XY = ['clientX', 'clientY']; + this._repaintDeleteButton(dom.point); + }; /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} - * @constructor + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - function Input(manager, callback) { - var self = this; - this.manager = manager; - this.callback = callback; - this.element = manager.element; - this.target = manager.options.inputTarget; - - // smaller wrapper around the handler, for the scope and the enabled state of the manager, - // so when disabled the input events are completely bypassed. - this.domHandler = function(ev) { - if (boolOrFn(manager.options.enable, [manager])) { - self.handler(ev); - } - }; - - this.init(); - - } - - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, - - /** - * bind the events - */ - init: function() { - this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); - }, - - /** - * unbind the events - */ - destroy: function() { - this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); - } + PointItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); + } }; /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} + * Hide the item from the DOM (when visible) */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; - - if (inputClass) { - Type = inputClass; - } else if (SUPPORT_POINTER_EVENTS) { - Type = PointerEventInput; - } else if (SUPPORT_ONLY_TOUCH) { - Type = TouchInput; - } else if (!SUPPORT_TOUCH) { - Type = MouseInput; - } else { - Type = TouchMouseInput; + PointItem.prototype.hide = function () { + if (this.displayed) { + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); } - return new (Type)(manager, inputHandler); - } + + this.displayed = false; + } + }; /** - * handle input events - * @param {Manager} manager - * @param {String} eventType - * @param {Object} input + * Reposition the item horizontally + * @Override */ - function inputHandler(manager, eventType, input) { - var pointersLen = input.pointers.length; - var changedPointersLen = input.changedPointers.length; - var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); - var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); - - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; - - if (isFirst) { - manager.session = {}; - } - - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; - - // compute scale, rotation etc - computeInputData(manager, input); + PointItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); - // emit secret event - manager.emit('hammer.input', input); + this.left = start - this.props.dot.width; - manager.recognize(input); - manager.session.prevInput = input; - } + // reposition point + this.dom.point.style.left = this.left + 'px'; + }; /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input + * Reposition the item vertically + * @Override */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; - - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } - - // to compute scale and rotation we need to store the multiple touches - if (pointersLength > 1 && !session.firstMultiple) { - session.firstMultiple = simpleCloneInputData(input); - } else if (pointersLength === 1) { - session.firstMultiple = false; - } - - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - - var center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; - - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); - - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); - - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - - computeIntervalInputData(session, input); - - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; - } - input.target = target; - } - - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; - - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; - - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } + PointItem.prototype.repositionY = function () { + var orientation = this.options.orientation.item; + var point = this.dom.point; - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } + if (orientation == 'top') { + point.style.top = this.top + 'px'; + } else { + point.style.top = this.parent.height - this.top - this.height + 'px'; + } + }; /** - * velocity is calculated every x ms - * @param {Object} session - * @param {Object} input + * Return the width of the item left from its start date + * @return {number} */ - function computeIntervalInputData(session, input) { - var last = session.lastInterval || input, - deltaTime = input.timeStamp - last.timeStamp, - velocity, velocityX, velocityY, direction; - - if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; - - var v = getVelocity(deltaTime, deltaX, deltaY); - velocityX = v.x; - velocityY = v.y; - velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; - direction = getDirection(deltaX, deltaY); - - session.lastInterval = input; - } else { - // use latest velocity info if it doesn't overtake a minimum period - velocity = last.velocity; - velocityX = last.velocityX; - velocityY = last.velocityY; - direction = last.direction; - } - - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } + PointItem.prototype.getWidthLeft = function () { + return this.props.dot.width; + }; /** - * create a simple clone from the input used for storage of firstInput and firstMultiple - * @param {Object} input - * @returns {Object} clonedInputData + * Return the width of the item right from its start date + * @return {number} */ - function simpleCloneInputData(input) { - // make a simple copy of the pointers because we will get a reference if we don't - // we only need clientXY for the calculations - var pointers = []; - var i = 0; - while (i < input.pointers.length) { - pointers[i] = { - clientX: round(input.pointers[i].clientX), - clientY: round(input.pointers[i].clientY) - }; - i++; - } + PointItem.prototype.getWidthRight = function () { + return this.width - this.props.dot.width; + }; - return { - timeStamp: now(), - pointers: pointers, - center: getCenter(pointers), - deltaX: input.deltaX, - deltaY: input.deltaY - }; - } + module.exports = PointItem; + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { /** - * get the center of all the pointers - * @param {Array} pointers - * @return {Object} center contains `x` and `y` properties + * This object contains all possible options. It will check if the types are correct, if required if the option is one + * of the allowed values. + * + * __any__ means that the name of the property does not matter. + * __type__ is a required field for all objects and contains the allowed types of all objects */ - function getCenter(pointers) { - var pointersLength = pointers.length; - - // no need to loop when only one touch - if (pointersLength === 1) { - return { - x: round(pointers[0].clientX), - y: round(pointers[0].clientY) - }; - } + 'use strict'; - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; - } - - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } - - /** - * calculate the velocity between two points. unit is in px per ms. - * @param {Number} deltaTime - * @param {Number} x - * @param {Number} y - * @return {Object} velocity `x` and `y` - */ - function getVelocity(deltaTime, x, y) { - return { - x: x / deltaTime || 0, - y: y / deltaTime || 0 - }; - } - - /** - * get the direction between two points - * @param {Number} x - * @param {Number} y - * @return {Number} direction - */ - function getDirection(x, y) { - if (x === y) { - return DIRECTION_NONE; - } - - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } - - /** - * calculate the absolute distance between two points - * @param {Object} p1 {x, y} - * @param {Object} p2 {x, y} - * @param {Array} [props] containing x and y keys - * @return {Number} distance - */ - function getDistance(p1, p2, props) { - if (!props) { - props = PROPS_XY; - } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - - return Math.sqrt((x * x) + (y * y)); - } - - /** - * calculate the angle between two coordinates - * @param {Object} p1 - * @param {Object} p2 - * @param {Array} [props] containing x and y keys - * @return {Number} angle - */ - function getAngle(p1, p2, props) { - if (!props) { - props = PROPS_XY; - } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - return Math.atan2(y, x) * 180 / Math.PI; - } - - /** - * calculate the rotation degrees between two pointersets - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} rotation - */ - function getRotation(start, end) { - return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); - } - - /** - * calculate the scale factor between two pointersets - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} scale - */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } - - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END - }; - - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - - /** - * Mouse events input - * @constructor - * @extends Input - */ - function MouseInput() { - this.evEl = MOUSE_ELEMENT_EVENTS; - this.evWin = MOUSE_WINDOW_EVENTS; - - this.allow = true; // used by Input.TouchMouse to disable mouse events - this.pressed = false; // mousedown state - - Input.apply(this, arguments); - } - - inherit(MouseInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; - - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } - - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } - - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; - } - - if (eventType & INPUT_END) { - this.pressed = false; - } - - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); - } - }); - - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL - }; - - // in IE10 the pointer types is defined as an enum - var IE10_POINTER_TYPE_ENUM = { - 2: INPUT_TYPE_TOUCH, - 3: INPUT_TYPE_PEN, - 4: INPUT_TYPE_MOUSE, - 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 - }; - - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } - - /** - * Pointer events input - * @constructor - * @extends Input - */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; - - Input.apply(this, arguments); - - this.store = (this.manager.session.pointerEvents = []); - } - - inherit(PointerEventInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function PEhandler(ev) { - var store = this.store; - var removePointer = false; - - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - - var isTouch = (pointerType == INPUT_TYPE_TOUCH); - - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - - // start and mouse must be down - if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { - if (storeIndex < 0) { - store.push(ev); - storeIndex = store.length - 1; - } - } else if (eventType & (INPUT_END | INPUT_CANCEL)) { - removePointer = true; - } - - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; - } - - // update the event in the store - store[storeIndex] = ev; - - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); - - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); - } - } - }); - - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; - - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - - /** - * Touch events input - * @constructor - * @extends Input - */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; - - Input.apply(this, arguments); - } - - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; - } - - if (!this.started) { - return; - } - - var touches = normalizeSingleTouches.call(this, ev, type); - - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; - } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); - - /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] - */ - function normalizeSingleTouches(ev, type) { - var all = toArray(ev.touches); - var changed = toArray(ev.changedTouches); - - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); - } - - return [all, changed]; - } - - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; - - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - - /** - * Multi-user touch events input - * @constructor - * @extends Input - */ - function TouchInput() { - this.evTarget = TOUCH_TARGET_EVENTS; - this.targetIds = {}; - - Input.apply(this, arguments); - } - - inherit(TouchInput, Input, { - handler: function MTEhandler(ev) { - var type = TOUCH_INPUT_MAP[ev.type]; - var touches = getTouches.call(this, ev, type); - if (!touches) { - return; - } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); - - /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] - */ - function getTouches(ev, type) { - var allTouches = toArray(ev.touches); - var targetIds = this.targetIds; - - // when there is only one touch, the process can be simplified - if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { - targetIds[allTouches[0].identifier] = true; - return [allTouches, allTouches]; - } - - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; - - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); - }); - - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; - } - } - - // filter changed touches to only contain touches that exist in the collected target ids - i = 0; - while (i < changedTouches.length) { - if (targetIds[changedTouches[i].identifier]) { - changedTargetTouches.push(changedTouches[i]); - } - - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; - } - i++; - } - - if (!changedTargetTouches.length) { - return; - } - - return [ - // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' - uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), - changedTargetTouches - ]; - } - - /** - * Combined touch and mouse input - * - * Touch has a higher priority then mouse, and while touching no mouse events are allowed. - * This because touch devices also emit mouse events while doing a touch. - * - * @constructor - * @extends Input - */ - function TouchMouseInput() { - Input.apply(this, arguments); - - var handler = bindFn(this.handler, this); - this.touch = new TouchInput(this.manager, handler); - this.mouse = new MouseInput(this.manager, handler); - } - - inherit(TouchMouseInput, Input, { - /** - * handle mouse and touch events - * @param {Hammer} manager - * @param {String} inputEvent - * @param {Object} inputData - */ - handler: function TMEhandler(manager, inputEvent, inputData) { - var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), - isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); - - // when we're in a touch event, so block all upcoming mouse events - // most mobile browser also emit mouseevents, right after touchstart - if (isTouch) { - this.mouse.allow = false; - } else if (isMouse && !this.mouse.allow) { - return; - } - - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } - - this.callback(manager, inputEvent, inputData); - }, - - /** - * remove the event listeners - */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); - } - }); - - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - - // magical touchAction value - var TOUCH_ACTION_COMPUTE = 'compute'; - var TOUCH_ACTION_AUTO = 'auto'; - var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented - var TOUCH_ACTION_NONE = 'none'; - var TOUCH_ACTION_PAN_X = 'pan-x'; - var TOUCH_ACTION_PAN_Y = 'pan-y'; - - /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor - */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); - } - - TouchAction.prototype = { - /** - * set the touchAction value on the element or enable the polyfill - * @param {String} value - */ - set: function(value) { - // find out the touch-action by the event handlers - if (value == TOUCH_ACTION_COMPUTE) { - value = this.compute(); - } - - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; - } - this.actions = value.toLowerCase().trim(); - }, - - /** - * just re-set the touchAction value - */ - update: function() { - this.set(this.manager.options.touchAction); - }, - - /** - * compute the value for the touchAction property based on the recognizer's settings - * @returns {String} value - */ - compute: function() { - var actions = []; - each(this.manager.recognizers, function(recognizer) { - if (boolOrFn(recognizer.options.enable, [recognizer])) { - actions = actions.concat(recognizer.getTouchAction()); - } - }); - return cleanTouchActions(actions.join(' ')); - }, - - /** - * this method is called on each input cycle and provides the preventing of the browser behavior - * @param {Object} input - */ - preventDefaults: function(input) { - // not needed with native support for the touchAction property - if (NATIVE_TOUCH_ACTION) { - return; - } - - var srcEvent = input.srcEvent; - var direction = input.offsetDirection; - - // if the touch action did prevented once this session - if (this.manager.session.prevented) { - srcEvent.preventDefault(); - return; - } - - var actions = this.actions; - var hasNone = inStr(actions, TOUCH_ACTION_NONE); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); - } - }, - - /** - * call preventDefault to prevent the browser's default behavior (scrolling in most cases) - * @param {Object} srcEvent - */ - preventSrc: function(srcEvent) { - this.manager.session.prevented = true; - srcEvent.preventDefault(); - } - }; - - /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} - */ - function cleanTouchActions(actions) { - // none - if (inStr(actions, TOUCH_ACTION_NONE)) { - return TOUCH_ACTION_NONE; - } - - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; - } - - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; - } - - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; - } - - return TOUCH_ACTION_AUTO; - } - - /** - * Recognizer flow explained; * - * All recognizers have the initial state of POSSIBLE when a input session starts. - * The definition of a input session is from the first input until the last input, with all it's movement in it. * - * Example session for mouse-input: mousedown -> mousemove -> mouseup - * - * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed - * which determines with state it should be. - * - * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to - * POSSIBLE to give it another change on the next cycle. - * - * Possible - * | - * +-----+---------------+ - * | | - * +-----+-----+ | - * | | | - * Failed Cancelled | - * +-------+------+ - * | | - * Recognized Began - * | - * Changed - * | - * Ended/Recognized - */ - var STATE_POSSIBLE = 1; - var STATE_BEGAN = 2; - var STATE_CHANGED = 4; - var STATE_ENDED = 8; - var STATE_RECOGNIZED = STATE_ENDED; - var STATE_CANCELLED = 16; - var STATE_FAILED = 32; - - /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options - */ - function Recognizer(options) { - this.id = uniqueId(); - - this.manager = null; - this.options = merge(options || {}, this.defaults); - - // default is enable true - this.options.enable = ifUndefined(this.options.enable, true); - - this.state = STATE_POSSIBLE; - - this.simultaneous = {}; - this.requireFail = []; - } - - Recognizer.prototype = { - /** - * @virtual - * @type {Object} - */ - defaults: {}, - - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); - - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; - }, - - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; - } - - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); - } - return this; - }, - - /** - * drop the simultaneous link. it doesnt remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRecognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { - return this; - } - - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; - }, - - /** - * recognizer can only run when an other is failing - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - requireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { - return this; - } - - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; - }, - - /** - * drop the requireFailure link. it does not remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRequireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { - return this; - } - - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); - } - return this; - }, - - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; - }, - - /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} - */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; - }, - - /** - * You should use `tryEmit` instead of `emit` directly to check - * that all the needed recognizers has failed before emitting. - * @param {Object} input - */ - emit: function(input) { - var self = this; - var state = this.state; - - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } - - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); - } - - emit(); // simple 'eventName' events - - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); - } - }, - - /** - * Check that all the require failure recognizers has failed, - * if true, it emits a gesture event, - * otherwise, setup the state to FAILED. - * @param {Object} input - */ - tryEmit: function(input) { - if (this.canEmit()) { - return this.emit(input); - } - // it's failing anyway - this.state = STATE_FAILED; - }, - - /** - * can we emit? - * @returns {boolean} - */ - canEmit: function() { - var i = 0; - while (i < this.requireFail.length) { - if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { - return false; - } - i++; - } - return true; - }, - - /** - * update the recognizer - * @param {Object} inputData - */ - recognize: function(inputData) { - // make a new copy of the inputData - // so we can change the inputData without messing up the other recognizers - var inputDataClone = extend({}, inputData); - - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; - } - - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; - } - - this.state = this.process(inputDataClone); - - // the recognizer has recognized a gesture - // so trigger an event - if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { - this.tryEmit(inputDataClone); - } - }, - - /** - * return the state of the recognizer - * the actual recognizing happens in this method - * @virtual - * @param {Object} inputData - * @returns {Const} STATE - */ - process: function(inputData) { }, // jshint ignore:line - - /** - * return the preferred touch-action - * @virtual - * @returns {Array} - */ - getTouchAction: function() { }, - - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } - }; - - /** - * get a usable string, used as event postfix - * @param {Const} state - * @returns {String} state - */ - function stateStr(state) { - if (state & STATE_CANCELLED) { - return 'cancel'; - } else if (state & STATE_ENDED) { - return 'end'; - } else if (state & STATE_CHANGED) { - return 'move'; - } else if (state & STATE_BEGAN) { - return 'start'; - } - return ''; - } - - /** - * direction cons to string - * @param {Const} direction - * @returns {String} - */ - function directionStr(direction) { - if (direction == DIRECTION_DOWN) { - return 'down'; - } else if (direction == DIRECTION_UP) { - return 'up'; - } else if (direction == DIRECTION_LEFT) { - return 'left'; - } else if (direction == DIRECTION_RIGHT) { - return 'right'; - } - return ''; - } - - /** - * get a recognizer by name if it is bound to a manager - * @param {Recognizer|String} otherRecognizer - * @param {Recognizer} recognizer - * @returns {Recognizer} - */ - function getRecognizerByNameIfManager(otherRecognizer, recognizer) { - var manager = recognizer.manager; - if (manager) { - return manager.get(otherRecognizer); - } - return otherRecognizer; - } - - /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer - */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } - - inherit(AttrRecognizer, Recognizer, { - /** - * @namespace - * @memberof AttrRecognizer - */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, - - /** - * Used to check if it the recognizer receives valid input, like input.distance > 10. - * @memberof AttrRecognizer - * @param {Object} input - * @returns {Boolean} recognized - */ - attrTest: function(input) { - var optionPointers = this.options.pointers; - return optionPointers === 0 || input.pointers.length === optionPointers; - }, - - /** - * Process the input and return the state for the recognizer - * @memberof AttrRecognizer - * @param {Object} input - * @returns {*} State - */ - process: function(input) { - var state = this.state; - var eventType = input.eventType; - - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); - - // on cancel input and we've recognized before, return STATE_CANCELLED - if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { - return state | STATE_CANCELLED; - } else if (isRecognized || isValid) { - if (eventType & INPUT_END) { - return state | STATE_ENDED; - } else if (!(state & STATE_BEGAN)) { - return STATE_BEGAN; - } - return state | STATE_CHANGED; - } - return STATE_FAILED; - } - }); - - /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer - */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); - - this.pX = null; - this.pY = null; - } - - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, - - getTouchAction: function() { - var direction = this.options.direction; - var actions = []; - if (direction & DIRECTION_HORIZONTAL) { - actions.push(TOUCH_ACTION_PAN_Y); - } - if (direction & DIRECTION_VERTICAL) { - actions.push(TOUCH_ACTION_PAN_X); - } - return actions; - }, - - directionTest: function(input) { - var options = this.options; - var hasMoved = true; - var distance = input.distance; - var direction = input.direction; - var x = input.deltaX; - var y = input.deltaY; - - // lock to axis? - if (!(direction & options.direction)) { - if (options.direction & DIRECTION_HORIZONTAL) { - direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; - hasMoved = x != this.pX; - distance = Math.abs(input.deltaX); - } else { - direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; - hasMoved = y != this.pY; - distance = Math.abs(input.deltaY); - } - } - input.direction = direction; - - return hasMoved && distance > options.threshold && direction & options.direction; - }, - - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, - - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; - - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } - - this._super.emit.call(this, input); - } + Object.defineProperty(exports, '__esModule', { + value: true }); + var string = 'string'; + var boolean = 'boolean'; + var number = 'number'; + var array = 'array'; + var object = 'object'; // should only be in a __type__ property + var dom = 'dom'; + var any = 'any'; - /** - * Pinch - * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). - * @constructor - * @extends AttrRecognizer - */ - function PinchRecognizer() { - AttrRecognizer.apply(this, arguments); - } - - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, - - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; + var allOptions = { + configure: { + enabled: { boolean: boolean }, + filter: { boolean: boolean, string: string, array: array, 'function': 'function' }, + container: { dom: dom }, + showButton: { boolean: boolean }, + __type__: { object: object, boolean: boolean, string: string, array: array, 'function': 'function' } + }, + edges: { + arrows: { + to: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, + middle: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, + from: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, + __type__: { string: ['from', 'to', 'middle'], object: object } }, - - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + color: { + color: { string: string }, + highlight: { string: string }, + hover: { string: string }, + inherit: { string: ['from', 'to', 'both'], boolean: boolean }, + opacity: { number: number }, + __type__: { object: object, string: string } }, - - emit: function(input) { - this._super.emit.call(this, input); - if (input.scale !== 1) { - var inOut = input.scale < 1 ? 'in' : 'out'; - this.manager.emit(this.options.event + inOut, input); - } - } - }); - - /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer - */ - function PressRecognizer() { - Recognizer.apply(this, arguments); - - this._timer = null; - this._input = null; - } - - inherit(PressRecognizer, Recognizer, { - /** - * @namespace - * @memberof PressRecognizer - */ - defaults: { - event: 'press', - pointers: 1, - time: 500, // minimal time of the pointer to be pressed - threshold: 5 // a minimal movement is ok, but keep it low + dashes: { boolean: boolean, array: array }, + font: { + color: { string: string }, + size: { number: number }, // px + face: { string: string }, + background: { string: string }, + strokeWidth: { number: number }, // px + strokeColor: { string: string }, + align: { string: ['horizontal', 'top', 'middle', 'bottom'] }, + __type__: { object: object, string: string } }, - - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; + hidden: { boolean: boolean }, + hoverWidth: { 'function': 'function', number: number }, + label: { string: string, 'undefined': 'undefined' }, + labelHighlightBold: { boolean: boolean }, + length: { number: number, 'undefined': 'undefined' }, + physics: { boolean: boolean }, + scaling: { + min: { number: number }, + max: { number: number }, + label: { + enabled: { boolean: boolean }, + min: { number: number }, + max: { number: number }, + maxVisible: { number: number }, + drawThreshold: { number: number }, + __type__: { object: object, boolean: boolean } + }, + customScalingFunction: { 'function': 'function' }, + __type__: { object: object } }, - - process: function(input) { - var options = this.options; - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTime = input.deltaTime > options.time; - - this._input = input; - - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { - this.reset(); - } else if (input.eventType & INPUT_START) { - this.reset(); - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.time, this); - } else if (input.eventType & INPUT_END) { - return STATE_RECOGNIZED; - } - return STATE_FAILED; + selectionWidth: { 'function': 'function', number: number }, + selfReferenceSize: { number: number }, + shadow: { + enabled: { boolean: boolean }, + size: { number: number }, + x: { number: number }, + y: { number: number }, + __type__: { object: object, boolean: boolean } }, - - reset: function() { - clearTimeout(this._timer); + smooth: { + enabled: { boolean: boolean }, + type: { string: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'] }, + roundness: { number: number }, + __type__: { object: object, boolean: boolean } }, - - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; - } - - if (input && (input.eventType & INPUT_END)) { - this.manager.emit(this.options.event + 'up', input); - } else { - this._input.timeStamp = now(); - this.manager.emit(this.options.event, this._input); - } - } - }); - - /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer - */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); - } - - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 + title: { string: string, 'undefined': 'undefined' }, + width: { number: number }, + value: { number: number, 'undefined': 'undefined' }, + __type__: { object: object } + }, + groups: { + useDefaultGroups: { boolean: boolean }, + __any__: 'get from nodes, will be overwritten below', + __type__: { object: object } + }, + interaction: { + dragNodes: { boolean: boolean }, + dragView: { boolean: boolean }, + hideEdgesOnDrag: { boolean: boolean }, + hideNodesOnDrag: { boolean: boolean }, + hover: { boolean: boolean }, + keyboard: { + enabled: { boolean: boolean }, + speed: { x: { number: number }, y: { number: number }, zoom: { number: number }, __type__: { object: object } }, + bindToWindow: { boolean: boolean }, + __type__: { object: object, boolean: boolean } }, - - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; + multiselect: { boolean: boolean }, + navigationButtons: { boolean: boolean }, + selectable: { boolean: boolean }, + selectConnectedEdges: { boolean: boolean }, + hoverConnectedEdges: { boolean: boolean }, + tooltipDelay: { number: number }, + zoomView: { boolean: boolean }, + __type__: { object: object } + }, + layout: { + randomSeed: { 'undefined': 'undefined', number: number }, + hierarchical: { + enabled: { boolean: boolean }, + levelSeparation: { number: number }, + direction: { string: ['UD', 'DU', 'LR', 'RL'] }, // UD, DU, LR, RL + sortMethod: { string: ['hubsize', 'directed'] }, // hubsize, directed + __type__: { object: object, boolean: boolean } }, - - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); - } - }); - - /** - * Swipe - * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. - * @constructor - * @extends AttrRecognizer - */ - function SwipeRecognizer() { - AttrRecognizer.apply(this, arguments); - } - - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 + __type__: { object: object } + }, + manipulation: { + enabled: { boolean: boolean }, + initiallyActive: { boolean: boolean }, + addNode: { boolean: boolean, 'function': 'function' }, + addEdge: { boolean: boolean, 'function': 'function' }, + editNode: { 'function': 'function' }, + editEdge: { boolean: boolean, 'function': 'function' }, + deleteNode: { boolean: boolean, 'function': 'function' }, + deleteEdge: { boolean: boolean, 'function': 'function' }, + controlNodeStyle: 'get from nodes, will be overwritten below', + __type__: { object: object, boolean: boolean } + }, + nodes: { + borderWidth: { number: number }, + borderWidthSelected: { number: number, 'undefined': 'undefined' }, + brokenImage: { string: string, 'undefined': 'undefined' }, + color: { + border: { string: string }, + background: { string: string }, + highlight: { + border: { string: string }, + background: { string: string }, + __type__: { object: object, string: string } + }, + hover: { + border: { string: string }, + background: { string: string }, + __type__: { object: object, string: string } + }, + __type__: { object: object, string: string } }, - - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); + fixed: { + x: { boolean: boolean }, + y: { boolean: boolean }, + __type__: { object: object, boolean: boolean } }, - - attrTest: function(input) { - var direction = this.options.direction; - var velocity; - - if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { - velocity = input.velocity; - } else if (direction & DIRECTION_HORIZONTAL) { - velocity = input.velocityX; - } else if (direction & DIRECTION_VERTICAL) { - velocity = input.velocityY; - } - - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + font: { + color: { string: string }, + size: { number: number }, // px + face: { string: string }, + background: { string: string }, + strokeWidth: { number: number }, // px + strokeColor: { string: string }, + __type__: { object: object, string: string } }, - - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } - - this.manager.emit(this.options.event, input); - } - }); - - /** - * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur - * between the given interval and position. The delay option can be used to recognize multi-taps without firing - * a single tap. - * - * The eventData from the emitted event contains the property `tapCount`, which contains the amount of - * multi-taps being recognized. - * @constructor - * @extends Recognizer - */ - function TapRecognizer() { - Recognizer.apply(this, arguments); - - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; - - this._timer = null; - this._input = null; - this.count = 0; - } - - inherit(TapRecognizer, Recognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'tap', - pointers: 1, - taps: 1, - interval: 300, // max time between the multi-tap taps - time: 250, // max time of the pointer to be down (like finger on the screen) - threshold: 2, // a minimal movement is ok, but keep it low - posThreshold: 10 // a multi-tap can be a bit off the initial position + group: { string: string, number: number, 'undefined': 'undefined' }, + hidden: { boolean: boolean }, + icon: { + face: { string: string }, + code: { string: string }, //'\uf007', + size: { number: number }, //50, + color: { string: string }, + __type__: { object: object } }, - - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; + id: { string: string, number: number }, + image: { string: string, 'undefined': 'undefined' }, // --> URL + label: { string: string, 'undefined': 'undefined' }, + labelHighlightBold: { boolean: boolean }, + level: { number: number, 'undefined': 'undefined' }, + mass: { number: number }, + physics: { boolean: boolean }, + scaling: { + min: { number: number }, + max: { number: number }, + label: { + enabled: { boolean: boolean }, + min: { number: number }, + max: { number: number }, + maxVisible: { number: number }, + drawThreshold: { number: number }, + __type__: { object: object, boolean: boolean } + }, + customScalingFunction: { 'function': 'function' }, + __type__: { object: object } }, - - process: function(input) { - var options = this.options; - - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; - - this.reset(); - - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); - } - - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (validMovement && validTouchTime && validPointers) { - if (input.eventType != INPUT_END) { - return this.failTimeout(); - } - - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - - this.pTime = input.timeStamp; - this.pCenter = input.center; - - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } - - this._input = input; - - // if tap count matches we have recognized it, - // else it has began recognizing... - var tapCount = this.count % options.taps; - if (tapCount === 0) { - // no failing requirements, immediately trigger the tap event - // or wait as long as the multitap interval to trigger - if (!this.hasRequireFailures()) { - return STATE_RECOGNIZED; - } else { - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.interval, this); - return STATE_BEGAN; - } - } - } - return STATE_FAILED; + shadow: { + enabled: { boolean: boolean }, + size: { number: number }, + x: { number: number }, + y: { number: number }, + __type__: { object: object, boolean: boolean } }, - - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; + shape: { string: ['ellipse', 'circle', 'database', 'box', 'text', 'image', 'circularImage', 'diamond', 'dot', 'star', 'triangle', 'triangleDown', 'square', 'icon'] }, + size: { number: number }, + title: { string: string, 'undefined': 'undefined' }, + value: { number: number, 'undefined': 'undefined' }, + x: { number: number }, + y: { number: number }, + __type__: { object: object } + }, + physics: { + enabled: { boolean: boolean }, + barnesHut: { + gravitationalConstant: { number: number }, + centralGravity: { number: number }, + springLength: { number: number }, + springConstant: { number: number }, + damping: { number: number }, + avoidOverlap: { number: number }, + __type__: { object: object } }, - - reset: function() { - clearTimeout(this._timer); + forceAtlas2Based: { + gravitationalConstant: { number: number }, + centralGravity: { number: number }, + springLength: { number: number }, + springConstant: { number: number }, + damping: { number: number }, + avoidOverlap: { number: number }, + __type__: { object: object } }, + repulsion: { + centralGravity: { number: number }, + springLength: { number: number }, + springConstant: { number: number }, + nodeDistance: { number: number }, + damping: { number: number }, + __type__: { object: object } + }, + hierarchicalRepulsion: { + centralGravity: { number: number }, + springLength: { number: number }, + springConstant: { number: number }, + nodeDistance: { number: number }, + damping: { number: number }, + __type__: { object: object } + }, + maxVelocity: { number: number }, + minVelocity: { number: number }, // px/s + solver: { string: ['barnesHut', 'repulsion', 'hierarchicalRepulsion', 'forceAtlas2Based'] }, + stabilization: { + enabled: { boolean: boolean }, + iterations: { number: number }, // maximum number of iteration to stabilize + updateInterval: { number: number }, + onlyDynamicEdges: { boolean: boolean }, + fit: { boolean: boolean }, + __type__: { object: object, boolean: boolean } + }, + timestep: { number: number }, + __type__: { object: object, boolean: boolean } + }, - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); - } - } - }); - - /** - * Simple way to create an manager with a default set of recognizers. - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Hammer(element, options) { - options = options || {}; - options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); - return new Manager(element, options); - } - - /** - * @const {string} - */ - Hammer.VERSION = '2.0.4'; - - /** - * default settings - * @namespace - */ - Hammer.defaults = { - /** - * set if DOM events are being triggered. - * But this is slower and unused by simple implementations, so disabled by default. - * @type {Boolean} - * @default false - */ - domEvents: false, - - /** - * The value for the touchAction property/fallback. - * When set to `compute` it will magically set the correct value based on the added recognizers. - * @type {String} - * @default compute - */ - touchAction: TOUCH_ACTION_COMPUTE, - - /** - * @type {Boolean} - * @default true - */ - enable: true, - - /** - * EXPERIMENTAL FEATURE -- can be removed/changed - * Change the parent input target element. - * If Null, then it is being set the to main element. - * @type {Null|EventTarget} - * @default null - */ - inputTarget: null, - - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, - - /** - * Default recognizer setup when calling `Hammer()` - * When creating a new Manager these will be skipped. - * @type {Array} - */ - preset: [ - // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] - [RotateRecognizer, { enable: false }], - [PinchRecognizer, { enable: false }, ['rotate']], - [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], - [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], - [TapRecognizer], - [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], - [PressRecognizer] - ], - - /** - * Some CSS properties can be used to improve the working of Hammer. - * Add them to this method and they will be set when creating a new Manager. - * @namespace - */ - cssProps: { - /** - * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userSelect: 'none', - - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', - - /** - * Disables the default callout shown when you touch and hold a touch target. - * On iOS, when you touch and hold a touch target such as a link, Safari displays - * a callout containing information about the link. This property allows you to disable that callout. - * @type {String} - * @default 'none' - */ - touchCallout: 'none', - - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', - - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', - - /** - * Overrides the highlight color shown when the user taps a link or a JavaScript - * clickable element in iOS. This property obeys the alpha value, if specified. - * @type {String} - * @default 'rgba(0,0,0,0)' - */ - tapHighlightColor: 'rgba(0,0,0,0)' - } + //globals : + autoResize: { boolean: boolean }, + clickToUse: { boolean: boolean }, + locale: { string: string }, + locales: { + __any__: { any: any }, + __type__: { object: object } + }, + height: { string: string }, + width: { string: string }, + __type__: { object: object } }; - var STOP = 1; - var FORCED_STOP = 2; - - /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Manager(element, options) { - options = options || {}; - - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; - - this.handlers = {}; - this.session = {}; - this.recognizers = []; - - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); - - toggleCssProps(this, true); - - each(options.recognizers, function(item) { - var recognizer = this.add(new (item[0])(item[1])); - item[2] && recognizer.recognizeWith(item[2]); - item[3] && recognizer.requireFailure(item[3]); - }, this); - } - - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + allOptions.groups.__any__ = allOptions.nodes; + allOptions.manipulation.controlNodeStyle = allOptions.nodes; - // Options that need a little more setup - if (options.touchAction) { - this.touchAction.update(); - } - if (options.inputTarget) { - // Clean up existing event listeners and reinitialize - this.input.destroy(); - this.input.target = options.inputTarget; - this.input.init(); - } - return this; + var configureOptions = { + nodes: { + borderWidth: [1, 0, 10, 1], + borderWidthSelected: [2, 0, 10, 1], + color: { + border: ['color', '#2B7CE9'], + background: ['color', '#97C2FC'], + highlight: { + border: ['color', '#2B7CE9'], + background: ['color', '#D2E5FF'] + }, + hover: { + border: ['color', '#2B7CE9'], + background: ['color', '#D2E5FF'] + } }, - - /** - * stop recognizing for this session. - * This session will be discarded, when a new [input]start event is fired. - * When forced, the recognizer cycle is stopped immediately. - * @param {Boolean} [force] - */ - stop: function(force) { - this.session.stopped = force ? FORCED_STOP : STOP; + fixed: { + x: false, + y: false }, - - /** - * run the recognizers! - * called by the inputHandler function on every movement of the pointers (touches) - * it walks through all the recognizers and tries to detect the gesture that is being made - * @param {Object} inputData - */ - recognize: function(inputData) { - var session = this.session; - if (session.stopped) { - return; - } - - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); - - var recognizer; - var recognizers = this.recognizers; - - // this holds the recognizer that is being recognized. - // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED - // if no recognizer is detecting a thing, it is set to `null` - var curRecognizer = session.curRecognizer; - - // reset when the last recognizer is recognized - // or when we're in a new session - if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { - curRecognizer = session.curRecognizer = null; - } - - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; - - // find out if we are allowed try to recognize the input for this one. - // 1. allow if the session is NOT forced stopped (see the .stop() method) - // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one - // that is being recognized. - // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. - // this can be setup with the `recognizeWith()` method on the recognizer. - if (session.stopped !== FORCED_STOP && ( // 1 - !curRecognizer || recognizer == curRecognizer || // 2 - recognizer.canRecognizeWith(curRecognizer))) { // 3 - recognizer.recognize(inputData); - } else { - recognizer.reset(); - } - - // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the - // current active recognizer. but only if we don't already have an active recognizer - if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { - curRecognizer = session.curRecognizer = recognizer; - } - i++; - } + font: { + color: ['color', '#343434'], + size: [14, 0, 100, 1], // px + face: ['arial', 'verdana', 'tahoma'], + background: ['color', 'none'], + strokeWidth: [0, 0, 50, 1], // px + strokeColor: ['color', '#ffffff'] }, - - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; - } - - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } - } - return null; + //group: 'string', + hidden: false, + labelHighlightBold: true, + //icon: { + // face: 'string', //'FontAwesome', + // code: 'string', //'\uf007', + // size: [50, 0, 200, 1], //50, + // color: ['color','#2B7CE9'] //'#aa00ff' + //}, + //image: 'string', // --> URL + physics: true, + scaling: { + min: [10, 0, 200, 1], + max: [30, 0, 200, 1], + label: { + enabled: false, + min: [14, 0, 200, 1], + max: [30, 0, 200, 1], + maxVisible: [30, 0, 200, 1], + drawThreshold: [5, 0, 20, 1] + } }, - - /** - * add a recognizer to the manager - * existing recognizers with the same event name will be removed - * @param {Recognizer} recognizer - * @returns {Recognizer|Manager} - */ - add: function(recognizer) { - if (invokeArrayArg(recognizer, 'add', this)) { - return this; - } - - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); - } - - this.recognizers.push(recognizer); - recognizer.manager = this; - - this.touchAction.update(); - return recognizer; + shadow: { + enabled: false, + size: [10, 0, 20, 1], + x: [5, -30, 30, 1], + y: [5, -30, 30, 1] }, - - /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} - */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', this)) { - return this; - } - - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); - - this.touchAction.update(); - return this; + shape: ['ellipse', 'box', 'circle', 'database', 'diamond', 'dot', 'square', 'star', 'text', 'triangle', 'triangleDown'], + size: [25, 0, 200, 1] + }, + edges: { + arrows: { + to: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, + from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] } + }, + color: { + color: ['color', '#848484'], + highlight: ['color', '#848484'], + hover: ['color', '#848484'], + inherit: ['from', 'to', 'both', true, false], + opacity: [1, 0, 1, 0.05] + }, + dashes: false, + font: { + color: ['color', '#343434'], + size: [14, 0, 100, 1], // px + face: ['arial', 'verdana', 'tahoma'], + background: ['color', 'none'], + strokeWidth: [2, 0, 50, 1], // px + strokeColor: ['color', '#ffffff'], + align: ['horizontal', 'top', 'middle', 'bottom'] + }, + hidden: false, + hoverWidth: [1.5, 0, 5, 0.1], + labelHighlightBold: true, + physics: true, + scaling: { + min: [1, 0, 100, 1], + max: [15, 0, 100, 1], + label: { + enabled: true, + min: [14, 0, 200, 1], + max: [30, 0, 200, 1], + maxVisible: [30, 0, 200, 1], + drawThreshold: [5, 0, 20, 1] + } + }, + selectionWidth: [1.5, 0, 5, 0.1], + selfReferenceSize: [20, 0, 200, 1], + shadow: { + enabled: false, + size: [10, 0, 20, 1], + x: [5, -30, 30, 1], + y: [5, -30, 30, 1] + }, + smooth: { + enabled: true, + type: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'], + roundness: [0.5, 0, 1, 0.05] + }, + width: [1, 0, 30, 1] + }, + layout: { + //randomSeed: [0, 0, 500, 1], + hierarchical: { + enabled: false, + levelSeparation: [150, 20, 500, 5], + direction: ['UD', 'DU', 'LR', 'RL'], // UD, DU, LR, RL + sortMethod: ['hubsize', 'directed'] // hubsize, directed + } + }, + interaction: { + dragNodes: true, + dragView: true, + hideEdgesOnDrag: false, + hideNodesOnDrag: false, + hover: false, + keyboard: { + enabled: false, + speed: { x: [10, 0, 40, 1], y: [10, 0, 40, 1], zoom: [0.02, 0, 0.1, 0.005] }, + bindToWindow: true + }, + multiselect: false, + navigationButtons: false, + selectable: true, + selectConnectedEdges: true, + hoverConnectedEdges: true, + tooltipDelay: [300, 0, 1000, 25], + zoomView: true + }, + manipulation: { + enabled: false, + initiallyActive: false + }, + physics: { + enabled: true, + barnesHut: { + //theta: [0.5, 0.1, 1, 0.05], + gravitationalConstant: [-2000, -30000, 0, 50], + centralGravity: [0.3, 0, 10, 0.05], + springLength: [95, 0, 500, 5], + springConstant: [0.04, 0, 1.2, 0.005], + damping: [0.09, 0, 1, 0.01], + avoidOverlap: [0, 0, 1, 0.01] }, - - /** - * bind event - * @param {String} events - * @param {Function} handler - * @returns {EventEmitter} this - */ - on: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - handlers[event] = handlers[event] || []; - handlers[event].push(handler); - }); - return this; + forceAtlas2Based: { + //theta: [0.5, 0.1, 1, 0.05], + gravitationalConstant: [-50, -500, 0, 1], + centralGravity: [0.01, 0, 1, 0.005], + springLength: [95, 0, 500, 5], + springConstant: [0.08, 0, 1.2, 0.005], + damping: [0.4, 0, 1, 0.01], + avoidOverlap: [0, 0, 1, 0.01] }, - - /** - * unbind event, leave emit blank to remove all handlers - * @param {String} events - * @param {Function} [handler] - * @returns {EventEmitter} this - */ - off: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - if (!handler) { - delete handlers[event]; - } else { - handlers[event].splice(inArray(handlers[event], handler), 1); - } - }); - return this; + repulsion: { + centralGravity: [0.2, 0, 10, 0.05], + springLength: [200, 0, 500, 5], + springConstant: [0.05, 0, 1.2, 0.005], + nodeDistance: [100, 0, 500, 5], + damping: [0.09, 0, 1, 0.01] }, - - /** - * emit event to the listeners - * @param {String} event - * @param {Object} data - */ - emit: function(event, data) { - // we also want to trigger dom events - if (this.options.domEvents) { - triggerDomEvent(event, data); - } - - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { - return; - } - - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; - - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; - } + hierarchicalRepulsion: { + centralGravity: [0.2, 0, 10, 0.05], + springLength: [100, 0, 500, 5], + springConstant: [0.01, 0, 1.2, 0.005], + nodeDistance: [120, 0, 500, 5], + damping: [0.09, 0, 1, 0.01] }, - - /** - * destroy the manager and unbinds all events - * it doesn't unbind dom events, that is the user own responsibility - */ - destroy: function() { - this.element && toggleCssProps(this, false); - - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; - } + maxVelocity: [50, 0, 150, 1], + minVelocity: [0.1, 0.01, 0.5, 0.01], + solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'], + timestep: [0.5, 0.01, 1, 0.01] + }, + global: { + locale: ['en', 'nl'] + } }; - /** - * add/remove the css properties as defined in manager.options.cssProps - * @param {Manager} manager - * @param {Boolean} add - */ - function toggleCssProps(manager, add) { - var element = manager.element; - each(manager.options.cssProps, function(value, name) { - element.style[prefixed(element.style, name)] = add ? value : ''; - }); - } - - /** - * trigger dom event - * @param {String} event - * @param {Object} data - */ - function triggerDomEvent(event, data) { - var gestureEvent = document.createEvent('Event'); - gestureEvent.initEvent(event, true, true); - gestureEvent.gesture = data; - data.target.dispatchEvent(gestureEvent); - } - - extend(Hammer, { - INPUT_START: INPUT_START, - INPUT_MOVE: INPUT_MOVE, - INPUT_END: INPUT_END, - INPUT_CANCEL: INPUT_CANCEL, - - STATE_POSSIBLE: STATE_POSSIBLE, - STATE_BEGAN: STATE_BEGAN, - STATE_CHANGED: STATE_CHANGED, - STATE_ENDED: STATE_ENDED, - STATE_RECOGNIZED: STATE_RECOGNIZED, - STATE_CANCELLED: STATE_CANCELLED, - STATE_FAILED: STATE_FAILED, - - DIRECTION_NONE: DIRECTION_NONE, - DIRECTION_LEFT: DIRECTION_LEFT, - DIRECTION_RIGHT: DIRECTION_RIGHT, - DIRECTION_UP: DIRECTION_UP, - DIRECTION_DOWN: DIRECTION_DOWN, - DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, - DIRECTION_VERTICAL: DIRECTION_VERTICAL, - DIRECTION_ALL: DIRECTION_ALL, - - Manager: Manager, - Input: Input, - TouchAction: TouchAction, - - TouchInput: TouchInput, - MouseInput: MouseInput, - PointerEventInput: PointerEventInput, - TouchMouseInput: TouchMouseInput, - SingleTouchInput: SingleTouchInput, - - Recognizer: Recognizer, - AttrRecognizer: AttrRecognizer, - Tap: TapRecognizer, - Pan: PanRecognizer, - Swipe: SwipeRecognizer, - Pinch: PinchRecognizer, - Rotate: RotateRecognizer, - Press: PressRecognizer, - - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed - }); - - if ("function" == TYPE_FUNCTION && __webpack_require__(8)) { - !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { - return Hammer; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof module != 'undefined' && module.exports) { - module.exports = Hammer; - } else { - window[exportName] = Hammer; - } - - })(window, document, 'Hammer'); - + exports.allOptions = allOptions; + exports.configureOptions = configureOptions; /***/ }, /* 8 */ /***/ function(module, exports, __webpack_require__) { - /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; + 'use strict'; - /* WEBPACK VAR INJECTION */}.call(exports, {})) + var Hammer = __webpack_require__(10); + var util = __webpack_require__(14); -/***/ }, -/* 9 */ -/***/ function(module, exports, __webpack_require__) { + /** + * @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 || {}; - // utility functions + this.selected = false; + this.displayed = false; + this.dirty = true; - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. + this.top = null; + this.left = null; + this.width = null; + this.height = null; - 'use strict'; + this.editable = null; + if (this.data && this.data.hasOwnProperty('editable') && typeof this.data.editable === 'boolean') { + this.editable = data.editable; + } + } - var moment = __webpack_require__(10); - var uuid = __webpack_require__(13); + Item.prototype.stack = true; /** - * Test whether given object is a number - * @param {*} object - * @return {Boolean} isNumber + * Select current item */ - exports.isNumber = function (object) { - return object instanceof Number || typeof object == 'number'; + Item.prototype.select = function () { + this.selected = true; + this.dirty = true; + if (this.displayed) this.redraw(); }; /** - * Remove everything in the DOM object - * @param DOMobject + * Unselect current item */ - exports.recursiveDOMDelete = function (DOMobject) { - if (DOMobject) { - while (DOMobject.hasChildNodes() === true) { - exports.recursiveDOMDelete(DOMobject.firstChild); - DOMobject.removeChild(DOMobject.firstChild); - } + Item.prototype.unselect = function () { + this.selected = false; + this.dirty = true; + if (this.displayed) this.redraw(); + }; + + /** + * 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) { + var groupChanged = data.group != undefined && this.data.group != data.group; + if (groupChanged) { + this.parent.itemSet._moveToGroup(this, data.group); + } + + if (data.hasOwnProperty('editable') && typeof data.editable === 'boolean') { + this.editable = data.editable; } + + this.data = data; + this.dirty = true; + if (this.displayed) this.redraw(); }; /** - * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value. - * - * @param min - * @param max - * @param total - * @param value - * @returns {number} + * Set a parent for the item + * @param {ItemSet | Group} parent */ - exports.giveRange = function (min, max, total, value) { - if (max == min) { - return 0.5; + Item.prototype.setParent = function (parent) { + if (this.displayed) { + this.hide(); + this.parent = parent; + if (this.parent) { + this.show(); + } } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); + this.parent = parent; } }; /** - * Test whether given object is a string - * @param {*} object - * @return {Boolean} isString + * 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 */ - exports.isString = function (object) { - return object instanceof String || typeof object == 'string'; + Item.prototype.isVisible = function (range) { + // Should be implemented by Item implementations + return false; }; /** - * Test whether given object is a Date, or a String containing a Date - * @param {Date | String} object - * @return {Boolean} isDate + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed */ - exports.isDate = function (object) { - if (object instanceof Date) { - return true; - } else if (exports.isString(object)) { - // test whether this string contains a date - var match = ASPDateRegex.exec(object); - if (match) { - return true; - } else if (!isNaN(Date.parse(object))) { - return true; - } - } + Item.prototype.show = function () { + return false; + }; + /** + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed + */ + Item.prototype.hide = function () { return false; }; /** - * Create a semi UUID - * source: http://stackoverflow.com/a/105074/1262753 - * @return {String} uuid + * Repaint the item */ - exports.randomUUID = function () { - return uuid.v4(); + Item.prototype.redraw = function () {}; + + /** + * Reposition the Item horizontally + */ + Item.prototype.repositionX = function () {}; + + /** + * Reposition the Item vertically + */ + Item.prototype.repositionY = function () {}; + + /** + * 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) { + var editable = (this.options.editable.remove || this.data.editable === true) && this.data.editable !== false; + + if (this.selected && editable && !this.dom.deleteButton) { + // create and show button + var me = this; + + var deleteButton = document.createElement('div'); + deleteButton.className = 'vis-delete'; + deleteButton.title = 'Delete this item'; + + // TODO: be able to destroy the delete button + new Hammer(deleteButton).on('tap', function (event) { + event.stopPropagation(); + me.parent.removeFromDataSet(me); + }); + + 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; + } }; /** - * assign all keys of an object that are not nested objects to a certain value (used for color objects). - * @param obj - * @param value + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private */ - exports.assignAllKeys = function (obj, value) { - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - if (typeof obj[prop] !== 'object') { - obj[prop] = value; + 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; + } + + var changed = this._contentToString(this.content) !== this._contentToString(content); + if (changed) { + // 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); } } + + this.content = content; } }; /** - * Fill an object with a possibly partially defined other object. Only copies values if the a object has an object requiring values. - * That means an object is not created on a property if only the b object has it. - * @param obj - * @param value + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private */ - exports.fillIfDefined = function (a, b) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + Item.prototype._updateTitle = function (element) { + if (this.data.title != null) { + element.title = this.data.title || ''; + } else { + element.removeAttribute('vis-title'); + } + }; - for (var prop in a) { - if (b[prop] !== undefined) { - if (typeof b[prop] !== 'object') { - if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } + /** + * 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 { + 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 { - if (typeof a[prop] === 'object') { - exports.fillIfDefined(a[prop], b[prop], allowDeletion); - } + element.removeAttribute('data-' + name); } } } }; /** - * Extend object a with the properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Object} a - * @param {... Object} b - * @return {Object} a + * Update custom styles of the element + * @param element + * @private */ - exports.protoExtend = function (a, b) { - for (var i = 1; i < arguments.length; i++) { - var other = arguments[i]; - for (var prop in other) { - a[prop] = other[prop]; - } + Item.prototype._updateStyle = function (element) { + // remove old styles + if (this.style) { + util.removeCssText(element, this.style); + this.style = null; } - return a; + + // append new styles + if (this.data.style) { + util.addCssText(element, this.data.style); + this.style = this.data.style; + } + }; + + /** + * Stringify the items contents + * @param {string | Element | undefined} content + * @returns {string | undefined} + * @private + */ + Item.prototype._contentToString = function (content) { + if (typeof content === 'string') return content; + if (content && 'outerHTML' in content) return content.outerHTML; + return content; }; /** - * Extend object a with the properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Object} a - * @param {... Object} b - * @return {Object} a + * Return the width of the item left from its start date + * @return {number} */ - exports.extend = function (a, b) { - for (var i = 1; i < arguments.length; i++) { - var other = arguments[i]; - for (var prop in other) { - if (other.hasOwnProperty(prop)) { - a[prop] = other[prop]; - } - } - } - return a; + Item.prototype.getWidthLeft = function () { + return 0; }; /** - * Extend object a with selected properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Array.} props - * @param {Object} a - * @param {Object} b - * @return {Object} a + * Return the width of the item right from the max of its start and end date + * @return {number} */ - exports.selectiveExtend = function (props, a, b) { - if (!Array.isArray(props)) { - throw new Error('Array with property names expected as first argument'); - } + Item.prototype.getWidthRight = function () { + return 0; + }; - for (var i = 2; i < arguments.length; i++) { - var other = arguments[i]; + module.exports = Item; - for (var p = 0; p < props.length; p++) { - var prop = props[p]; - if (other.hasOwnProperty(prop)) { - a[prop] = other[prop]; - } - } - } - return a; - }; + // should be implemented by the item - /** - * Extend object a with selected properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Array.} props - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - exports.selectiveDeepExtend = function (props, a, b) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + // should be implemented by the item - // TODO: add support for Arrays to deepExtend - if (Array.isArray(b)) { - throw new TypeError('Arrays are not supported by deepExtend'); - } - for (var i = 2; i < arguments.length; i++) { - var other = arguments[i]; - for (var p = 0; p < props.length; p++) { - var prop = props[p]; - if (other.hasOwnProperty(prop)) { - if (b[prop] && b[prop].constructor === Object) { - if (a[prop] === undefined) { - a[prop] = {}; - } - if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop], false, allowDeletion); + // should be implemented by the item + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var _componentsEdge = __webpack_require__(3); + + var _componentsEdge2 = _interopRequireDefault(_componentsEdge); + + var _componentsSharedLabel = __webpack_require__(4); + + var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); + + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + + var EdgesHandler = (function () { + function EdgesHandler(body, images, groups) { + var _this = this; + + _classCallCheck(this, EdgesHandler); + + this.body = body; + this.images = images; + this.groups = groups; + + // create the edge API in the body container + this.body.functions.createEdge = this.create.bind(this); + + this.edgesListeners = { + add: function add(event, params) { + _this.add(params.items); + }, + update: function update(event, params) { + _this.update(params.items); + }, + remove: function remove(event, params) { + _this.remove(params.items); + } + }; + + this.options = {}; + this.defaultOptions = { + arrows: { + to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: { enabled: false, scaleFactor: 1 }, + from: { enabled: false, scaleFactor: 1 } + }, + color: { + color: '#848484', + highlight: '#848484', + hover: '#848484', + inherit: 'from', + opacity: 1 + }, + dashes: false, + font: { + color: '#343434', + size: 14, // px + face: 'arial', + background: 'none', + strokeWidth: 2, // px + strokeColor: '#ffffff', + align: 'horizontal' + }, + hidden: false, + hoverWidth: 1.5, + label: undefined, + labelHighlightBold: true, + length: undefined, + physics: true, + scaling: { + min: 1, + max: 15, + label: { + enabled: true, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 5 + }, + customScalingFunction: function customScalingFunction(min, max, total, value) { + if (max === min) { + return 0.5; } else { - if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; - } + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); } - } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); - } else { - a[prop] = b[prop]; } - } - } - } - return a; - }; + }, + selectionWidth: 1.5, + selfReferenceSize: 20, + shadow: { + enabled: false, + size: 10, + x: 5, + y: 5 + }, + smooth: { + enabled: true, + type: 'dynamic', + roundness: 0.5 + }, + title: undefined, + width: 1, + value: undefined + }; - /** - * Extend object a with selected properties of object b or a series of objects - * Only properties with defined values are copied - * @param {Array.} props - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - exports.selectiveNotDeepExtend = function (props, a, b) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + util.extend(this.options, this.defaultOptions); - // TODO: add support for Arrays to deepExtend - if (Array.isArray(b)) { - throw new TypeError('Arrays are not supported by deepExtend'); + this.bindEventListeners(); } - for (var prop in b) { - if (b.hasOwnProperty(prop)) { - if (props.indexOf(prop) == -1) { - if (b[prop] && b[prop].constructor === Object) { - if (a[prop] === undefined) { - a[prop] = {}; - } - if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop]); - } else { - if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; + + _createClass(EdgesHandler, [{ + key: 'bindEventListeners', + value: function bindEventListeners() { + var _this2 = this; + + // this allows external modules to force all dynamic curves to turn static. + this.body.emitter.on('_forceDisableDynamicCurves', function (type) { + if (type === 'dynamic') { + type = 'continuous'; + } + var emitChange = false; + for (var edgeId in _this2.body.edges) { + if (_this2.body.edges.hasOwnProperty(edgeId)) { + var edge = _this2.body.edges[edgeId]; + var edgeData = _this2.body.data.edges._data[edgeId]; + + // only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined. + // this is because a change in the global would not affect these curves. + if (edgeData !== undefined) { + var edgeOptions = edgeData.smooth; + if (edgeOptions !== undefined) { + if (edgeOptions.enabled === true && edgeOptions.type === 'dynamic') { + if (type === undefined) { + edge.setOptions({ smooth: false }); + } else { + edge.setOptions({ smooth: { type: type } }); + } + emitChange = true; + } + } } } - } else if (Array.isArray(b[prop])) { - throw new TypeError('Arrays are not supported by deepExtend'); - } else { - a[prop] = b[prop]; } - } + if (emitChange === true) { + _this2.body.emitter.emit('_dataChanged'); + } + }); + + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on('_dataUpdated', function () { + _this2.reconnectEdges(); + _this2.markAllEdgesAsDirty(); + }); + + // refresh the edges. Used when reverting from hierarchical layout + this.body.emitter.on('refreshEdges', this.refresh.bind(this)); + this.body.emitter.on('refresh', this.refresh.bind(this)); + this.body.emitter.on('destroy', function () { + delete _this2.body.functions.createEdge; + delete _this2.edgesListeners.add; + delete _this2.edgesListeners.update; + delete _this2.edgesListeners.remove; + delete _this2.edgesListeners; + }); } - } - return a; - }; + }, { + key: 'setOptions', + value: function setOptions(options) { + if (options !== undefined) { + // use the parser from the Edge class to fill in all shorthand notations + _componentsEdge2['default'].parseOptions(this.options, options); - /** - * Deep extend an object a with the properties of object b - * @param {Object} a - * @param {Object} b - * @param [Boolean] protoExtend --> optional parameter. If true, the prototype values will also be extended. - * (ie. the options objects that inherit from others will also get the inherited options) - * @param [Boolean] global --> optional parameter. If true, the values of fields that are null will not deleted - * @returns {Object} - */ - exports.deepExtend = function (a, b, protoExtend, allowDeletion) { - for (var prop in b) { - if (b.hasOwnProperty(prop) || protoExtend === true) { - if (b[prop] && b[prop].constructor === Object) { - if (a[prop] === undefined) { - a[prop] = {}; + // hanlde multiple input cases for color + if (options.color !== undefined) { + this.markAllEdgesAsDirty(); } - if (a[prop].constructor === Object) { - exports.deepExtend(a[prop], b[prop], protoExtend); - } else { - if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { - delete a[prop]; - } else { - a[prop] = b[prop]; + + // update smooth settings in all edges + var dataChanged = false; + if (options.smooth !== undefined) { + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged; + } } } - } else if (Array.isArray(b[prop])) { - a[prop] = []; - for (var i = 0; i < b[prop].length; i++) { - a[prop].push(b[prop][i]); + + // update fonts in all edges + if (options.font !== undefined) { + // use the parser from the Label class to fill in all shorthand notations + _componentsSharedLabel2['default'].parseOptions(this.options.font, options); + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId].updateLabelModule(); + } + } + } + + // update the state of the variables if needed + if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) { + this.body.emitter.emit('_dataChanged'); } - } else { - a[prop] = b[prop]; } } - } - return a; - }; + }, { + key: 'setData', - /** - * Test whether all elements in two arrays are equal. - * @param {Array} a - * @param {Array} b - * @return {boolean} Returns true if both arrays have the same length and same - * elements. - */ - exports.equalArray = function (a, b) { - if (a.length != b.length) return false; + /** + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private + * @private + */ + value: function setData(edges) { + var _this3 = this; - for (var i = 0, len = a.length; i < len; i++) { - if (a[i] != b[i]) return false; - } + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - return true; - }; + var oldEdgesData = this.body.data.edges; - /** - * Convert an object to another type - * @param {Boolean | Number | String | Date | Moment | Null | undefined} object - * @param {String | undefined} type Name of the type. Available types: - * 'Boolean', 'Number', 'String', - * 'Date', 'Moment', ISODate', 'ASPDate'. - * @return {*} object - * @throws Error - */ - exports.convert = function (object, type) { - var match; + if (edges instanceof DataSet || edges instanceof DataView) { + this.body.data.edges = edges; + } else if (Array.isArray(edges)) { + this.body.data.edges = new DataSet(); + this.body.data.edges.add(edges); + } else if (!edges) { + this.body.data.edges = new DataSet(); + } else { + throw new TypeError('Array or DataSet expected'); + } - if (object === undefined) { - return undefined; - } - if (object === null) { - return null; - } + // TODO: is this null or undefined or false? + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.off(event, callback); + }); + } - if (!type) { - return object; - } - if (!(typeof type === 'string') && !(type instanceof String)) { - throw new Error('Type must be a string'); - } + // remove drawn edges + this.body.edges = {}; - //noinspection FallthroughInSwitchStatementJS - switch (type) { - case 'boolean': - case 'Boolean': - return Boolean(object); + // TODO: is this null or undefined or false? + if (this.body.data.edges) { + // subscribe to new dataset + util.forEach(this.edgesListeners, function (callback, event) { + _this3.body.data.edges.on(event, callback); + }); - case 'number': - case 'Number': - return Number(object.valueOf()); + // draw all new nodes + var ids = this.body.data.edges.getIds(); + this.add(ids, true); + } - case 'string': - case 'String': - return String(object); + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); + } + } + }, { + key: 'add', - case 'Date': - if (exports.isNumber(object)) { - return new Date(object); + /** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + + var edges = this.body.edges; + var edgesData = this.body.data.edges; + + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } + + var data = edgesData.get(id, { 'showInternalIds': true }); + edges[id] = this.create(data); } - if (object instanceof Date) { - return new Date(object.valueOf()); - } else if (moment.isMoment(object)) { - return new Date(object.valueOf()); + + if (doNotEmit === false) { + this.body.emitter.emit('_dataChanged'); } - if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])); // parse number + } + }, { + key: 'update', + + /** + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + value: function update(ids) { + var edges = this.body.edges; + var edgesData = this.body.data.edges; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var data = edgesData.get(id); + var edge = edges[id]; + if (edge === null) { + // update edge + edge.disconnect(); + dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. + edge.connect(); } else { - return moment(object).toDate(); // parse string + // create edge + this.body.edges[id] = this.create(data); + dataChanged = true; } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); } - case 'Moment': - if (exports.isNumber(object)) { - return moment(object); - } - if (object instanceof Date) { - return moment(object.valueOf()); - } else if (moment.isMoment(object)) { - return moment(object); - } - if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return moment(Number(match[1])); // parse number - } else { - return moment(object); // parse string - } + if (dataChanged === true) { + this.body.emitter.emit('_dataChanged'); } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); + this.body.emitter.emit('_dataUpdated'); } + } + }, { + key: 'remove', - case 'ISODate': - if (exports.isNumber(object)) { - return new Date(object); - } else if (object instanceof Date) { - return object.toISOString(); - } else if (moment.isMoment(object)) { - return object.toDate().toISOString(); - } else if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - if (match) { - // object is an ASP date - return new Date(Number(match[1])).toISOString(); // parse number - } else { - return new Date(object).toISOString(); // parse string + /** + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var edges = this.body.edges; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge !== undefined) { + edge.edgeType.cleanup(); + edge.disconnect(); + delete edges[id]; } - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate'); } - case 'ASPDate': - if (exports.isNumber(object)) { - return '/Date(' + object + ')/'; - } else if (object instanceof Date) { - return '/Date(' + object.valueOf() + ')/'; - } else if (exports.isString(object)) { - match = ASPDateRegex.exec(object); - var value; - if (match) { - // object is an ASP date - value = new Date(Number(match[1])).valueOf(); // parse number - } else { - value = new Date(object).valueOf(); // parse string + this.body.emitter.emit('_dataChanged'); + } + }, { + key: 'refresh', + value: function refresh() { + var edges = this.body.edges; + for (var edgeId in edges) { + var edge = undefined; + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + } + var data = this.body.data.edges._data[edgeId]; + if (edge !== undefined && data !== undefined) { + edge.setOptions(data); } - return '/Date(' + value + ')/'; - } else { - throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); } - - default: - throw new Error('Unknown type "' + type + '"'); - } - }; - - // parse ASP.Net Date pattern, - // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' - // code from http://momentjs.com/ - var ASPDateRegex = /^\/?Date\((\-?\d+)/i; - - /** - * Get the type of an object, for example exports.getType([]) returns 'Array' - * @param {*} object - * @return {String} type - */ - exports.getType = function (object) { - var type = typeof object; - - if (type == 'object') { - if (object === null) { - return 'null'; - } - if (object instanceof Boolean) { - return 'Boolean'; } - if (object instanceof Number) { - return 'Number'; + }, { + key: 'create', + value: function create(properties) { + return new _componentsEdge2['default'](properties, this.body, this.options); } - if (object instanceof String) { - return 'String'; + }, { + key: 'markAllEdgesAsDirty', + value: function markAllEdgesAsDirty() { + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].edgeType.colorDirty = true; + } } - if (Array.isArray(object)) { - return 'Array'; + }, { + key: 'reconnectEdges', + + /** + * Reconnect all edges + * @private + */ + value: function reconnectEdges() { + var id; + var nodes = this.body.nodes; + var edges = this.body.edges; + + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } + } + + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } + } } - if (object instanceof Date) { - return 'Date'; + }, { + key: 'getConnectedNodes', + value: function getConnectedNodes(edgeId) { + var nodeList = []; + if (this.body.edges[edgeId] !== undefined) { + var edge = this.body.edges[edgeId]; + if (edge.fromId) { + nodeList.push(edge.fromId); + } + if (edge.toId) { + nodeList.push(edge.toId); + } + } + return nodeList; } - return 'Object'; - } else if (type == 'number') { - return 'Number'; - } else if (type == 'boolean') { - return 'Boolean'; - } else if (type == 'string') { - return 'String'; - } else if (type === undefined) { - return 'undefined'; - } + }]); - return type; - }; + return EdgesHandler; + })(); - /** - * Used to extend an array and copy it. This is used to propagate paths recursively. - * - * @param arr - * @param newValue - * @returns {Array} - */ - exports.copyAndExtendArray = function (arr, newValue) { - var newArr = []; - for (var i = 0; i < arr.length; i++) { - newArr.push(arr[i]); - } - newArr.push(newValue); - return newArr; - }; + exports['default'] = EdgesHandler; + module.exports = exports['default']; - /** - * Used to extend an array and copy it. This is used to propagate paths recursively. - * - * @param arr - * @param newValue - * @returns {Array} - */ - exports.copyArray = function (arr) { - var newArr = []; - for (var i = 0; i < arr.length; i++) { - newArr.push(arr[i]); - } - return newArr; - }; +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Retrieve the absolute left value of a DOM element - * @param {Element} elem A dom element, for example a div - * @return {number} left The absolute left position of this element - * in the browser page. - */ - exports.getAbsoluteLeft = function (elem) { - return elem.getBoundingClientRect().left; - }; + // Only load hammer.js when in a browser environment + // (loading hammer.js in a node.js environment gives errors) + 'use strict'; - /** - * Retrieve the absolute top value of a DOM element - * @param {Element} elem A dom element, for example a div - * @return {number} top The absolute top position of this element - * in the browser page. - */ - exports.getAbsoluteTop = function (elem) { - return elem.getBoundingClientRect().top; - }; + if (typeof window !== 'undefined') { + var propagating = __webpack_require__(11); + var Hammer = window['Hammer'] || __webpack_require__(12); + module.exports = propagating(Hammer, { + preventDefault: 'mouse' + }); + } else { + module.exports = function () { + throw Error('hammer.js is only available in a browser, not in node.js.'); + }; + } - /** - * add a className to the given elements style - * @param {Element} elem - * @param {String} className - */ - exports.addClassName = function (elem, className) { - var classes = elem.className.split(' '); - if (classes.indexOf(className) == -1) { - classes.push(className); // add the class to the array - elem.className = classes.join(' '); - } - }; +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { - /** - * add a className to the given elements style - * @param {Element} elem - * @param {String} className - */ - exports.removeClassName = function (elem, className) { - var classes = elem.className.split(' '); - var index = classes.indexOf(className); - if (index != -1) { - classes.splice(index, 1); // remove the class from the array - elem.className = classes.join(' '); - } - }; + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - /** - * For each method for both arrays and objects. - * In case of an array, the built-in Array.forEach() is applied. - * In case of an Object, the method loops over all properties of the object. - * @param {Object | Array} object An Object or Array - * @param {function} callback Callback method, called for each item in - * the object or array with three parameters: - * callback(value, index, object) - */ - exports.forEach = function (object, callback) { - var i, len; - if (Array.isArray(object)) { - // array - for (i = 0, len = object.length; i < len; i++) { - callback(object[i], i, object); - } + (function (factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); } else { - // object - for (i in object) { - if (object.hasOwnProperty(i)) { - callback(object[i], i, object); - } - } + // Browser globals (root is window) + window.propagating = factory(); } - }; - - /** - * Convert an object into an array: all objects properties are put into the - * array. The resulting array is unordered. - * @param {Object} object - * @param {Array} array - */ - exports.toArray = function (object) { - var array = []; + }(function () { + var _firstTarget = null; // singleton, will contain the target element where the touch event started + var _processing = false; // singleton, true when a touch event is being handled - for (var prop in object) { - if (object.hasOwnProperty(prop)) array.push(object[prop]); - } + /** + * Extend an Hammer.js instance with event propagation. + * + * Features: + * - Events emitted by hammer will propagate in order from child to parent + * elements. + * - Events are extended with a function `event.stopPropagation()` to stop + * propagation to parent elements. + * - An option `preventDefault` to stop all default browser behavior. + * + * Usage: + * var hammer = propagatingHammer(new Hammer(element)); + * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true}); + * + * @param {Hammer.Manager} hammer An hammer instance. + * @param {Object} [options] Available options: + * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`. + * Enforce preventing the default browser behavior. + * Cannot be set to `false`. + * @return {Hammer.Manager} Returns the same hammer instance with extended + * functionality + */ + return function propagating(hammer, options) { + var _options = options || { + preventDefault: false + }; - return array; - }; + if (hammer.Manager) { + // This looks like the Hammer constructor. + // Overload the constructors with our own. + var Hammer = hammer; - /** - * Update a property in an object - * @param {Object} object - * @param {String} key - * @param {*} value - * @return {Boolean} changed - */ - exports.updateProperty = function (object, key, value) { - if (object[key] !== value) { - object[key] = value; - return true; - } else { - return false; - } - }; + var PropagatingHammer = function(element, options) { + var o = Object.create(_options); + if (options) Hammer.extend(o, options); + return propagating(new Hammer(element, o), o); + }; + Hammer.extend(PropagatingHammer, Hammer); - /** - * Add and event listener. Works for all browsers - * @param {Element} element An html element - * @param {string} action The action, for example "click", - * without the prefix "on" - * @param {function} listener The callback function to be executed - * @param {boolean} [useCapture] - */ - exports.addEventListener = function (element, action, listener, useCapture) { - if (element.addEventListener) { - if (useCapture === undefined) useCapture = false; + PropagatingHammer.Manager = function (element, options) { + var o = Object.create(_options); + if (options) Hammer.extend(o, options); + return propagating(new Hammer.Manager(element, o), o); + }; - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox + return PropagatingHammer; } - element.addEventListener(action, listener, useCapture); - } else { - element.attachEvent('on' + action, listener); // IE browsers - } - }; + // create a wrapper object which will override the functions + // `on`, `off`, `destroy`, and `emit` of the hammer instance + var wrapper = Object.create(hammer); - /** - * Remove an event listener from an element - * @param {Element} element An html dom element - * @param {string} action The name of the event, for example "mousedown" - * @param {function} listener The listener function - * @param {boolean} [useCapture] - */ - exports.removeEventListener = function (element, action, listener, useCapture) { - if (element.removeEventListener) { - // non-IE browsers - if (useCapture === undefined) useCapture = false; + // attach to DOM element + var element = hammer.element; + element.hammer = wrapper; - if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { - action = 'DOMMouseScroll'; // For Firefox - } + // register an event to catch the start of a gesture and store the + // target in a singleton + hammer.on('hammer.input', function (event) { + if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) { + event.preventDefault(); + } + if (event.isFirst) { + _firstTarget = event.target; + } + }); - element.removeEventListener(action, listener, useCapture); - } else { - // IE browsers - element.detachEvent('on' + action, listener); - } - }; + /** @type {Object.>} */ + wrapper._handlers = {}; - /** - * Cancels the event if it is cancelable, without stopping further propagation of the event. - */ - exports.preventDefault = function (event) { - if (!event) event = window.event; + /** + * Register a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} handler A callback function, called as handler(event) + * @returns {Hammer.Manager} Returns the hammer instance + */ + wrapper.on = function (events, handler) { + // register the handler + split(events).forEach(function (event) { + var _handlers = wrapper._handlers[event]; + if (!_handlers) { + wrapper._handlers[event] = _handlers = []; - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } else { - event.returnValue = false; // IE browsers - } - }; + // register the static, propagated handler + hammer.on(event, propagatedHandler); + } + _handlers.push(handler); + }); - /** - * Get HTML element which is the target of the event - * @param {Event} event - * @return {Element} target element - */ - exports.getTarget = function (event) { - // code from http://www.quirksmode.org/js/events_properties.html - if (!event) { - event = window.event; - } + return wrapper; + }; + + /** + * Unregister a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} [handler] Optional. The registered handler. If not + * provided, all handlers for given events + * are removed. + * @returns {Hammer.Manager} Returns the hammer instance + */ + wrapper.off = function (events, handler) { + // unregister the handler + split(events).forEach(function (event) { + var _handlers = wrapper._handlers[event]; + if (_handlers) { + _handlers = handler ? _handlers.filter(function (h) { + return h !== handler; + }) : []; + + if (_handlers.length > 0) { + wrapper._handlers[event] = _handlers; + } + else { + // remove static, propagated handler + hammer.off(event, propagatedHandler); + delete wrapper._handlers[event]; + } + } + }); - var target; + return wrapper; + }; - if (event.target) { - target = event.target; - } else if (event.srcElement) { - target = event.srcElement; - } + /** + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event + */ + wrapper.emit = function(eventType, event) { + _firstTarget = event.target; + hammer.emit(eventType, event); + }; - if (target.nodeType != undefined && target.nodeType == 3) { - // defeat Safari bug - target = target.parentNode; - } + wrapper.destroy = function () { + // Detach from DOM element + delete hammer.element.hammer; - return target; - }; + // clear all handlers + wrapper._handlers = {}; - /** - * Check if given element contains given parent somewhere in the DOM tree - * @param {Element} element - * @param {Element} parent - */ - exports.hasParent = function (element, parent) { - var e = element; + // call original hammer destroy + hammer.destroy(); + }; - while (e) { - if (e === parent) { - return true; + // split a string with space separated words + function split(events) { + return events.match(/[^ ]+/g); } - e = e.parentNode; - } - return false; - }; - - exports.option = {}; - - /** - * Convert a value into a boolean - * @param {Boolean | function | undefined} value - * @param {Boolean} [defaultValue] - * @returns {Boolean} bool - */ - exports.option.asBoolean = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + /** + * A static event handler, applying event propagation. + * @param {Object} event + */ + function propagatedHandler(event) { + // let only a single hammer instance handle this event + if (event.type !== 'hammer.input') { + // it is possible that the same srcEvent is used with multiple hammer events, + // we keep track on which events are handled in an object _handled + if (!event.srcEvent._handled) { + event.srcEvent._handled = {}; + } - if (value != null) { - return value != false; - } + if (event.srcEvent._handled[event.type]) { + return; + } + else { + event.srcEvent._handled[event.type] = true; + } + } - return defaultValue || null; - }; + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - /** - * Convert a value into a number - * @param {Boolean | function | undefined} value - * @param {Number} [defaultValue] - * @returns {Number} number - */ - exports.option.asNumber = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + // attach firstTarget property to the event + event.firstTarget = _firstTarget; - if (value != null) { - return Number(value) || defaultValue || null; - } + // propagate over all elements (until stopped) + var elem = _firstTarget; + while (elem && !stopped) { + var _handlers = elem.hammer && elem.hammer._handlers[event.type]; + if (_handlers) { + for (var i = 0; i < _handlers.length && !stopped; i++) { + _handlers[i](event); + } + } - return defaultValue || null; - }; + elem = elem.parentNode; + } + } - /** - * Convert a value into a string - * @param {String | function | undefined} value - * @param {String} [defaultValue] - * @returns {String} str - */ - exports.option.asString = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + return wrapper; + }; + })); - if (value != null) { - return String(value); - } - return defaultValue || null; - }; +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Convert a size or location into a string with pixels or a percentage - * @param {String | Number | function | undefined} value - * @param {String} [defaultValue] - * @returns {String} size - */ - exports.option.asSize = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 + * http://hammerjs.github.io/ + * + * Copyright (c) 2014 Jorik Tangelder; + * Licensed under the MIT license */ + (function(window, document, exportName, undefined) { + 'use strict'; - if (exports.isString(value)) { - return value; - } else if (exports.isNumber(value)) { - return value + 'px'; - } else { - return defaultValue || null; - } - }; + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); - /** - * Convert a value into a DOM element - * @param {HTMLElement | function | undefined} value - * @param {HTMLElement} [defaultValue] - * @returns {HTMLElement | null} dom - */ - exports.option.asElement = function (value, defaultValue) { - if (typeof value == 'function') { - value = value(); - } + var TYPE_FUNCTION = 'function'; - return value || defaultValue || null; - }; + var round = Math.round; + var abs = Math.abs; + var now = Date.now; /** - * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb - * - * @param {String} hex - * @returns {{r: *, g: *, b: *}} | 255 range + * set a timeout with a given scope + * @param {Function} fn + * @param {Number} timeout + * @param {Object} context + * @returns {number} */ - exports.hexToRGB = function (hex) { - // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") - var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, function (m, r, g, b) { - return r + r + g + g + b + b; - }); - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : null; - }; + function setTimeoutContext(fn, timeout, context) { + return setTimeout(bindFn(fn, context), timeout); + } /** - * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string. - * @param color - * @param opacity - * @returns {*} + * if the argument is an array, we want to execute the fn on each entry + * if it aint an array we don't want to do a thing. + * this is used by all the methods that accept a single and array argument. + * @param {*|Array} arg + * @param {String} fn + * @param {Object} [context] + * @returns {Boolean} */ - exports.overrideOpacity = function (color, opacity) { - if (color.indexOf('rgba') != -1) { - return color; - } else if (color.indexOf('rgb') != -1) { - var rgb = color.substr(color.indexOf('(') + 1).replace(')', '').split(','); - return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')'; - } else { - var rgb = exports.hexToRGB(color); - if (rgb == null) { - return color; - } else { - return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')'; + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; } - } - }; + return false; + } /** - * - * @param red 0 -- 255 - * @param green 0 -- 255 - * @param blue 0 -- 255 - * @returns {string} - * @constructor + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context */ - exports.RGBToHex = function (red, green, blue) { - return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); - }; + function each(obj, iterator, context) { + var i; - /** - * Parse a color property into an object with border, background, and - * highlight colors - * @param {Object | String} color - * @return {Object} colorObject - */ - exports.parseColor = function (color) { - var c; - if (exports.isString(color) === true) { - if (exports.isValidRGB(color) === true) { - var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) { - return parseInt(value); - }); - color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]); + if (!obj) { + return; } - if (exports.isValidHex(color) === true) { - var hsv = exports.hexToHSV(color); - var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) }; - var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 }; - var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v); - var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v); - c = { - background: color, - border: darkerColorHex, - highlight: { - background: lighterColorHex, - border: darkerColorHex - }, - hover: { - background: lighterColorHex, - border: darkerColorHex + + if (obj.forEach) { + obj.forEach(iterator, context); + } else if (obj.length !== undefined) { + i = 0; + while (i < obj.length) { + iterator.call(context, obj[i], i, obj); + i++; } - }; } else { - c = { - background: color, - border: color, - highlight: { - background: color, - border: color - }, - hover: { - background: color, - border: color + for (i in obj) { + obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); } - }; - } - } else { - c = {}; - c.background = color.background || undefined; - c.border = color.border || undefined; - - if (exports.isString(color.highlight)) { - c.highlight = { - border: color.highlight, - background: color.highlight - }; - } else { - c.highlight = {}; - c.highlight.background = color.highlight && color.highlight.background || undefined; - c.highlight.border = color.highlight && color.highlight.border || undefined; - } - - if (exports.isString(color.hover)) { - c.hover = { - border: color.hover, - background: color.hover - }; - } else { - c.hover = {}; - c.hover.background = color.hover && color.hover.background || undefined; - c.hover.border = color.hover && color.hover.border || undefined; } - } - - return c; - }; + } /** - * http://www.javascripter.net/faq/rgb2hsv.htm - * - * @param red - * @param green - * @param blue - * @returns {*} - * @constructor + * extend object. + * means that properties in dest will be overwritten by the ones in src. + * @param {Object} dest + * @param {Object} src + * @param {Boolean} [merge] + * @returns {Object} dest */ - exports.RGBToHSV = function (red, green, blue) { - red = red / 255;green = green / 255;blue = blue / 255; - var minRGB = Math.min(red, Math.min(green, blue)); - var maxRGB = Math.max(red, Math.max(green, blue)); - - // Black-gray-white - if (minRGB == maxRGB) { - return { h: 0, s: 0, v: minRGB }; - } - - // Colors other than black-gray-white: - var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red; - var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5; - var hue = 60 * (h - d / (maxRGB - minRGB)) / 360; - var saturation = (maxRGB - minRGB) / maxRGB; - var value = maxRGB; - return { h: hue, s: saturation, v: value }; - }; - - var cssUtil = { - // split a string with css styles into an object with key/values - split: function split(cssText) { - var styles = {}; - - cssText.split(';').forEach(function (style) { - if (style.trim() != '') { - var parts = style.split(':'); - var key = parts[0].trim(); - var value = parts[1].trim(); - styles[key] = value; - } - }); - - return styles; - }, - - // build a css text string from an object with key/values - join: function join(styles) { - return Object.keys(styles).map(function (key) { - return key + ': ' + styles[key]; - }).join('; '); - } - }; + function extend(dest, src, merge) { + var keys = Object.keys(src); + var i = 0; + while (i < keys.length) { + if (!merge || (merge && dest[keys[i]] === undefined)) { + dest[keys[i]] = src[keys[i]]; + } + i++; + } + return dest; + } /** - * Append a string with css styles to an element - * @param {Element} element - * @param {String} cssText + * merge the values from src in the dest. + * means that properties that exist in dest will not be overwritten by src + * @param {Object} dest + * @param {Object} src + * @returns {Object} dest */ - exports.addCssText = function (element, cssText) { - var currentStyles = cssUtil.split(element.style.cssText); - var newStyles = cssUtil.split(cssText); - var styles = exports.extend(currentStyles, newStyles); - - element.style.cssText = cssUtil.join(styles); - }; + function merge(dest, src) { + return extend(dest, src, true); + } /** - * Remove a string with css styles from an element - * @param {Element} element - * @param {String} cssText + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] */ - exports.removeCssText = function (element, cssText) { - var styles = cssUtil.split(element.style.cssText); - var removeStyles = cssUtil.split(cssText); + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - for (var key in removeStyles) { - if (removeStyles.hasOwnProperty(key)) { - delete styles[key]; - } - } + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - element.style.cssText = cssUtil.join(styles); - }; + if (properties) { + extend(childP, properties); + } + } /** - * https://gist.github.com/mjijackson/5311256 - * @param h - * @param s - * @param v - * @returns {{r: number, g: number, b: number}} - * @constructor - */ - exports.HSVToRGB = function (h, s, v) { - var r, g, b; - - var i = Math.floor(h * 6); - var f = h * 6 - i; - var p = v * (1 - s); - var q = v * (1 - f * s); - var t = v * (1 - (1 - f) * s); - - switch (i % 6) { - case 0: - r = v, g = t, b = p;break; - case 1: - r = q, g = v, b = p;break; - case 2: - r = p, g = v, b = t;break; - case 3: - r = p, g = q, b = v;break; - case 4: - r = t, g = p, b = v;break; - case 5: - r = v, g = p, b = q;break; - } - - return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; - }; - - exports.HSVToHex = function (h, s, v) { - var rgb = exports.HSVToRGB(h, s, v); - return exports.RGBToHex(rgb.r, rgb.g, rgb.b); - }; - - exports.hexToHSV = function (hex) { - var rgb = exports.hexToRGB(hex); - return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); - }; - - exports.isValidHex = function (hex) { - var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); - return isOk; - }; - - exports.isValidRGB = function (rgb) { - rgb = rgb.replace(' ', ''); - var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb); - return isOk; - }; - exports.isValidRGBA = function (rgba) { - rgba = rgba.replace(' ', ''); - var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba); - return isOk; - }; + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} + */ + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } /** - * This recursively redirects the prototype of JSON objects to the referenceObject - * This is used for default options. - * - * @param referenceObject - * @returns {*} + * let a boolean value also be a function that must return a boolean + * this first item in args will be used as the context + * @param {Boolean|Function} val + * @param {Array} [args] + * @returns {Boolean} */ - exports.selectiveBridgeObject = function (fields, referenceObject) { - if (typeof referenceObject == 'object') { - var objectTo = Object.create(referenceObject); - for (var i = 0; i < fields.length; i++) { - if (referenceObject.hasOwnProperty(fields[i])) { - if (typeof referenceObject[fields[i]] == 'object') { - objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]); - } - } + function boolOrFn(val, args) { + if (typeof val == TYPE_FUNCTION) { + return val.apply(args ? args[0] || undefined : undefined, args); } - return objectTo; - } else { - return null; - } - }; + return val; + } /** - * This recursively redirects the prototype of JSON objects to the referenceObject - * This is used for default options. - * - * @param referenceObject + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 * @returns {*} */ - exports.bridgeObject = function (referenceObject) { - if (typeof referenceObject == 'object') { - var objectTo = Object.create(referenceObject); - for (var i in referenceObject) { - if (referenceObject.hasOwnProperty(i)) { - if (typeof referenceObject[i] == 'object') { - objectTo[i] = exports.bridgeObject(referenceObject[i]); - } - } - } - return objectTo; - } else { - return null; - } - }; + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } /** - * this is used to set the options of subobjects in the options object. A requirement of these subobjects - * is that they have an 'enabled' element which is optional for the user but mandatory for the program. - * - * @param [object] mergeTarget | this is either this.options or the options used for the groups. - * @param [object] options | options - * @param [String] option | this is the option key in the options argument - * @private + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - exports.mergeOptions = function (mergeTarget, options, option) { - var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } - if (options[option] === null) { - mergeTarget[option] = undefined; - delete mergeTarget[option]; - } else { - if (options[option] !== undefined) { - if (typeof options[option] === 'boolean') { - mergeTarget[option].enabled = options[option]; - } else { - if (options[option].enabled === undefined) { - mergeTarget[option].enabled = true; - } - for (var prop in options[option]) { - if (options[option].hasOwnProperty(prop)) { - mergeTarget[option][prop] = options[option][prop]; - } + /** + * removeEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler + */ + function removeEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.removeEventListener(type, handler, false); + }); + } + + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; } - } + node = node.parentNode; } - } - }; + return false; + } /** - * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses - * this function will then iterate in both directions over this sorted list to find all visible items. - * - * @param {Item[]} orderedItems | Items ordered by start - * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher - * @param {String} field - * @param {String} field2 - * @returns {number} - * @private + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found */ - exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) { - var maxIterations = 10000; - var iteration = 0; - var low = 0; - var high = orderedItems.length - 1; - - while (low <= high && iteration < maxIterations) { - var middle = Math.floor((low + high) / 2); + function inStr(str, find) { + return str.indexOf(find) > -1; + } - var item = orderedItems[middle]; - var value = field2 === undefined ? item[field] : item[field][field2]; + /** + * split string on whitespace + * @param {String} str + * @returns {Array} words + */ + function splitStr(str) { + return str.trim().split(/\s+/g); + } - var searchResult = searchFunction(value); - if (searchResult == 0) { - // jihaa, found a visible item! - return middle; - } else if (searchResult == -1) { - // it is too small --> increase low - low = middle + 1; + /** + * find if a array contains the object using indexOf or a simple polyFill + * @param {Array} src + * @param {String} find + * @param {String} [findByKey] + * @return {Boolean|Number} false when not found, or the index + */ + function inArray(src, find, findByKey) { + if (src.indexOf && !findByKey) { + return src.indexOf(find); } else { - // it is too big --> decrease high - high = middle - 1; + var i = 0; + while (i < src.length) { + if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { + return i; + } + i++; + } + return -1; } + } - iteration++; - } - - return -1; - }; + /** + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} + */ + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } /** - * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of - * two values, we return either the one before or the one after, depending on user input - * If it is found, we return the index, else -1. - * - * @param {Array} orderedItems - * @param {{start: number, end: number}} target - * @param {String} field - * @param {String} sidePreference 'before' or 'after' - * @returns {number} - * @private + * unique array with objects based on a key (like 'id') or just by the array's value + * @param {Array} src [{id:1},{id:2},{id:1}] + * @param {String} [key] + * @param {Boolean} [sort=False] + * @returns {Array} [{id:1},{id:2}] */ - exports.binarySearchValue = function (orderedItems, target, field, sidePreference) { - var maxIterations = 10000; - var iteration = 0; - var low = 0; - var high = orderedItems.length - 1; - var prevValue, value, nextValue, middle; + function uniqueArray(src, key, sort) { + var results = []; + var values = []; + var i = 0; - while (low <= high && iteration < maxIterations) { - // get a new guess - middle = Math.floor(0.5 * (high + low)); - prevValue = orderedItems[Math.max(0, middle - 1)][field]; - value = orderedItems[middle][field]; - nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field]; + while (i < src.length) { + var val = key ? src[i][key] : src[i]; + if (inArray(values, val) < 0) { + results.push(src[i]); + } + values[i] = val; + i++; + } - if (value == target) { - // we found the target - return middle; - } else if (prevValue < target && value > target) { - // target is in between of the previous and the current - return sidePreference == 'before' ? Math.max(0, middle - 1) : middle; - } else if (value < target && nextValue > target) { - // target is in between of the current and the next - return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1); - } else { - // didnt find the target, we need to change our boundaries. - if (value < target) { - // it is too small --> increase low - low = middle + 1; - } else { - // it is too big --> decrease high - high = middle - 1; - } + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); + } } - iteration++; - } - // didnt find anything. Return -1. - return -1; - }; + return results; + } - /* - * Easing Functions - inspired from http://gizma.com/easing/ - * only considering the t value for the range [0, 1] => [0, 1] - * https://gist.github.com/gre/1650294 + /** + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ - exports.easingFunctions = { - // no easing, no acceleration - linear: function linear(t) { - return t; - }, - // accelerating from zero velocity - easeInQuad: function easeInQuad(t) { - return t * t; - }, - // decelerating to zero velocity - easeOutQuad: function easeOutQuad(t) { - return t * (2 - t); - }, - // acceleration until halfway, then deceleration - easeInOutQuad: function easeInOutQuad(t) { - return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; - }, - // accelerating from zero velocity - easeInCubic: function easeInCubic(t) { - return t * t * t; - }, - // decelerating to zero velocity - easeOutCubic: function easeOutCubic(t) { - return --t * t * t + 1; - }, - // acceleration until halfway, then deceleration - easeInOutCubic: function easeInOutCubic(t) { - return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; - }, - // accelerating from zero velocity - easeInQuart: function easeInQuart(t) { - return t * t * t * t; - }, - // decelerating to zero velocity - easeOutQuart: function easeOutQuart(t) { - return 1 - --t * t * t * t; - }, - // acceleration until halfway, then deceleration - easeInOutQuart: function easeInOutQuart(t) { - return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; - }, - // accelerating from zero velocity - easeInQuint: function easeInQuint(t) { - return t * t * t * t * t; - }, - // decelerating to zero velocity - easeOutQuint: function easeOutQuint(t) { - return 1 + --t * t * t * t * t; - }, - // acceleration until halfway, then deceleration - easeInOutQuint: function easeInOutQuint(t) { - return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; - } - }; - -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { - - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. - 'use strict'; - - module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(11); - -/***/ }, -/* 11 */ -/***/ function(module, exports, __webpack_require__) { - - /* WEBPACK VAR INJECTION */(function(module) {//! moment.js - //! version : 2.10.3 - //! authors : Tim Wood, Iskren Chernev, Moment.js contributors - //! license : MIT - //! momentjs.com - - (function (global, factory) { - true ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() - }(this, function () { 'use strict'; - - var hookCallback; - - function utils_hooks__hooks () { - return hookCallback.apply(null, arguments); - } + function prefixed(obj, property) { + var prefix, prop; + var camelProp = property[0].toUpperCase() + property.slice(1); - // This is done to register the method called with moment() - // without creating circular dependencies. - function setHookCallback (callback) { - hookCallback = callback; - } + var i = 0; + while (i < VENDOR_PREFIXES.length) { + prefix = VENDOR_PREFIXES[i]; + prop = (prefix) ? prefix + camelProp : property; - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; + if (prop in obj) { + return prop; + } + i++; } + return undefined; + } - function isDate(input) { - return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; - } + /** + * get a unique id + * @returns {number} uniqueId + */ + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } + /** + * get the window object of an element + * @param {HTMLElement} element + * @returns {DocumentView|Window} + */ + function getWindowForElement(element) { + var doc = element.ownerDocument; + return (doc.defaultView || doc.parentWindow); + } - function hasOwnProp(a, b) { - return Object.prototype.hasOwnProperty.call(a, b); - } + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } - } + var SUPPORT_TOUCH = ('ontouchstart' in window); + var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; + var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; - } + var COMPUTE_INTERVAL = 25; - return a; - } + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; - function create_utc__createUTC (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, true).utc(); - } + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; - function defaultParsingFlags() { - // We need to deep clone this object. - return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso : false - }; - } + var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; + var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; + var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; - function getParsingFlags(m) { - if (m._pf == null) { - m._pf = defaultParsingFlags(); - } - return m._pf; - } + var PROPS_XY = ['x', 'y']; + var PROPS_CLIENT_XY = ['clientX', 'clientY']; - function valid__isValid(m) { - if (m._isValid == null) { - var flags = getParsingFlags(m); - m._isValid = !isNaN(m._d.getTime()) && - flags.overflow < 0 && - !flags.empty && - !flags.invalidMonth && - !flags.nullInput && - !flags.invalidFormat && - !flags.userInvalidated; + /** + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} + * @constructor + */ + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; - if (m._strict) { - m._isValid = m._isValid && - flags.charsLeftOver === 0 && - flags.unusedTokens.length === 0 && - flags.bigHour === undefined; - } + // smaller wrapper around the handler, for the scope and the enabled state of the manager, + // so when disabled the input events are completely bypassed. + this.domHandler = function(ev) { + if (boolOrFn(manager.options.enable, [manager])) { + self.handler(ev); } - return m._isValid; - } + }; - function valid__createInvalid (flags) { - var m = create_utc__createUTC(NaN); - if (flags != null) { - extend(getParsingFlags(m), flags); - } - else { - getParsingFlags(m).userInvalidated = true; - } + this.init(); - return m; - } + } - var momentProperties = utils_hooks__hooks.momentProperties = []; + Input.prototype = { + /** + * should handle the inputEvent data and trigger the callback + * @virtual + */ + handler: function() { }, - function copyConfig(to, from) { - var i, prop, val; + /** + * bind the events + */ + init: function() { + this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + }, - if (typeof from._isAMomentObject !== 'undefined') { - to._isAMomentObject = from._isAMomentObject; - } - if (typeof from._i !== 'undefined') { - to._i = from._i; - } - if (typeof from._f !== 'undefined') { - to._f = from._f; - } - if (typeof from._l !== 'undefined') { - to._l = from._l; - } - if (typeof from._strict !== 'undefined') { - to._strict = from._strict; - } - if (typeof from._tzm !== 'undefined') { - to._tzm = from._tzm; - } - if (typeof from._isUTC !== 'undefined') { - to._isUTC = from._isUTC; - } - if (typeof from._offset !== 'undefined') { - to._offset = from._offset; - } - if (typeof from._pf !== 'undefined') { - to._pf = getParsingFlags(from); - } - if (typeof from._locale !== 'undefined') { - to._locale = from._locale; - } + /** + * unbind the events + */ + destroy: function() { + this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + } + }; - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } - } + /** + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} + */ + function createInputInstance(manager) { + var Type; + var inputClass = manager.options.inputClass; - return to; + if (inputClass) { + Type = inputClass; + } else if (SUPPORT_POINTER_EVENTS) { + Type = PointerEventInput; + } else if (SUPPORT_ONLY_TOUCH) { + Type = TouchInput; + } else if (!SUPPORT_TOUCH) { + Type = MouseInput; + } else { + Type = TouchMouseInput; } + return new (Type)(manager, inputHandler); + } - var updateInProgress = false; + /** + * handle input events + * @param {Manager} manager + * @param {String} eventType + * @param {Object} input + */ + function inputHandler(manager, eventType, input) { + var pointersLen = input.pointers.length; + var changedPointersLen = input.changedPointers.length; + var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); + var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); - // Moment prototype object - function Moment(config) { - copyConfig(this, config); - this._d = new Date(+config._d); - // Prevent infinite loop in case updateOffset creates new moment - // objects. - if (updateInProgress === false) { - updateInProgress = true; - utils_hooks__hooks.updateOffset(this); - updateInProgress = false; - } - } + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; - function isMoment (obj) { - return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); + if (isFirst) { + manager.session = {}; } - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } + // compute scale, rotation etc + computeInputData(manager, input); - return value; - } + // emit secret event + manager.emit('hammer.input', input); - function compareArrays(array1, array2, dontConvert) { - var len = Math.min(array1.length, array2.length), - lengthDiff = Math.abs(array1.length - array2.length), - diffs = 0, - i; - for (i = 0; i < len; i++) { - if ((dontConvert && array1[i] !== array2[i]) || - (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { - diffs++; - } - } - return diffs + lengthDiff; - } + manager.recognize(input); + manager.session.prevInput = input; + } - function Locale() { - } + /** + * extend the data with some usable properties like scale, rotate, velocity etc + * @param {Object} manager + * @param {Object} input + */ + function computeInputData(manager, input) { + var session = manager.session; + var pointers = input.pointers; + var pointersLength = pointers.length; - var locales = {}; - var globalLocale; + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); + } - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; + // to compute scale and rotation we need to store the multiple touches + if (pointersLength > 1 && !session.firstMultiple) { + session.firstMultiple = simpleCloneInputData(input); + } else if (pointersLength === 1) { + session.firstMultiple = false; } - // pick the locale from the array - // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each - // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root - function chooseLocale(names) { - var i = 0, j, next, locale, split; + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - while (i < names.length) { - split = normalizeLocale(names[i]).split('-'); - j = split.length; - next = normalizeLocale(names[i + 1]); - next = next ? next.split('-') : null; - while (j > 0) { - locale = loadLocale(split.slice(0, j).join('-')); - if (locale) { - return locale; - } - if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { - //the next array item is better than a shallower substring of this one - break; - } - j--; - } - i++; - } - return null; - } + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; - function loadLocale(name) { - var oldLocale = null; - // TODO: Find a better way to register and load all the locales in Node - if (!locales[name] && typeof module !== 'undefined' && - module && module.exports) { - try { - oldLocale = globalLocale._abbr; - !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); - // because defineLocale currently also sets the global locale, we - // want to undo that for lazy loaded locales - locale_locales__getSetGlobalLocale(oldLocale); - } catch (e) { } - } - return locales[name]; - } + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); - // This function will load locale and then set the global locale. If - // no arguments are passed in, it will simply return the current global - // locale key. - function locale_locales__getSetGlobalLocale (key, values) { - var data; - if (key) { - if (typeof values === 'undefined') { - data = locale_locales__getLocale(key); - } - else { - data = defineLocale(key, values); - } + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - if (data) { - // moment.duration._locale = moment._locale = data; - globalLocale = data; - } - } + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - return globalLocale._abbr; + computeIntervalInputData(session, input); + + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; } + input.target = target; + } - function defineLocale (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; - // backwards compat for now: also set the locale - locale_locales__getSetGlobalLocale(name); + if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { + prevDelta = session.prevDelta = { + x: prevInput.deltaX || 0, + y: prevInput.deltaY || 0 + }; - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } + offset = session.offsetDelta = { + x: center.x, + y: center.y + }; } - // returns locale data - function locale_locales__getLocale (key) { - var locale; + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); + } - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; - } + /** + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input + */ + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - if (!key) { - return globalLocale; - } + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } + var v = getVelocity(deltaTime, deltaX, deltaY); + velocityX = v.x; + velocityY = v.y; + velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; + direction = getDirection(deltaX, deltaY); - return chooseLocale(key); + session.lastInterval = input; + } else { + // use latest velocity info if it doesn't overtake a minimum period + velocity = last.velocity; + velocityX = last.velocityX; + velocityY = last.velocityY; + direction = last.direction; } - var aliases = {}; + input.velocity = velocity; + input.velocityX = velocityX; + input.velocityY = velocityY; + input.direction = direction; + } - function addUnitAlias (unit, shorthand) { - var lowerCase = unit.toLowerCase(); - aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; + /** + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData + */ + function simpleCloneInputData(input) { + // make a simple copy of the pointers because we will get a reference if we don't + // we only need clientXY for the calculations + var pointers = []; + var i = 0; + while (i < input.pointers.length) { + pointers[i] = { + clientX: round(input.pointers[i].clientX), + clientY: round(input.pointers[i].clientY) + }; + i++; } - function normalizeUnits(units) { - return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; - } + return { + timeStamp: now(), + pointers: pointers, + center: getCenter(pointers), + deltaX: input.deltaX, + deltaY: input.deltaY + }; + } - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; + /** + * get the center of all the pointers + * @param {Array} pointers + * @return {Object} center contains `x` and `y` properties + */ + function getCenter(pointers) { + var pointersLength = pointers.length; - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } + // no need to loop when only one touch + if (pointersLength === 1) { + return { + x: round(pointers[0].clientX), + y: round(pointers[0].clientY) + }; + } - return normalizedInput; + var x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; + i++; } - function makeGetSet (unit, keepTime) { - return function (value) { - if (value != null) { - get_set__set(this, unit, value); - utils_hooks__hooks.updateOffset(this, keepTime); - return this; - } else { - return get_set__get(this, unit); - } - }; + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; + } + + /** + * calculate the velocity between two points. unit is in px per ms. + * @param {Number} deltaTime + * @param {Number} x + * @param {Number} y + * @return {Object} velocity `x` and `y` + */ + function getVelocity(deltaTime, x, y) { + return { + x: x / deltaTime || 0, + y: y / deltaTime || 0 + }; + } + + /** + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction + */ + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; } - function get_set__get (mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; } + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; + } - function get_set__set (mom, unit, value) { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + /** + * calculate the absolute distance between two points + * @param {Object} p1 {x, y} + * @param {Object} p2 {x, y} + * @param {Array} [props] containing x and y keys + * @return {Number} distance + */ + function getDistance(p1, p2, props) { + if (!props) { + props = PROPS_XY; } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; - // MOMENTS + return Math.sqrt((x * x) + (y * y)); + } - function getSet (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } - } else { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - return this[units](value); - } - } - return this; + /** + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle + */ + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; + } + + /** + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation + */ + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); + } + + /** + * calculate the scale factor between two pointersets + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} scale + */ + function getScale(start, end) { + return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); + } - function zeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; + var MOUSE_INPUT_MAP = { + mousedown: INPUT_START, + mousemove: INPUT_MOVE, + mouseup: INPUT_END + }; - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } + var MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; + /** + * Mouse events input + * @constructor + * @extends Input + */ + function MouseInput() { + this.evEl = MOUSE_ELEMENT_EVENTS; + this.evWin = MOUSE_WINDOW_EVENTS; - var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; + this.allow = true; // used by Input.TouchMouse to disable mouse events + this.pressed = false; // mousedown state - var formatFunctions = {}; + Input.apply(this, arguments); + } - var formatTokenFunctions = {}; + inherit(MouseInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; - // token: 'M' - // padded: ['MM', 2] - // ordinal: 'Mo' - // callback: function () { this.month() + 1 } - function addFormatToken (token, padded, ordinal, callback) { - var func = callback; - if (typeof callback === 'string') { - func = function () { - return this[callback](); - }; - } - if (token) { - formatTokenFunctions[token] = func; - } - if (padded) { - formatTokenFunctions[padded[0]] = function () { - return zeroFill(func.apply(this, arguments), padded[1], padded[2]); - }; - } - if (ordinal) { - formatTokenFunctions[ordinal] = function () { - return this.localeData().ordinal(func.apply(this, arguments), token); - }; + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = true; } - } - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); + if (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; } - return input.replace(/\\/g, ''); - } - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; + } - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } + if (eventType & INPUT_END) { + this.pressed = false; } - return function (mom) { - var output = ''; - for (i = 0; i < length; i++) { - output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; - } - return output; - }; + this.callback(this.manager, eventType, { + pointers: [ev], + changedPointers: [ev], + pointerType: INPUT_TYPE_MOUSE, + srcEvent: ev + }); } + }); - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } + var POINTER_INPUT_MAP = { + pointerdown: INPUT_START, + pointermove: INPUT_MOVE, + pointerup: INPUT_END, + pointercancel: INPUT_CANCEL, + pointerout: INPUT_CANCEL + }; - format = expandFormat(format, m.localeData()); + // in IE10 the pointer types is defined as an enum + var IE10_POINTER_TYPE_ENUM = { + 2: INPUT_TYPE_TOUCH, + 3: INPUT_TYPE_PEN, + 4: INPUT_TYPE_MOUSE, + 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 + }; - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } + var POINTER_ELEMENT_EVENTS = 'pointerdown'; + var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - return formatFunctions[format](m); - } + // IE10 has prefixed support, and case-sensitive + if (window.MSPointerEvent) { + POINTER_ELEMENT_EVENTS = 'MSPointerDown'; + POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; + } - function expandFormat(format, locale) { - var i = 5; + /** + * Pointer events input + * @constructor + * @extends Input + */ + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } + Input.apply(this, arguments); - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } + this.store = (this.manager.session.pointerEvents = []); + } - return format; - } + inherit(PointerEventInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - var match1 = /\d/; // 0 - 9 - var match2 = /\d\d/; // 00 - 99 - var match3 = /\d{3}/; // 000 - 999 - var match4 = /\d{4}/; // 0000 - 9999 - var match6 = /[+-]?\d{6}/; // -999999 - 999999 - var match1to2 = /\d\d?/; // 0 - 99 - var match1to3 = /\d{1,3}/; // 0 - 999 - var match1to4 = /\d{1,4}/; // 0 - 9999 - var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - var matchUnsigned = /\d+/; // 0 - inf - var matchSigned = /[+-]?\d+/; // -inf - inf + var isTouch = (pointerType == INPUT_TYPE_TOUCH); - var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + // start and mouse must be down + if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { + if (storeIndex < 0) { + store.push(ev); + storeIndex = store.length - 1; + } + } else if (eventType & (INPUT_END | INPUT_CANCEL)) { + removePointer = true; + } - // any word (or two) characters or numbers including two/three word month in arabic. - var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; + } - var regexes = {}; + // update the event in the store + store[storeIndex] = ev; - function addRegexToken (token, regex, strictRegex) { - regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { - return (isStrict && strictRegex) ? strictRegex : regex; - }; - } + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); - function getParseRegexForToken (token, config) { - if (!hasOwnProp(regexes, token)) { - return new RegExp(unescapeFormat(token)); + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); } - - return regexes[token](config._strict, config._locale); } + }); - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function unescapeFormat(s) { - return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - var tokens = {}; + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - function addParseToken (token, callback) { - var i, func = callback; - if (typeof token === 'string') { - token = [token]; + /** + * Touch events input + * @constructor + * @extends Input + */ + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; + + Input.apply(this, arguments); + } + + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; } - if (typeof callback === 'number') { - func = function (input, array) { - array[callback] = toInt(input); - }; + + if (!this.started) { + return; } - for (i = 0; i < token.length; i++) { - tokens[token[i]] = func; + + var touches = normalizeSingleTouches.call(this, ev, type); + + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; } - } - function addWeekParseToken (token, callback) { - addParseToken(token, function (input, array, config, token) { - config._w = config._w || {}; - callback(input, config._w, config, token); + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev }); } + }); - function addTimeToArrayFromToken(token, input, config) { - if (input != null && hasOwnProp(tokens, token)) { - tokens[token](input, config._a, config, token); - } + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function normalizeSingleTouches(ev, type) { + var all = toArray(ev.touches); + var changed = toArray(ev.changedTouches); + + if (type & (INPUT_END | INPUT_CANCEL)) { + all = uniqueArray(all.concat(changed), 'identifier', true); } - var YEAR = 0; - var MONTH = 1; - var DATE = 2; - var HOUR = 3; - var MINUTE = 4; - var SECOND = 5; - var MILLISECOND = 6; + return [all, changed]; + } - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } + var TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - // FORMATTING + var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - addFormatToken('M', ['MM', 2], 'Mo', function () { - return this.month() + 1; - }); + /** + * Multi-user touch events input + * @constructor + * @extends Input + */ + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; - addFormatToken('MMM', 0, 0, function (format) { - return this.localeData().monthsShort(this, format); - }); + Input.apply(this, arguments); + } - addFormatToken('MMMM', 0, 0, function (format) { - return this.localeData().months(this, format); - }); + inherit(TouchInput, Input, { + handler: function MTEhandler(ev) { + var type = TOUCH_INPUT_MAP[ev.type]; + var touches = getTouches.call(this, ev, type); + if (!touches) { + return; + } - // ALIASES + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); - addUnitAlias('month', 'M'); + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function getTouches(ev, type) { + var allTouches = toArray(ev.touches); + var targetIds = this.targetIds; - // PARSING + // when there is only one touch, the process can be simplified + if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { + targetIds[allTouches[0].identifier] = true; + return [allTouches, allTouches]; + } - addRegexToken('M', match1to2); - addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', matchWord); - addRegexToken('MMMM', matchWord); + var i, + targetTouches, + changedTouches = toArray(ev.changedTouches), + changedTargetTouches = [], + target = this.target; - addParseToken(['M', 'MM'], function (input, array) { - array[MONTH] = toInt(input) - 1; + // get target touches from touches + targetTouches = allTouches.filter(function(touch) { + return hasParent(touch.target, target); }); - addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { - var month = config._locale.monthsParse(input, token, config._strict); - // if we didn't find a month name, mark the date as invalid. - if (month != null) { - array[MONTH] = month; - } else { - getParsingFlags(config).invalidMonth = input; + // collect touches + if (type === INPUT_START) { + i = 0; + while (i < targetTouches.length) { + targetIds[targetTouches[i].identifier] = true; + i++; } - }); + } - // LOCALES + // filter changed touches to only contain touches that exist in the collected target ids + i = 0; + while (i < changedTouches.length) { + if (targetIds[changedTouches[i].identifier]) { + changedTargetTouches.push(changedTouches[i]); + } - var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); - function localeMonths (m) { - return this._months[m.month()]; + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; + } + i++; } - var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); - function localeMonthsShort (m) { - return this._monthsShort[m.month()]; + if (!changedTargetTouches.length) { + return; } - function localeMonthsParse (monthName, format, strict) { - var i, mom, regex; - - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; + } - for (i = 0; i < 12; i++) { - // make the regex if we don't have it already - mom = create_utc__createUTC([2000, i]); - if (strict && !this._longMonthsParse[i]) { - this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); - this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); - } - if (!strict && !this._monthsParse[i]) { - regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); - this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { - return i; - } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { - return i; - } else if (!strict && this._monthsParse[i].test(monthName)) { - return i; - } - } - } + /** + * Combined touch and mouse input + * + * Touch has a higher priority then mouse, and while touching no mouse events are allowed. + * This because touch devices also emit mouse events while doing a touch. + * + * @constructor + * @extends Input + */ + function TouchMouseInput() { + Input.apply(this, arguments); - // MOMENTS + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } - function setMonth (mom, value) { - var dayOfMonth; + inherit(TouchMouseInput, Input, { + /** + * handle mouse and touch events + * @param {Hammer} manager + * @param {String} inputEvent + * @param {Object} inputData + */ + handler: function TMEhandler(manager, inputEvent, inputData) { + var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), + isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); - // TODO: Move this out of here! - if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; - } + // when we're in a touch event, so block all upcoming mouse events + // most mobile browser also emit mouseevents, right after touchstart + if (isTouch) { + this.mouse.allow = false; + } else if (isMouse && !this.mouse.allow) { + return; } - dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } - - function getSetMonth (value) { - if (value != null) { - setMonth(this, value); - utils_hooks__hooks.updateOffset(this, true); - return this; - } else { - return get_set__get(this, 'Month'); + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; } - } - function getDaysInMonth () { - return daysInMonth(this.year(), this.month()); + this.callback(manager, inputEvent, inputData); + }, + + /** + * remove the event listeners + */ + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); } + }); - function checkOverflow (m) { - var overflow; - var a = m._a; + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - if (a && getParsingFlags(m).overflow === -2) { - overflow = - a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : - a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : - a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : - a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : - a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : - a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : - -1; + // magical touchAction value + var TOUCH_ACTION_COMPUTE = 'compute'; + var TOUCH_ACTION_AUTO = 'auto'; + var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented + var TOUCH_ACTION_NONE = 'none'; + var TOUCH_ACTION_PAN_X = 'pan-x'; + var TOUCH_ACTION_PAN_Y = 'pan-y'; - if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } + /** + * Touch Action + * sets the touchAction property or uses the js alternative + * @param {Manager} manager + * @param {String} value + * @constructor + */ + function TouchAction(manager, value) { + this.manager = manager; + this.set(value); + } - getParsingFlags(m).overflow = overflow; + TouchAction.prototype = { + /** + * set the touchAction value on the element or enable the polyfill + * @param {String} value + */ + set: function(value) { + // find out the touch-action by the event handlers + if (value == TOUCH_ACTION_COMPUTE) { + value = this.compute(); } - return m; - } - - function warn(msg) { - if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); + if (NATIVE_TOUCH_ACTION) { + this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; } - } + this.actions = value.toLowerCase().trim(); + }, - function deprecate(msg, fn) { - var firstTime = true, - msgWithStack = msg + '\n' + (new Error()).stack; + /** + * just re-set the touchAction value + */ + update: function() { + this.set(this.manager.options.touchAction); + }, - return extend(function () { - if (firstTime) { - warn(msgWithStack); - firstTime = false; + /** + * compute the value for the touchAction property based on the recognizer's settings + * @returns {String} value + */ + compute: function() { + var actions = []; + each(this.manager.recognizers, function(recognizer) { + if (boolOrFn(recognizer.options.enable, [recognizer])) { + actions = actions.concat(recognizer.getTouchAction()); } - return fn.apply(this, arguments); - }, fn); - } - - var deprecations = {}; + }); + return cleanTouchActions(actions.join(' ')); + }, - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - warn(msg); - deprecations[name] = true; + /** + * this method is called on each input cycle and provides the preventing of the browser behavior + * @param {Object} input + */ + preventDefaults: function(input) { + // not needed with native support for the touchAction property + if (NATIVE_TOUCH_ACTION) { + return; } - } - - utils_hooks__hooks.suppressDeprecationWarnings = false; - var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; + var srcEvent = input.srcEvent; + var direction = input.offsetDirection; - var isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ]; + // if the touch action did prevented once this session + if (this.manager.session.prevented) { + srcEvent.preventDefault(); + return; + } - // iso time formats and regexes - var isoTimes = [ - ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], - ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], - ['HH:mm', /(T| )\d\d:\d\d/], - ['HH', /(T| )\d\d/] - ]; + var actions = this.actions; + var hasNone = inStr(actions, TOUCH_ACTION_NONE); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + if (hasNone || + (hasPanY && direction & DIRECTION_HORIZONTAL) || + (hasPanX && direction & DIRECTION_VERTICAL)) { + return this.preventSrc(srcEvent); + } + }, - // date from iso format - function configFromISO(config) { - var i, l, - string = config._i, - match = from_string__isoRegex.exec(string); + /** + * call preventDefault to prevent the browser's default behavior (scrolling in most cases) + * @param {Object} srcEvent + */ + preventSrc: function(srcEvent) { + this.manager.session.prevented = true; + srcEvent.preventDefault(); + } + }; - if (match) { - getParsingFlags(config).iso = true; - for (i = 0, l = isoDates.length; i < l; i++) { - if (isoDates[i][1].exec(string)) { - // match[5] should be 'T' or undefined - config._f = isoDates[i][0] + (match[6] || ' '); - break; - } - } - for (i = 0, l = isoTimes.length; i < l; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(matchOffset)) { - config._f += 'Z'; - } - configFromStringAndFormat(config); - } else { - config._isValid = false; - } + /** + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} + */ + function cleanTouchActions(actions) { + // none + if (inStr(actions, TOUCH_ACTION_NONE)) { + return TOUCH_ACTION_NONE; } - // date from iso format or fallback - function configFromString(config) { - var matched = aspNetJsonRegex.exec(config._i); + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - if (matched !== null) { - config._d = new Date(+matched[1]); - return; - } + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + } - configFromISO(config); - if (config._isValid === false) { - delete config._isValid; - utils_hooks__hooks.createFromInputFallback(config); - } + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; } - utils_hooks__hooks.createFromInputFallback = deprecate( - 'moment construction falls back to js Date. This is ' + - 'discouraged and will be removed in upcoming major ' + - 'release. Please refer to ' + - 'https://github.com/moment/moment/issues/1407 for more info.', - function (config) { - config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); - } - ); + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; + } - function createDate (y, m, d, h, M, s, ms) { - //can't just apply() to create a date: - //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply - var date = new Date(y, m, d, h, M, s, ms); + return TOUCH_ACTION_AUTO; + } - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } + /** + * Recognizer flow explained; * + * All recognizers have the initial state of POSSIBLE when a input session starts. + * The definition of a input session is from the first input until the last input, with all it's movement in it. * + * Example session for mouse-input: mousedown -> mousemove -> mouseup + * + * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed + * which determines with state it should be. + * + * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to + * POSSIBLE to give it another change on the next cycle. + * + * Possible + * | + * +-----+---------------+ + * | | + * +-----+-----+ | + * | | | + * Failed Cancelled | + * +-------+------+ + * | | + * Recognized Began + * | + * Changed + * | + * Ended/Recognized + */ + var STATE_POSSIBLE = 1; + var STATE_BEGAN = 2; + var STATE_CHANGED = 4; + var STATE_ENDED = 8; + var STATE_RECOGNIZED = STATE_ENDED; + var STATE_CANCELLED = 16; + var STATE_FAILED = 32; - function createUTCDate (y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } + /** + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options + */ + function Recognizer(options) { + this.id = uniqueId(); - addFormatToken(0, ['YY', 2], 0, function () { - return this.year() % 100; - }); + this.manager = null; + this.options = merge(options || {}, this.defaults); - addFormatToken(0, ['YYYY', 4], 0, 'year'); - addFormatToken(0, ['YYYYY', 5], 0, 'year'); - addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - // ALIASES + this.state = STATE_POSSIBLE; - addUnitAlias('year', 'y'); + this.simultaneous = {}; + this.requireFail = []; + } - // PARSING + Recognizer.prototype = { + /** + * @virtual + * @type {Object} + */ + defaults: {}, - addRegexToken('Y', matchSigned); - addRegexToken('YY', match1to2, match2); - addRegexToken('YYYY', match1to4, match4); - addRegexToken('YYYYY', match1to6, match6); - addRegexToken('YYYYYY', match1to6, match6); + /** + * set options + * @param {Object} options + * @return {Recognizer} + */ + set: function(options) { + extend(this.options, options); - addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); - addParseToken('YY', function (input, array) { - array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + // also update the touchAction, in case something changed about the directions/enabled state + this.manager && this.manager.touchAction.update(); + return this; + }, - // HELPERS + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; + } - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); + } + return this; + }, - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - } + /** + * drop the simultaneous link. it doesnt remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRecognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { + return this; + } - // HOOKS + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; + }, - utils_hooks__hooks.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; + /** + * recognizer can only run when an other is failing + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + requireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { + return this; + } - // MOMENTS + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); + } + return this; + }, - var getSetYear = makeGetSet('FullYear', false); + /** + * drop the requireFailure link. it does not remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRequireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { + return this; + } - function getIsLeapYear () { - return isLeapYear(this.year()); - } + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); + } + return this; + }, - addFormatToken('w', ['ww', 2], 'wo', 'week'); - addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + /** + * has require failures boolean + * @returns {boolean} + */ + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, - // ALIASES + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, - addUnitAlias('week', 'w'); - addUnitAlias('isoWeek', 'W'); + /** + * You should use `tryEmit` instead of `emit` directly to check + * that all the needed recognizers has failed before emitting. + * @param {Object} input + */ + emit: function(input) { + var self = this; + var state = this.state; - // PARSING + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); + } - addRegexToken('w', match1to2); - addRegexToken('ww', match1to2, match2); - addRegexToken('W', match1to2); - addRegexToken('WW', match1to2, match2); + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); + } - addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { - week[token.substr(0, 1)] = toInt(input); - }); + emit(); // simple 'eventName' events - // HELPERS + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); + } + }, - // firstDayOfWeek 0 = sun, 6 = sat - // the day of the week that starts the week - // (usually sunday or monday) - // firstDayOfWeekOfYear 0 = sun, 6 = sat - // the first week is the week that contains the first - // of this day of the week - // (eg. ISO weeks use thursday (4)) - function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { - var end = firstDayOfWeekOfYear - firstDayOfWeek, - daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), - adjustedMoment; + /** + * Check that all the require failure recognizers has failed, + * if true, it emits a gesture event, + * otherwise, setup the state to FAILED. + * @param {Object} input + */ + tryEmit: function(input) { + if (this.canEmit()) { + return this.emit(input); + } + // it's failing anyway + this.state = STATE_FAILED; + }, + + /** + * can we emit? + * @returns {boolean} + */ + canEmit: function() { + var i = 0; + while (i < this.requireFail.length) { + if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { + return false; + } + i++; + } + return true; + }, + /** + * update the recognizer + * @param {Object} inputData + */ + recognize: function(inputData) { + // make a new copy of the inputData + // so we can change the inputData without messing up the other recognizers + var inputDataClone = extend({}, inputData); - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; } - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; } - adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; - } + this.state = this.process(inputDataClone); - // LOCALES + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); + } + }, - function localeWeek (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - } + /** + * return the state of the recognizer + * the actual recognizing happens in this method + * @virtual + * @param {Object} inputData + * @returns {Const} STATE + */ + process: function(inputData) { }, // jshint ignore:line - var defaultLocaleWeek = { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. - }; + /** + * return the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, - function localeFirstDayOfWeek () { - return this._week.dow; - } + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } + }; - function localeFirstDayOfYear () { - return this._week.doy; + /** + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state + */ + function stateStr(state) { + if (state & STATE_CANCELLED) { + return 'cancel'; + } else if (state & STATE_ENDED) { + return 'end'; + } else if (state & STATE_CHANGED) { + return 'move'; + } else if (state & STATE_BEGAN) { + return 'start'; } + return ''; + } - // MOMENTS - - function getSetWeek (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); + /** + * direction cons to string + * @param {Const} direction + * @returns {String} + */ + function directionStr(direction) { + if (direction == DIRECTION_DOWN) { + return 'down'; + } else if (direction == DIRECTION_UP) { + return 'up'; + } else if (direction == DIRECTION_LEFT) { + return 'left'; + } else if (direction == DIRECTION_RIGHT) { + return 'right'; } + return ''; + } - function getSetISOWeek (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); + /** + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} + */ + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); } + return otherRecognizer; + } - addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - - // ALIASES - - addUnitAlias('dayOfYear', 'DDD'); - - // PARSING - - addRegexToken('DDD', match1to3); - addRegexToken('DDDD', match3); - addParseToken(['DDD', 'DDDD'], function (input, array, config) { - config._dayOfYear = toInt(input); - }); - - // HELPERS - - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = createUTCDate(year, 0, 1).getUTCDay(); - var daysToAdd; - var dayOfYear; - - d = d === 0 ? 7 : d; - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + /** + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer + */ + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } - return { - year : dayOfYear > 0 ? year : year - 1, - dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, - // MOMENTS + /** + * Used to check if it the recognizer receives valid input, like input.distance > 10. + * @memberof AttrRecognizer + * @param {Object} input + * @returns {Boolean} recognized + */ + attrTest: function(input) { + var optionPointers = this.options.pointers; + return optionPointers === 0 || input.pointers.length === optionPointers; + }, - function getSetDayOfYear (input) { - var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); - } + /** + * Process the input and return the state for the recognizer + * @memberof AttrRecognizer + * @param {Object} input + * @returns {*} State + */ + process: function(input) { + var state = this.state; + var eventType = input.eventType; - // Pick the first defined of two or three arguments. - function defaults(a, b, c) { - if (a != null) { - return a; - } - if (b != null) { - return b; - } - return c; - } + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; + // on cancel input and we've recognized before, return STATE_CANCELLED + if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { + return state | STATE_CANCELLED; + } else if (isRecognized || isValid) { + if (eventType & INPUT_END) { + return state | STATE_ENDED; + } else if (!(state & STATE_BEGAN)) { + return STATE_BEGAN; + } + return state | STATE_CHANGED; } - return [now.getFullYear(), now.getMonth(), now.getDate()]; + return STATE_FAILED; } + }); - // convert an array to a date. - // the array should mirror the parameters below - // note: all values past the year are optional and will default to the lowest possible value. - // [year, month, day , hour, minute, second, millisecond] - function configFromArray (config) { - var i, date, input = [], currentDate, yearToUse; + /** + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - if (config._d) { - return; - } + this.pX = null; + this.pY = null; + } - currentDate = currentDateArray(config); + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); + getTouchAction: function() { + var direction = this.options.direction; + var actions = []; + if (direction & DIRECTION_HORIZONTAL) { + actions.push(TOUCH_ACTION_PAN_Y); + } + if (direction & DIRECTION_VERTICAL) { + actions.push(TOUCH_ACTION_PAN_X); } + return actions; + }, - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); + directionTest: function(input) { + var options = this.options; + var hasMoved = true; + var distance = input.distance; + var direction = input.direction; + var x = input.deltaX; + var y = input.deltaY; - if (config._dayOfYear > daysInYear(yearToUse)) { - getParsingFlags(config)._overflowDayOfYear = true; + // lock to axis? + if (!(direction & options.direction)) { + if (options.direction & DIRECTION_HORIZONTAL) { + direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; + hasMoved = x != this.pX; + distance = Math.abs(input.deltaX); + } else { + direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; + hasMoved = y != this.pY; + distance = Math.abs(input.deltaY); } - - date = createUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); } + input.direction = direction; - // Default to current date. - // * if no year, month, day of month are given, default to today - // * if day of month is given, default month and year - // * if month is given, default only year - // * if year is given, don't default anything - for (i = 0; i < 3 && config._a[i] == null; ++i) { - config._a[i] = input[i] = currentDate[i]; - } + return hasMoved && distance > options.threshold && direction & options.direction; + }, - // Zero out whatever was not defaulted, including time - for (; i < 7; i++) { - config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; - } + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - // Check for 24:00:00.000 - if (config._a[HOUR] === 24 && - config._a[MINUTE] === 0 && - config._a[SECOND] === 0 && - config._a[MILLISECOND] === 0) { - config._nextDay = true; - config._a[HOUR] = 0; - } + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); - // Apply timezone offset from input. The actual utcOffset can be changed - // with parseZone. - if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); } - if (config._nextDay) { - config._a[HOUR] = 24; - } + this._super.emit.call(this, input); } + }); - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; + /** + * Pinch + * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). + * @constructor + * @extends AttrRecognizer + */ + function PinchRecognizer() { + AttrRecognizer.apply(this, arguments); + } - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 + }, - // TODO: We need to take the current isoWeekYear, but that depends on - // how we interpret now (local, utc, fixed offset). So create - // a now version of current config (take local/utc/offset flags, and - // create now). - weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); - week = defaults(w.W, 1); - weekday = defaults(w.E, 1); - } else { - dow = config._locale._week.dow; - doy = config._locale._week.doy; + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); - week = defaults(w.w, 1); + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, - if (w.d != null) { - // weekday -- low day numbers are considered next week - weekday = w.d; - if (weekday < dow) { - ++week; - } - } else if (w.e != null) { - // local weekday -- counting starts from begining of week - weekday = w.e + dow; - } else { - // default to begining of week - weekday = dow; - } + emit: function(input) { + this._super.emit.call(this, input); + if (input.scale !== 1) { + var inOut = input.scale < 1 ? 'in' : 'out'; + this.manager.emit(this.options.event + inOut, input); } - temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); - - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; } + }); - utils_hooks__hooks.ISO_8601 = function () {}; - - // date from string and format string - function configFromStringAndFormat(config) { - // TODO: Move this to another part of the creation flow to prevent circular deps - if (config._f === utils_hooks__hooks.ISO_8601) { - configFromISO(config); - return; - } + /** + * Press + * Recognized when the pointer is down for x ms without any movement. + * @constructor + * @extends Recognizer + */ + function PressRecognizer() { + Recognizer.apply(this, arguments); - config._a = []; - getParsingFlags(config).empty = true; + this._timer = null; + this._input = null; + } - // This array is used to make a Date, either with `new Date` or `Date.UTC` - var string = '' + config._i, - i, parsedInput, tokens, token, skipped, - stringLength = string.length, - totalParsedInputLength = 0; + inherit(PressRecognizer, Recognizer, { + /** + * @namespace + * @memberof PressRecognizer + */ + defaults: { + event: 'press', + pointers: 1, + time: 500, // minimal time of the pointer to be pressed + threshold: 5 // a minimal movement is ok, but keep it low + }, - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; - if (parsedInput) { - skipped = string.substr(0, string.indexOf(parsedInput)); - if (skipped.length > 0) { - getParsingFlags(config).unusedInput.push(skipped); - } - string = string.slice(string.indexOf(parsedInput) + parsedInput.length); - totalParsedInputLength += parsedInput.length; - } - // don't parse if it's not a known token - if (formatTokenFunctions[token]) { - if (parsedInput) { - getParsingFlags(config).empty = false; - } - else { - getParsingFlags(config).unusedTokens.push(token); - } - addTimeToArrayFromToken(token, parsedInput, config); - } - else if (config._strict && !parsedInput) { - getParsingFlags(config).unusedTokens.push(token); - } - } + process: function(input) { + var options = this.options; + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTime = input.deltaTime > options.time; - // add remaining unparsed input length to the string - getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - getParsingFlags(config).unusedInput.push(string); - } + this._input = input; - // clear _12h flag if hour is <= 12 - if (getParsingFlags(config).bigHour === true && - config._a[HOUR] <= 12 && - config._a[HOUR] > 0) { - getParsingFlags(config).bigHour = undefined; + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { + this.reset(); + } else if (input.eventType & INPUT_START) { + this.reset(); + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.time, this); + } else if (input.eventType & INPUT_END) { + return STATE_RECOGNIZED; } - // handle meridiem - config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); - - configFromArray(config); - checkOverflow(config); - } - - - function meridiemFixWrap (locale, hour, meridiem) { - var isPm; + return STATE_FAILED; + }, - if (meridiem == null) { - // nothing to do - return hour; + reset: function() { + clearTimeout(this._timer); + }, + + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; } - if (locale.meridiemHour != null) { - return locale.meridiemHour(hour, meridiem); - } else if (locale.isPM != null) { - // Fallback - isPm = locale.isPM(meridiem); - if (isPm && hour < 12) { - hour += 12; - } - if (!isPm && hour === 12) { - hour = 0; - } - return hour; + + if (input && (input.eventType & INPUT_END)) { + this.manager.emit(this.options.event + 'up', input); } else { - // this is not supposed to happen - return hour; + this._input.timeStamp = now(); + this.manager.emit(this.options.event, this._input); } } + }); - function configFromStringAndArray(config) { - var tempConfig, - bestMoment, + /** + * Rotate + * Recognized when two or more pointer are moving in a circular motion. + * @constructor + * @extends AttrRecognizer + */ + function RotateRecognizer() { + AttrRecognizer.apply(this, arguments); + } - scoreToBeat, - i, - currentScore; + inherit(RotateRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof RotateRecognizer + */ + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, - if (config._f.length === 0) { - getParsingFlags(config).invalidFormat = true; - config._d = new Date(NaN); - return; - } + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - for (i = 0; i < config._f.length; i++) { - currentScore = 0; - tempConfig = copyConfig({}, config); - if (config._useUTC != null) { - tempConfig._useUTC = config._useUTC; - } - tempConfig._f = config._f[i]; - configFromStringAndFormat(tempConfig); + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + } + }); - if (!valid__isValid(tempConfig)) { - continue; - } + /** + * Swipe + * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function SwipeRecognizer() { + AttrRecognizer.apply(this, arguments); + } - // if there is any input that was not parsed add a penalty for that format - currentScore += getParsingFlags(tempConfig).charsLeftOver; + inherit(SwipeRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof SwipeRecognizer + */ + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - //or tokens - currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - getParsingFlags(tempConfig).score = currentScore; + attrTest: function(input) { + var direction = this.options.direction; + var velocity; - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } + if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { + velocity = input.velocity; + } else if (direction & DIRECTION_HORIZONTAL) { + velocity = input.velocityX; + } else if (direction & DIRECTION_VERTICAL) { + velocity = input.velocityY; } - extend(config, bestMoment || tempConfig); - } + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + }, - function configFromObject(config) { - if (config._d) { - return; + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); } - var i = normalizeObjectUnits(config._i); - config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; - - configFromArray(config); + this.manager.emit(this.options.event, input); } + }); - function createFromConfig (config) { - var input = config._i, - format = config._f, - res; + /** + * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur + * between the given interval and position. The delay option can be used to recognize multi-taps without firing + * a single tap. + * + * The eventData from the emitted event contains the property `tapCount`, which contains the amount of + * multi-taps being recognized. + * @constructor + * @extends Recognizer + */ + function TapRecognizer() { + Recognizer.apply(this, arguments); - config._locale = config._locale || locale_locales__getLocale(config._l); + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; - if (input === null || (format === undefined && input === '')) { - return valid__createInvalid({nullInput: true}); - } + this._timer = null; + this._input = null; + this.count = 0; + } - if (typeof input === 'string') { - config._i = input = config._locale.preparse(input); - } + inherit(TapRecognizer, Recognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'tap', + pointers: 1, + taps: 1, + interval: 300, // max time between the multi-tap taps + time: 250, // max time of the pointer to be down (like finger on the screen) + threshold: 2, // a minimal movement is ok, but keep it low + posThreshold: 10 // a multi-tap can be a bit off the initial position + }, - if (isMoment(input)) { - return new Moment(checkOverflow(input)); - } else if (isArray(format)) { - configFromStringAndArray(config); - } else if (format) { - configFromStringAndFormat(config); - } else if (isDate(input)) { - config._d = input; - } else { - configFromInput(config); - } + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - res = new Moment(checkOverflow(config)); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; - } + process: function(input) { + var options = this.options; - return res; - } + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - function configFromInput(config) { - var input = config._i; - if (input === undefined) { - config._d = new Date(); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if (typeof input === 'string') { - configFromString(config); - } else if (isArray(input)) { - config._a = map(input.slice(0), function (obj) { - return parseInt(obj, 10); - }); - configFromArray(config); - } else if (typeof(input) === 'object') { - configFromObject(config); - } else if (typeof(input) === 'number') { - // from milliseconds - config._d = new Date(input); - } else { - utils_hooks__hooks.createFromInputFallback(config); + this.reset(); + + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); } - } - function createLocalOrUTC (input, format, locale, strict, isUTC) { - var c = {}; + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (validMovement && validTouchTime && validPointers) { + if (input.eventType != INPUT_END) { + return this.failTimeout(); + } - if (typeof(locale) === 'boolean') { - strict = locale; - locale = undefined; - } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c._isAMomentObject = true; - c._useUTC = c._isUTC = isUTC; - c._l = locale; - c._i = input; - c._f = format; - c._strict = strict; + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - return createFromConfig(c); - } + this.pTime = input.timeStamp; + this.pCenter = input.center; - function local__createLocal (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, false); - } + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; + } - var prototypeMin = deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other < this ? this : other; - } - ); + this._input = input; - var prototypeMax = deprecate( - 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other > this ? this : other; + // if tap count matches we have recognized it, + // else it has began recognizing... + var tapCount = this.count % options.taps; + if (tapCount === 0) { + // no failing requirements, immediately trigger the tap event + // or wait as long as the multitap interval to trigger + if (!this.hasRequireFailures()) { + return STATE_RECOGNIZED; + } else { + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.interval, this); + return STATE_BEGAN; + } + } } - ); + return STATE_FAILED; + }, - // Pick a moment m from moments so that m[fn](other) is true for all - // other. This relies on the function fn to be transitive. - // - // moments should either be an array of moment objects or an array, whose - // first element is an array of moment objects. - function pickBy(fn, moments) { - var res, i; - if (moments.length === 1 && isArray(moments[0])) { - moments = moments[0]; - } - if (!moments.length) { - return local__createLocal(); - } - res = moments[0]; - for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { - res = moments[i]; - } + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, + + reset: function() { + clearTimeout(this._timer); + }, + + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); } - return res; } + }); - // TODO: Use [].sort instead? - function min () { - var args = [].slice.call(arguments, 0); + /** + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Hammer(element, options) { + options = options || {}; + options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); + return new Manager(element, options); + } - return pickBy('isBefore', args); - } + /** + * @const {string} + */ + Hammer.VERSION = '2.0.4'; - function max () { - var args = [].slice.call(arguments, 0); + /** + * default settings + * @namespace + */ + Hammer.defaults = { + /** + * set if DOM events are being triggered. + * But this is slower and unused by simple implementations, so disabled by default. + * @type {Boolean} + * @default false + */ + domEvents: false, - return pickBy('isAfter', args); - } + /** + * The value for the touchAction property/fallback. + * When set to `compute` it will magically set the correct value based on the added recognizers. + * @type {String} + * @default compute + */ + touchAction: TOUCH_ACTION_COMPUTE, - function Duration (duration) { - var normalizedInput = normalizeObjectUnits(duration), - years = normalizedInput.year || 0, - quarters = normalizedInput.quarter || 0, - months = normalizedInput.month || 0, - weeks = normalizedInput.week || 0, - days = normalizedInput.day || 0, - hours = normalizedInput.hour || 0, - minutes = normalizedInput.minute || 0, - seconds = normalizedInput.second || 0, - milliseconds = normalizedInput.millisecond || 0; + /** + * @type {Boolean} + * @default true + */ + enable: true, - // representation for dateAddRemove - this._milliseconds = +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 - // Because of dateAddRemove treats 24 hours as different from a - // day when working around DST, we need to store them separately - this._days = +days + - weeks * 7; - // It is impossible translate months into days without knowing - // which months you are are talking about, so we have to store - // it separately. - this._months = +months + - quarters * 3 + - years * 12; + /** + * EXPERIMENTAL FEATURE -- can be removed/changed + * Change the parent input target element. + * If Null, then it is being set the to main element. + * @type {Null|EventTarget} + * @default null + */ + inputTarget: null, - this._data = {}; + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, - this._locale = locale_locales__getLocale(); + /** + * Default recognizer setup when calling `Hammer()` + * When creating a new Manager these will be skipped. + * @type {Array} + */ + preset: [ + // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] + [RotateRecognizer, { enable: false }], + [PinchRecognizer, { enable: false }, ['rotate']], + [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], + [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], + [TapRecognizer], + [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], + [PressRecognizer] + ], - this._bubble(); - } + /** + * Some CSS properties can be used to improve the working of Hammer. + * Add them to this method and they will be set when creating a new Manager. + * @namespace + */ + cssProps: { + /** + * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userSelect: 'none', - function isDuration (obj) { - return obj instanceof Duration; - } + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', - function offset (token, separator) { - addFormatToken(token, 0, 0, function () { - var offset = this.utcOffset(); - var sign = '+'; - if (offset < 0) { - offset = -offset; - sign = '-'; - } - return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); - }); - } + /** + * Disables the default callout shown when you touch and hold a touch target. + * On iOS, when you touch and hold a touch target such as a link, Safari displays + * a callout containing information about the link. This property allows you to disable that callout. + * @type {String} + * @default 'none' + */ + touchCallout: 'none', - offset('Z', ':'); - offset('ZZ', ''); + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - // PARSING + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', - addRegexToken('Z', matchOffset); - addRegexToken('ZZ', matchOffset); - addParseToken(['Z', 'ZZ'], function (input, array, config) { - config._useUTC = true; - config._tzm = offsetFromString(input); - }); + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in iOS. This property obeys the alpha value, if specified. + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' + } + }; + + var STOP = 1; + var FORCED_STOP = 2; - // HELPERS + /** + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Manager(element, options) { + options = options || {}; - // timezone chunker - // '+10:00' > ['10', '00'] - // '-1530' > ['-15', '30'] - var chunkOffset = /([\+\-]|\d\d)/gi; + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - function offsetFromString(string) { - var matches = ((string || '').match(matchOffset) || []); - var chunk = matches[matches.length - 1] || []; - var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; - var minutes = +(parts[1] * 60) + toInt(parts[2]); + this.handlers = {}; + this.session = {}; + this.recognizers = []; - return parts[0] === '+' ? minutes : -minutes; - } + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); - // Return a moment from input, that is local/utc/zone equivalent to model. - function cloneWithOffset(input, model) { - var res, diff; - if (model._isUTC) { - res = model.clone(); - diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); - // Use low-level api, because this fn is low-level api. - res._d.setTime(+res._d + diff); - utils_hooks__hooks.updateOffset(res, false); - return res; - } else { - return local__createLocal(input).local(); - } - return model._isUTC ? local__createLocal(input).zone(model._offset || 0) : local__createLocal(input).local(); - } + toggleCssProps(this, true); - function getDateOffset (m) { - // On Firefox.24 Date#getTimezoneOffset returns a floating point. - // https://github.com/moment/moment/pull/1871 - return -Math.round(m._d.getTimezoneOffset() / 15) * 15; - } + each(options.recognizers, function(item) { + var recognizer = this.add(new (item[0])(item[1])); + item[2] && recognizer.recognizeWith(item[2]); + item[3] && recognizer.requireFailure(item[3]); + }, this); + } - // HOOKS + Manager.prototype = { + /** + * set options + * @param {Object} options + * @returns {Manager} + */ + set: function(options) { + extend(this.options, options); - // This function will be called whenever a moment is mutated. - // It is intended to keep the offset in sync with the timezone. - utils_hooks__hooks.updateOffset = function () {}; + // Options that need a little more setup + if (options.touchAction) { + this.touchAction.update(); + } + if (options.inputTarget) { + // Clean up existing event listeners and reinitialize + this.input.destroy(); + this.input.target = options.inputTarget; + this.input.init(); + } + return this; + }, - // MOMENTS + /** + * stop recognizing for this session. + * This session will be discarded, when a new [input]start event is fired. + * When forced, the recognizer cycle is stopped immediately. + * @param {Boolean} [force] + */ + stop: function(force) { + this.session.stopped = force ? FORCED_STOP : STOP; + }, - // keepLocalTime = true means only change the timezone, without - // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> - // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset - // +0200, so we adjust the time as needed, to be valid. - // - // Keeping the time actually adds/subtracts (one hour) - // from the actual represented time. That is why we call updateOffset - // a second time. In case it wants us to change the offset again - // _changeInProgress == true case, then we have to adjust, because - // there is no such time in the given timezone. - function getSetOffset (input, keepLocalTime) { - var offset = this._offset || 0, - localAdjust; - if (input != null) { - if (typeof input === 'string') { - input = offsetFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = getDateOffset(this); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.add(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - utils_hooks__hooks.updateOffset(this, true); - this._changeInProgress = null; - } - } - return this; - } else { - return this._isUTC ? offset : getDateOffset(this); + /** + * run the recognizers! + * called by the inputHandler function on every movement of the pointers (touches) + * it walks through all the recognizers and tries to detect the gesture that is being made + * @param {Object} inputData + */ + recognize: function(inputData) { + var session = this.session; + if (session.stopped) { + return; } - } - function getSetZone (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); - this.utcOffset(input, keepLocalTime); + var recognizer; + var recognizers = this.recognizers; - return this; - } else { - return -this.utcOffset(); + // this holds the recognizer that is being recognized. + // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED + // if no recognizer is detecting a thing, it is set to `null` + var curRecognizer = session.curRecognizer; + + // reset when the last recognizer is recognized + // or when we're in a new session + if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { + curRecognizer = session.curRecognizer = null; } - } - function setOffsetToUTC (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - } + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; - function setOffsetToLocal (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; + // find out if we are allowed try to recognize the input for this one. + // 1. allow if the session is NOT forced stopped (see the .stop() method) + // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one + // that is being recognized. + // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. + // this can be setup with the `recognizeWith()` method on the recognizer. + if (session.stopped !== FORCED_STOP && ( // 1 + !curRecognizer || recognizer == curRecognizer || // 2 + recognizer.canRecognizeWith(curRecognizer))) { // 3 + recognizer.recognize(inputData); + } else { + recognizer.reset(); + } - if (keepLocalTime) { - this.subtract(getDateOffset(this), 'm'); + // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the + // current active recognizer. but only if we don't already have an active recognizer + if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { + curRecognizer = session.curRecognizer = recognizer; } + i++; } - return this; - } + }, - function setOffsetToParsedOffset () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(offsetFromString(this._i)); + /** + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} + */ + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; } - return this; - } - function hasAlignedHourOffset (input) { - if (!input) { - input = 0; - } - else { - input = local__createLocal(input).utcOffset(); + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } } + return null; + }, - return (this.utcOffset() - input) % 60 === 0; - } - - function isDaylightSavingTime () { - return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); - } - - function isDaylightSavingTimeShifted () { - if (this._a) { - var other = this._isUTC ? create_utc__createUTC(this._a) : local__createLocal(this._a); - return this.isValid() && compareArrays(this._a, other.toArray()) > 0; + /** + * add a recognizer to the manager + * existing recognizers with the same event name will be removed + * @param {Recognizer} recognizer + * @returns {Recognizer|Manager} + */ + add: function(recognizer) { + if (invokeArrayArg(recognizer, 'add', this)) { + return this; } - return false; - } + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); + } - function isLocal () { - return !this._isUTC; - } + this.recognizers.push(recognizer); + recognizer.manager = this; - function isUtcOffset () { - return this._isUTC; - } + this.touchAction.update(); + return recognizer; + }, - function isUtc () { - return this._isUTC && this._offset === 0; - } + /** + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} + */ + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; + this.touchAction.update(); + return this; + }, - function create__createDuration (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - diffRes; + /** + * bind event + * @param {String} events + * @param {Function} handler + * @returns {EventEmitter} this + */ + on: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + handlers[event] = handlers[event] || []; + handlers[event].push(handler); + }); + return this; + }, - if (isDuration(input)) { - duration = { - ms : input._milliseconds, - d : input._days, - M : input._months - }; - } else if (typeof input === 'number') { - duration = {}; - if (key) { - duration[key] = input; + /** + * unbind event, leave emit blank to remove all handlers + * @param {String} events + * @param {Function} [handler] + * @returns {EventEmitter} this + */ + off: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + if (!handler) { + delete handlers[event]; } else { - duration.milliseconds = input; + handlers[event].splice(inArray(handlers[event], handler), 1); } - } else if (!!(match = aspNetRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : 0, - d : toInt(match[DATE]) * sign, - h : toInt(match[HOUR]) * sign, - m : toInt(match[MINUTE]) * sign, - s : toInt(match[SECOND]) * sign, - ms : toInt(match[MILLISECOND]) * sign - }; - } else if (!!(match = create__isoRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : parseIso(match[2], sign), - M : parseIso(match[3], sign), - d : parseIso(match[4], sign), - h : parseIso(match[5], sign), - m : parseIso(match[6], sign), - s : parseIso(match[7], sign), - w : parseIso(match[8], sign) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); + }); + return this; + }, - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; + /** + * emit event to the listeners + * @param {String} event + * @param {Object} data + */ + emit: function(event, data) { + // we also want to trigger dom events + if (this.options.domEvents) { + triggerDomEvent(event, data); } - ret = new Duration(duration); + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; + } - if (isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; + + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; } + }, - return ret; + /** + * destroy the manager and unbinds all events + * it doesn't unbind dom events, that is the user own responsibility + */ + destroy: function() { + this.element && toggleCssProps(this, false); + + this.handlers = {}; + this.session = {}; + this.input.destroy(); + this.element = null; } + }; - create__createDuration.fn = Duration.prototype; + /** + * add/remove the css properties as defined in manager.options.cssProps + * @param {Manager} manager + * @param {Boolean} add + */ + function toggleCssProps(manager, add) { + var element = manager.element; + each(manager.options.cssProps, function(value, name) { + element.style[prefixed(element.style, name)] = add ? value : ''; + }); + } - function parseIso (inp, sign) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; - } + /** + * trigger dom event + * @param {String} event + * @param {Object} data + */ + function triggerDomEvent(event, data) { + var gestureEvent = document.createEvent('Event'); + gestureEvent.initEvent(event, true, true); + gestureEvent.gesture = data; + data.target.dispatchEvent(gestureEvent); + } - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; - } + STATE_POSSIBLE: STATE_POSSIBLE, + STATE_BEGAN: STATE_BEGAN, + STATE_CHANGED: STATE_CHANGED, + STATE_ENDED: STATE_ENDED, + STATE_RECOGNIZED: STATE_RECOGNIZED, + STATE_CANCELLED: STATE_CANCELLED, + STATE_FAILED: STATE_FAILED, - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); + DIRECTION_NONE: DIRECTION_NONE, + DIRECTION_LEFT: DIRECTION_LEFT, + DIRECTION_RIGHT: DIRECTION_RIGHT, + DIRECTION_UP: DIRECTION_UP, + DIRECTION_DOWN: DIRECTION_DOWN, + DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, + DIRECTION_VERTICAL: DIRECTION_VERTICAL, + DIRECTION_ALL: DIRECTION_ALL, - return res; - } + Manager: Manager, + Input: Input, + TouchAction: TouchAction, - function momentsDifference(base, other) { - var res; - other = cloneWithOffset(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, - return res; - } + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, + + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); + + if ("function" == TYPE_FUNCTION && __webpack_require__(13)) { + !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { + return Hammer; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof module != 'undefined' && module.exports) { + module.exports = Hammer; + } else { + window[exportName] = Hammer; + } - function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); - tmp = val; val = period; period = tmp; - } + })(window, document, 'Hammer'); - val = typeof val === 'string' ? +val : val; - dur = create__createDuration(val, period); - add_subtract__addSubtract(this, dur, direction); - return this; - }; - } - function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { - if (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); - } - if (days) { - get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); - } - if (months) { - setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); - } - if (updateOffset) { - utils_hooks__hooks.updateOffset(mom, days || months); - } - } + /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; - var add_subtract__add = createAdder(1, 'add'); - var add_subtract__subtract = createAdder(-1, 'subtract'); + /* WEBPACK VAR INJECTION */}.call(exports, {})) - function moment_calendar__calendar (time) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're local/utc/offset or not. - var now = time || local__createLocal(), - sod = cloneWithOffset(now, this).startOf('day'), - diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; - return this.format(this.localeData().calendar(format, this, local__createLocal(now))); - } +/***/ }, +/* 14 */ +/***/ function(module, exports, __webpack_require__) { - function clone () { - return new Moment(this); - } + // utility functions - function isAfter (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this > +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return inputMs < +this.clone().startOf(units); - } - } + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. - function isBefore (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this < +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return +this.clone().endOf(units) < inputMs; - } - } + 'use strict'; - function isBetween (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - } + var moment = __webpack_require__(15); + var uuid = __webpack_require__(18); - function isSame (input, units) { - var inputMs; - units = normalizeUnits(units || 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this === +input; - } else { - inputMs = +local__createLocal(input); - return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); - } - } + /** + * Test whether given object is a number + * @param {*} object + * @return {Boolean} isNumber + */ + exports.isNumber = function (object) { + return object instanceof Number || typeof object == 'number'; + }; - function absFloor (number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); - } + /** + * Remove everything in the DOM object + * @param DOMobject + */ + exports.recursiveDOMDelete = function (DOMobject) { + if (DOMobject) { + while (DOMobject.hasChildNodes() === true) { + exports.recursiveDOMDelete(DOMobject.firstChild); + DOMobject.removeChild(DOMobject.firstChild); } + } + }; - function diff (input, units, asFloat) { - var that = cloneWithOffset(input, this), - zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, - delta, output; + /** + * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value. + * + * @param min + * @param max + * @param total + * @param value + * @returns {number} + */ + exports.giveRange = function (min, max, total, value) { + if (max == min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + }; - units = normalizeUnits(units); + /** + * Test whether given object is a string + * @param {*} object + * @return {Boolean} isString + */ + exports.isString = function (object) { + return object instanceof String || typeof object == 'string'; + }; - if (units === 'year' || units === 'month' || units === 'quarter') { - output = monthDiff(this, that); - if (units === 'quarter') { - output = output / 3; - } else if (units === 'year') { - output = output / 12; - } - } else { - delta = this - that; - output = units === 'second' ? delta / 1e3 : // 1000 - units === 'minute' ? delta / 6e4 : // 1000 * 60 - units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 - units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst - units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst - delta; - } - return asFloat ? output : absFloor(output); + /** + * Test whether given object is a Date, or a String containing a Date + * @param {Date | String} object + * @return {Boolean} isDate + */ + exports.isDate = function (object) { + if (object instanceof Date) { + return true; + } else if (exports.isString(object)) { + // test whether this string contains a date + var match = ASPDateRegex.exec(object); + if (match) { + return true; + } else if (!isNaN(Date.parse(object))) { + return true; } + } - function monthDiff (a, b) { - // difference in months - var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), - // b is in (anchor - 1 month, anchor + 1 month) - anchor = a.clone().add(wholeMonthDiff, 'months'), - anchor2, adjust; + return false; + }; - if (b - anchor < 0) { - anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor - anchor2); - } else { - anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor2 - anchor); - } + /** + * Create a semi UUID + * source: http://stackoverflow.com/a/105074/1262753 + * @return {String} uuid + */ + exports.randomUUID = function () { + return uuid.v4(); + }; - return -(wholeMonthDiff + adjust); + /** + * assign all keys of an object that are not nested objects to a certain value (used for color objects). + * @param obj + * @param value + */ + exports.assignAllKeys = function (obj, value) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (typeof obj[prop] !== 'object') { + obj[prop] = value; + } } + } + }; - utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; - - function toString () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); - } + /** + * Fill an object with a possibly partially defined other object. Only copies values if the a object has an object requiring values. + * That means an object is not created on a property if only the b object has it. + * @param obj + * @param value + */ + exports.fillIfDefined = function (a, b) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - function moment_format__toISOString () { - var m = this.clone().utc(); - if (0 < m.year() && m.year() <= 9999) { - if ('function' === typeof Date.prototype.toISOString) { - // native implementation is ~50x faster, use it when we can - return this.toDate().toISOString(); - } else { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } + for (var prop in a) { + if (b[prop] !== undefined) { + if (typeof b[prop] !== 'object') { + if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + a[prop] = b[prop]; } + } else { + if (typeof a[prop] === 'object') { + exports.fillIfDefined(a[prop], b[prop], allowDeletion); + } + } } + } + }; - function format (inputString) { - var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); - return this.localeData().postformat(output); + /** + * Extend object a with the properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ + exports.protoExtend = function (a, b) { + for (var i = 1; i < arguments.length; i++) { + var other = arguments[i]; + for (var prop in other) { + a[prop] = other[prop]; } + } + return a; + }; - function from (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + /** + * Extend object a with the properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Object} a + * @param {... Object} b + * @return {Object} a + */ + exports.extend = function (a, b) { + for (var i = 1; i < arguments.length; i++) { + var other = arguments[i]; + for (var prop in other) { + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } } + } + return a; + }; - function fromNow (withoutSuffix) { - return this.from(local__createLocal(), withoutSuffix); - } + /** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + exports.selectiveExtend = function (props, a, b) { + if (!Array.isArray(props)) { + throw new Error('Array with property names expected as first argument'); + } - function to (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); - } + for (var i = 2; i < arguments.length; i++) { + var other = arguments[i]; - function toNow (withoutSuffix) { - return this.to(local__createLocal(), withoutSuffix); + for (var p = 0; p < props.length; p++) { + var prop = props[p]; + if (other.hasOwnProperty(prop)) { + a[prop] = other[prop]; + } } + } + return a; + }; - function locale (key) { - var newLocaleData; + /** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + exports.selectiveDeepExtend = function (props, a, b) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = locale_locales__getLocale(key); - if (newLocaleData != null) { - this._locale = newLocaleData; + // TODO: add support for Arrays to deepExtend + if (Array.isArray(b)) { + throw new TypeError('Arrays are not supported by deepExtend'); + } + for (var i = 2; i < arguments.length; i++) { + var other = arguments[i]; + for (var p = 0; p < props.length; p++) { + var prop = props[p]; + if (other.hasOwnProperty(prop)) { + if (b[prop] && b[prop].constructor === Object) { + if (a[prop] === undefined) { + a[prop] = {}; + } + if (a[prop].constructor === Object) { + exports.deepExtend(a[prop], b[prop], false, allowDeletion); + } else { + if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; + } else { + a[prop] = b[prop]; } - return this; + } + } else if (Array.isArray(b[prop])) { + throw new TypeError('Arrays are not supported by deepExtend'); + } else { + a[prop] = b[prop]; } + } } + } + return a; + }; - var lang = deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); + /** + * Extend object a with selected properties of object b or a series of objects + * Only properties with defined values are copied + * @param {Array.} props + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + exports.selectiveNotDeepExtend = function (props, a, b) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; + + // TODO: add support for Arrays to deepExtend + if (Array.isArray(b)) { + throw new TypeError('Arrays are not supported by deepExtend'); + } + for (var prop in b) { + if (b.hasOwnProperty(prop)) { + if (props.indexOf(prop) == -1) { + if (b[prop] && b[prop].constructor === Object) { + if (a[prop] === undefined) { + a[prop] = {}; + } + if (a[prop].constructor === Object) { + exports.deepExtend(a[prop], b[prop]); + } else { + if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; } else { - return this.locale(key); + a[prop] = b[prop]; } + } + } else if (Array.isArray(b[prop])) { + throw new TypeError('Arrays are not supported by deepExtend'); + } else { + a[prop] = b[prop]; } - ); - - function localeData () { - return this._locale; + } } + } + return a; + }; - function startOf (units) { - units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); - } - - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } - - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); + /** + * Deep extend an object a with the properties of object b + * @param {Object} a + * @param {Object} b + * @param [Boolean] protoExtend --> optional parameter. If true, the prototype values will also be extended. + * (ie. the options objects that inherit from others will also get the inherited options) + * @param [Boolean] global --> optional parameter. If true, the values of fields that are null will not deleted + * @returns {Object} + */ + exports.deepExtend = function (a, b, protoExtend, allowDeletion) { + for (var prop in b) { + if (b.hasOwnProperty(prop) || protoExtend === true) { + if (b[prop] && b[prop].constructor === Object) { + if (a[prop] === undefined) { + a[prop] = {}; } - - return this; - } - - function endOf (units) { - units = normalizeUnits(units); - if (units === undefined || units === 'millisecond') { - return this; + if (a[prop].constructor === Object) { + exports.deepExtend(a[prop], b[prop], protoExtend); + } else { + if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) { + delete a[prop]; + } else { + a[prop] = b[prop]; + } } - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); - } - - function to_type__valueOf () { - return +this._d - ((this._offset || 0) * 60000); - } - - function unix () { - return Math.floor(+this / 1000); + } else if (Array.isArray(b[prop])) { + a[prop] = []; + for (var i = 0; i < b[prop].length; i++) { + a[prop].push(b[prop][i]); + } + } else { + a[prop] = b[prop]; + } } + } + return a; + }; - function toDate () { - return this._offset ? new Date(+this) : this._d; - } + /** + * Test whether all elements in two arrays are equal. + * @param {Array} a + * @param {Array} b + * @return {boolean} Returns true if both arrays have the same length and same + * elements. + */ + exports.equalArray = function (a, b) { + if (a.length != b.length) return false; - function toArray () { - var m = this; - return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; - } + for (var i = 0, len = a.length; i < len; i++) { + if (a[i] != b[i]) return false; + } - function moment_valid__isValid () { - return valid__isValid(this); - } + return true; + }; - function parsingFlags () { - return extend({}, getParsingFlags(this)); - } + /** + * Convert an object to another type + * @param {Boolean | Number | String | Date | Moment | Null | undefined} object + * @param {String | undefined} type Name of the type. Available types: + * 'Boolean', 'Number', 'String', + * 'Date', 'Moment', ISODate', 'ASPDate'. + * @return {*} object + * @throws Error + */ + exports.convert = function (object, type) { + var match; - function invalidAt () { - return getParsingFlags(this).overflow; - } + if (object === undefined) { + return undefined; + } + if (object === null) { + return null; + } - addFormatToken(0, ['gg', 2], 0, function () { - return this.weekYear() % 100; - }); + if (!type) { + return object; + } + if (!(typeof type === 'string') && !(type instanceof String)) { + throw new Error('Type must be a string'); + } - addFormatToken(0, ['GG', 2], 0, function () { - return this.isoWeekYear() % 100; - }); + //noinspection FallthroughInSwitchStatementJS + switch (type) { + case 'boolean': + case 'Boolean': + return Boolean(object); - function addWeekYearFormatToken (token, getter) { - addFormatToken(0, [token, token.length], 0, getter); - } + case 'number': + case 'Number': + return Number(object.valueOf()); - addWeekYearFormatToken('gggg', 'weekYear'); - addWeekYearFormatToken('ggggg', 'weekYear'); - addWeekYearFormatToken('GGGG', 'isoWeekYear'); - addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + case 'string': + case 'String': + return String(object); - // ALIASES + case 'Date': + if (exports.isNumber(object)) { + return new Date(object); + } + if (object instanceof Date) { + return new Date(object.valueOf()); + } else if (moment.isMoment(object)) { + return new Date(object.valueOf()); + } + if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])); // parse number + } else { + return moment(object).toDate(); // parse string + } + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); + } - addUnitAlias('weekYear', 'gg'); - addUnitAlias('isoWeekYear', 'GG'); + case 'Moment': + if (exports.isNumber(object)) { + return moment(object); + } + if (object instanceof Date) { + return moment(object.valueOf()); + } else if (moment.isMoment(object)) { + return moment(object); + } + if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return moment(Number(match[1])); // parse number + } else { + return moment(object); // parse string + } + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date'); + } - // PARSING + case 'ISODate': + if (exports.isNumber(object)) { + return new Date(object); + } else if (object instanceof Date) { + return object.toISOString(); + } else if (moment.isMoment(object)) { + return object.toDate().toISOString(); + } else if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + if (match) { + // object is an ASP date + return new Date(Number(match[1])).toISOString(); // parse number + } else { + return new Date(object).toISOString(); // parse string + } + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate'); + } - addRegexToken('G', matchSigned); - addRegexToken('g', matchSigned); - addRegexToken('GG', match1to2, match2); - addRegexToken('gg', match1to2, match2); - addRegexToken('GGGG', match1to4, match4); - addRegexToken('gggg', match1to4, match4); - addRegexToken('GGGGG', match1to6, match6); - addRegexToken('ggggg', match1to6, match6); + case 'ASPDate': + if (exports.isNumber(object)) { + return '/Date(' + object + ')/'; + } else if (object instanceof Date) { + return '/Date(' + object.valueOf() + ')/'; + } else if (exports.isString(object)) { + match = ASPDateRegex.exec(object); + var value; + if (match) { + // object is an ASP date + value = new Date(Number(match[1])).valueOf(); // parse number + } else { + value = new Date(object).valueOf(); // parse string + } + return '/Date(' + value + ')/'; + } else { + throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate'); + } - addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { - week[token.substr(0, 2)] = toInt(input); - }); + default: + throw new Error('Unknown type "' + type + '"'); + } + }; - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = utils_hooks__hooks.parseTwoDigitYear(input); - }); + // parse ASP.Net Date pattern, + // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/' + // code from http://momentjs.com/ + var ASPDateRegex = /^\/?Date\((\-?\d+)/i; - // HELPERS + /** + * Get the type of an object, for example exports.getType([]) returns 'Array' + * @param {*} object + * @return {String} type + */ + exports.getType = function (object) { + var type = typeof object; - function weeksInYear(year, dow, doy) { - return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; + if (type == 'object') { + if (object === null) { + return 'null'; } - - // MOMENTS - - function getSetWeekYear (input) { - var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; - return input == null ? year : this.add((input - year), 'y'); + if (object instanceof Boolean) { + return 'Boolean'; } - - function getSetISOWeekYear (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); + if (object instanceof Number) { + return 'Number'; } - - function getISOWeeksInYear () { - return weeksInYear(this.year(), 1, 4); + if (object instanceof String) { + return 'String'; } - - function getWeeksInYear () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + if (Array.isArray(object)) { + return 'Array'; } - - addFormatToken('Q', 0, 0, 'quarter'); - - // ALIASES - - addUnitAlias('quarter', 'Q'); - - // PARSING - - addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; - }); - - // MOMENTS - - function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + if (object instanceof Date) { + return 'Date'; } + return 'Object'; + } else if (type == 'number') { + return 'Number'; + } else if (type == 'boolean') { + return 'Boolean'; + } else if (type == 'string') { + return 'String'; + } else if (type === undefined) { + return 'undefined'; + } - addFormatToken('D', ['DD', 2], 'Do', 'date'); - - // ALIASES - - addUnitAlias('date', 'D'); - - // PARSING - - addRegexToken('D', match1to2); - addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; - }); - - addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0], 10); - }); - - // MOMENTS - - var getSetDayOfMonth = makeGetSet('Date', true); - - addFormatToken('d', 0, 'do', 'day'); - - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); - }); - - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); - }); - - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); - }); - - addFormatToken('e', 0, 0, 'weekday'); - addFormatToken('E', 0, 0, 'isoWeekday'); - - // ALIASES - - addUnitAlias('day', 'd'); - addUnitAlias('weekday', 'e'); - addUnitAlias('isoWeekday', 'E'); - - // PARSING - - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); + return type; + }; - addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { - var weekday = config._locale.weekdaysParse(input); - // if we didn't get a weekday name, mark the date as invalid - if (weekday != null) { - week.d = weekday; - } else { - getParsingFlags(config).invalidWeekday = input; - } - }); + /** + * Used to extend an array and copy it. This is used to propagate paths recursively. + * + * @param arr + * @param newValue + * @returns {Array} + */ + exports.copyAndExtendArray = function (arr, newValue) { + var newArr = []; + for (var i = 0; i < arr.length; i++) { + newArr.push(arr[i]); + } + newArr.push(newValue); + return newArr; + }; - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(input); - }); + /** + * Used to extend an array and copy it. This is used to propagate paths recursively. + * + * @param arr + * @param newValue + * @returns {Array} + */ + exports.copyArray = function (arr) { + var newArr = []; + for (var i = 0; i < arr.length; i++) { + newArr.push(arr[i]); + } + return newArr; + }; - // HELPERS + /** + * Retrieve the absolute left value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} left The absolute left position of this element + * in the browser page. + */ + exports.getAbsoluteLeft = function (elem) { + return elem.getBoundingClientRect().left; + }; - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } + /** + * Retrieve the absolute top value of a DOM element + * @param {Element} elem A dom element, for example a div + * @return {number} top The absolute top position of this element + * in the browser page. + */ + exports.getAbsoluteTop = function (elem) { + return elem.getBoundingClientRect().top; + }; - // LOCALES + /** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ + exports.addClassName = function (elem, className) { + var classes = elem.className.split(' '); + if (classes.indexOf(className) == -1) { + classes.push(className); // add the class to the array + elem.className = classes.join(' '); + } + }; - var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m) { - return this._weekdays[m.day()]; - } + /** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ + exports.removeClassName = function (elem, className) { + var classes = elem.className.split(' '); + var index = classes.indexOf(className); + if (index != -1) { + classes.splice(index, 1); // remove the class from the array + elem.className = classes.join(' '); + } + }; - var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { - return this._weekdaysShort[m.day()]; + /** + * For each method for both arrays and objects. + * In case of an array, the built-in Array.forEach() is applied. + * In case of an Object, the method loops over all properties of the object. + * @param {Object | Array} object An Object or Array + * @param {function} callback Callback method, called for each item in + * the object or array with three parameters: + * callback(value, index, object) + */ + exports.forEach = function (object, callback) { + var i, len; + if (Array.isArray(object)) { + // array + for (i = 0, len = object.length; i < len; i++) { + callback(object[i], i, object); } - - var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { - return this._weekdaysMin[m.day()]; + } else { + // object + for (i in object) { + if (object.hasOwnProperty(i)) { + callback(object[i], i, object); + } } + } + }; - function localeWeekdaysParse (weekdayName) { - var i, mom, regex; + /** + * Convert an object into an array: all objects properties are put into the + * array. The resulting array is unordered. + * @param {Object} object + * @param {Array} array + */ + exports.toArray = function (object) { + var array = []; - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } + for (var prop in object) { + if (object.hasOwnProperty(prop)) array.push(object[prop]); + } - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = local__createLocal([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } - } - } + return array; + }; - // MOMENTS + /** + * Update a property in an object + * @param {Object} object + * @param {String} key + * @param {*} value + * @return {Boolean} changed + */ + exports.updateProperty = function (object, key, value) { + if (object[key] !== value) { + object[key] = value; + return true; + } else { + return false; + } + }; - function getSetDayOfWeek (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; - } - } + /** + * Add and event listener. Works for all browsers + * @param {Element} element An html element + * @param {string} action The action, for example "click", + * without the prefix "on" + * @param {function} listener The callback function to be executed + * @param {boolean} [useCapture] + */ + exports.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) useCapture = false; - function getSetLocaleDayOfWeek (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - function getSetISODayOfWeek (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); - } + element.addEventListener(action, listener, useCapture); + } else { + element.attachEvent('on' + action, listener); // IE browsers + } + }; - addFormatToken('H', ['HH', 2], 0, 'hour'); - addFormatToken('h', ['hh', 2], 0, function () { - return this.hours() % 12 || 12; - }); + /** + * Remove an event listener from an element + * @param {Element} element An html dom element + * @param {string} action The name of the event, for example "mousedown" + * @param {function} listener The listener function + * @param {boolean} [useCapture] + */ + exports.removeEventListener = function (element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) useCapture = false; - function meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { - return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); - }); + if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) { + action = 'DOMMouseScroll'; // For Firefox } - meridiem('a', true); - meridiem('A', false); + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent('on' + action, listener); + } + }; - // ALIASES + /** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + */ + exports.preventDefault = function (event) { + if (!event) event = window.event; - addUnitAlias('hour', 'h'); + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } else { + event.returnValue = false; // IE browsers + } + }; - // PARSING + /** + * Get HTML element which is the target of the event + * @param {Event} event + * @return {Element} target element + */ + exports.getTarget = function (event) { + // code from http://www.quirksmode.org/js/events_properties.html + if (!event) { + event = window.event; + } - function matchMeridiem (isStrict, locale) { - return locale._meridiemParse; - } + var target; - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); - addRegexToken('HH', match1to2, match2); - addRegexToken('hh', match1to2, match2); + if (event.target) { + target = event.target; + } else if (event.srcElement) { + target = event.srcElement; + } - addParseToken(['H', 'HH'], HOUR); - addParseToken(['a', 'A'], function (input, array, config) { - config._isPm = config._locale.isPM(input); - config._meridiem = input; - }); - addParseToken(['h', 'hh'], function (input, array, config) { - array[HOUR] = toInt(input); - getParsingFlags(config).bigHour = true; - }); + if (target.nodeType != undefined && target.nodeType == 3) { + // defeat Safari bug + target = target.parentNode; + } - // LOCALES + return target; + }; - function localeIsPM (input) { - // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays - // Using charAt should be more compatible. - return ((input + '').toLowerCase().charAt(0) === 'p'); - } + /** + * Check if given element contains given parent somewhere in the DOM tree + * @param {Element} element + * @param {Element} parent + */ + exports.hasParent = function (element, parent) { + var e = element; - var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } + while (e) { + if (e === parent) { + return true; } + e = e.parentNode; + } + return false; + }; - // MOMENTS - - // Setting the hour should keep the time, because the user explicitly - // specified which hour he wants. So trying to maintain the same hour (in - // a new timezone) makes sense. Adding/subtracting hours does not follow - // this rule. - var getSetHour = makeGetSet('Hours', true); - - addFormatToken('m', ['mm', 2], 0, 'minute'); - - // ALIASES + exports.option = {}; - addUnitAlias('minute', 'm'); + /** + * Convert a value into a boolean + * @param {Boolean | function | undefined} value + * @param {Boolean} [defaultValue] + * @returns {Boolean} bool + */ + exports.option.asBoolean = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - // PARSING + if (value != null) { + return value != false; + } - addRegexToken('m', match1to2); - addRegexToken('mm', match1to2, match2); - addParseToken(['m', 'mm'], MINUTE); + return defaultValue || null; + }; - // MOMENTS + /** + * Convert a value into a number + * @param {Boolean | function | undefined} value + * @param {Number} [defaultValue] + * @returns {Number} number + */ + exports.option.asNumber = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - var getSetMinute = makeGetSet('Minutes', false); + if (value != null) { + return Number(value) || defaultValue || null; + } - addFormatToken('s', ['ss', 2], 0, 'second'); + return defaultValue || null; + }; - // ALIASES + /** + * Convert a value into a string + * @param {String | function | undefined} value + * @param {String} [defaultValue] + * @returns {String} str + */ + exports.option.asString = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - addUnitAlias('second', 's'); + if (value != null) { + return String(value); + } - // PARSING + return defaultValue || null; + }; - addRegexToken('s', match1to2); - addRegexToken('ss', match1to2, match2); - addParseToken(['s', 'ss'], SECOND); + /** + * Convert a size or location into a string with pixels or a percentage + * @param {String | Number | function | undefined} value + * @param {String} [defaultValue] + * @returns {String} size + */ + exports.option.asSize = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - // MOMENTS + if (exports.isString(value)) { + return value; + } else if (exports.isNumber(value)) { + return value + 'px'; + } else { + return defaultValue || null; + } + }; - var getSetSecond = makeGetSet('Seconds', false); + /** + * Convert a value into a DOM element + * @param {HTMLElement | function | undefined} value + * @param {HTMLElement} [defaultValue] + * @returns {HTMLElement | null} dom + */ + exports.option.asElement = function (value, defaultValue) { + if (typeof value == 'function') { + value = value(); + } - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); - }); + return value || defaultValue || null; + }; - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); - }); + /** + * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + * + * @param {String} hex + * @returns {{r: *, g: *, b: *}} | 255 range + */ + exports.hexToRGB = function (hex) { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, function (m, r, g, b) { + return r + r + g + g + b + b; + }); + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; + }; - function millisecond__milliseconds (token) { - addFormatToken(0, [token, 3], 0, 'millisecond'); + /** + * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string. + * @param color + * @param opacity + * @returns {*} + */ + exports.overrideOpacity = function (color, opacity) { + if (color.indexOf('rgba') != -1) { + return color; + } else if (color.indexOf('rgb') != -1) { + var rgb = color.substr(color.indexOf('(') + 1).replace(')', '').split(','); + return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')'; + } else { + var rgb = exports.hexToRGB(color); + if (rgb == null) { + return color; + } else { + return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')'; } + } + }; - millisecond__milliseconds('SSS'); - millisecond__milliseconds('SSSS'); - - // ALIASES - - addUnitAlias('millisecond', 'ms'); + /** + * + * @param red 0 -- 255 + * @param green 0 -- 255 + * @param blue 0 -- 255 + * @returns {string} + * @constructor + */ + exports.RGBToHex = function (red, green, blue) { + return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1); + }; - // PARSING + /** + * Parse a color property into an object with border, background, and + * highlight colors + * @param {Object | String} color + * @return {Object} colorObject + */ + exports.parseColor = function (color) { + var c; + if (exports.isString(color) === true) { + if (exports.isValidRGB(color) === true) { + var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) { + return parseInt(value); + }); + color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]); + } + if (exports.isValidHex(color) === true) { + var hsv = exports.hexToHSV(color); + var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) }; + var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 }; + var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v); + var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v); + c = { + background: color, + border: darkerColorHex, + highlight: { + background: lighterColorHex, + border: darkerColorHex + }, + hover: { + background: lighterColorHex, + border: darkerColorHex + } + }; + } else { + c = { + background: color, + border: color, + highlight: { + background: color, + border: color + }, + hover: { + background: color, + border: color + } + }; + } + } else { + c = {}; + c.background = color.background || undefined; + c.border = color.border || undefined; - addRegexToken('S', match1to3, match1); - addRegexToken('SS', match1to3, match2); - addRegexToken('SSS', match1to3, match3); - addRegexToken('SSSS', matchUnsigned); - addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { - array[MILLISECOND] = toInt(('0.' + input) * 1000); - }); + if (exports.isString(color.highlight)) { + c.highlight = { + border: color.highlight, + background: color.highlight + }; + } else { + c.highlight = {}; + c.highlight.background = color.highlight && color.highlight.background || undefined; + c.highlight.border = color.highlight && color.highlight.border || undefined; + } - // MOMENTS + if (exports.isString(color.hover)) { + c.hover = { + border: color.hover, + background: color.hover + }; + } else { + c.hover = {}; + c.hover.background = color.hover && color.hover.background || undefined; + c.hover.border = color.hover && color.hover.border || undefined; + } + } - var getSetMillisecond = makeGetSet('Milliseconds', false); + return c; + }; - addFormatToken('z', 0, 0, 'zoneAbbr'); - addFormatToken('zz', 0, 0, 'zoneName'); + /** + * http://www.javascripter.net/faq/rgb2hsv.htm + * + * @param red + * @param green + * @param blue + * @returns {*} + * @constructor + */ + exports.RGBToHSV = function (red, green, blue) { + red = red / 255;green = green / 255;blue = blue / 255; + var minRGB = Math.min(red, Math.min(green, blue)); + var maxRGB = Math.max(red, Math.max(green, blue)); - // MOMENTS + // Black-gray-white + if (minRGB == maxRGB) { + return { h: 0, s: 0, v: minRGB }; + } - function getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; - } + // Colors other than black-gray-white: + var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red; + var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5; + var hue = 60 * (h - d / (maxRGB - minRGB)) / 360; + var saturation = (maxRGB - minRGB) / maxRGB; + var value = maxRGB; + return { h: hue, s: saturation, v: value }; + }; - function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - } + var cssUtil = { + // split a string with css styles into an object with key/values + split: function split(cssText) { + var styles = {}; - var momentPrototype__proto = Moment.prototype; + cssText.split(';').forEach(function (style) { + if (style.trim() != '') { + var parts = style.split(':'); + var key = parts[0].trim(); + var value = parts[1].trim(); + styles[key] = value; + } + }); - momentPrototype__proto.add = add_subtract__add; - momentPrototype__proto.calendar = moment_calendar__calendar; - momentPrototype__proto.clone = clone; - momentPrototype__proto.diff = diff; - momentPrototype__proto.endOf = endOf; - momentPrototype__proto.format = format; - momentPrototype__proto.from = from; - momentPrototype__proto.fromNow = fromNow; - momentPrototype__proto.to = to; - momentPrototype__proto.toNow = toNow; - momentPrototype__proto.get = getSet; - momentPrototype__proto.invalidAt = invalidAt; - momentPrototype__proto.isAfter = isAfter; - momentPrototype__proto.isBefore = isBefore; - momentPrototype__proto.isBetween = isBetween; - momentPrototype__proto.isSame = isSame; - momentPrototype__proto.isValid = moment_valid__isValid; - momentPrototype__proto.lang = lang; - momentPrototype__proto.locale = locale; - momentPrototype__proto.localeData = localeData; - momentPrototype__proto.max = prototypeMax; - momentPrototype__proto.min = prototypeMin; - momentPrototype__proto.parsingFlags = parsingFlags; - momentPrototype__proto.set = getSet; - momentPrototype__proto.startOf = startOf; - momentPrototype__proto.subtract = add_subtract__subtract; - momentPrototype__proto.toArray = toArray; - momentPrototype__proto.toDate = toDate; - momentPrototype__proto.toISOString = moment_format__toISOString; - momentPrototype__proto.toJSON = moment_format__toISOString; - momentPrototype__proto.toString = toString; - momentPrototype__proto.unix = unix; - momentPrototype__proto.valueOf = to_type__valueOf; + return styles; + }, - // Year - momentPrototype__proto.year = getSetYear; - momentPrototype__proto.isLeapYear = getIsLeapYear; + // build a css text string from an object with key/values + join: function join(styles) { + return Object.keys(styles).map(function (key) { + return key + ': ' + styles[key]; + }).join('; '); + } + }; - // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; - momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + /** + * Append a string with css styles to an element + * @param {Element} element + * @param {String} cssText + */ + exports.addCssText = function (element, cssText) { + var currentStyles = cssUtil.split(element.style.cssText); + var newStyles = cssUtil.split(cssText); + var styles = exports.extend(currentStyles, newStyles); - // Quarter - momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + element.style.cssText = cssUtil.join(styles); + }; - // Month - momentPrototype__proto.month = getSetMonth; - momentPrototype__proto.daysInMonth = getDaysInMonth; + /** + * Remove a string with css styles from an element + * @param {Element} element + * @param {String} cssText + */ + exports.removeCssText = function (element, cssText) { + var styles = cssUtil.split(element.style.cssText); + var removeStyles = cssUtil.split(cssText); - // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; - momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + for (var key in removeStyles) { + if (removeStyles.hasOwnProperty(key)) { + delete styles[key]; + } + } - // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; - momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; + element.style.cssText = cssUtil.join(styles); + }; - // Hour - momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; + /** + * https://gist.github.com/mjijackson/5311256 + * @param h + * @param s + * @param v + * @returns {{r: number, g: number, b: number}} + * @constructor + */ + exports.HSVToRGB = function (h, s, v) { + var r, g, b; - // Minute - momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); - // Second - momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + switch (i % 6) { + case 0: + r = v, g = t, b = p;break; + case 1: + r = q, g = v, b = p;break; + case 2: + r = p, g = v, b = t;break; + case 3: + r = p, g = q, b = v;break; + case 4: + r = t, g = p, b = v;break; + case 5: + r = v, g = p, b = q;break; + } - // Millisecond - momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) }; + }; - // Offset - momentPrototype__proto.utcOffset = getSetOffset; - momentPrototype__proto.utc = setOffsetToUTC; - momentPrototype__proto.local = setOffsetToLocal; - momentPrototype__proto.parseZone = setOffsetToParsedOffset; - momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; - momentPrototype__proto.isDST = isDaylightSavingTime; - momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; - momentPrototype__proto.isLocal = isLocal; - momentPrototype__proto.isUtcOffset = isUtcOffset; - momentPrototype__proto.isUtc = isUtc; - momentPrototype__proto.isUTC = isUtc; + exports.HSVToHex = function (h, s, v) { + var rgb = exports.HSVToRGB(h, s, v); + return exports.RGBToHex(rgb.r, rgb.g, rgb.b); + }; - // Timezone - momentPrototype__proto.zoneAbbr = getZoneAbbr; - momentPrototype__proto.zoneName = getZoneName; + exports.hexToHSV = function (hex) { + var rgb = exports.hexToRGB(hex); + return exports.RGBToHSV(rgb.r, rgb.g, rgb.b); + }; - // Deprecations - momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); - momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); - momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); - momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); + exports.isValidHex = function (hex) { + var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); + return isOk; + }; - var momentPrototype = momentPrototype__proto; + exports.isValidRGB = function (rgb) { + rgb = rgb.replace(' ', ''); + var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb); + return isOk; + }; + exports.isValidRGBA = function (rgba) { + rgba = rgba.replace(' ', ''); + var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba); + return isOk; + }; - function moment__createUnix (input) { - return local__createLocal(input * 1000); + /** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ + exports.selectiveBridgeObject = function (fields, referenceObject) { + if (typeof referenceObject == 'object') { + var objectTo = Object.create(referenceObject); + for (var i = 0; i < fields.length; i++) { + if (referenceObject.hasOwnProperty(fields[i])) { + if (typeof referenceObject[fields[i]] == 'object') { + objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]); + } + } } + return objectTo; + } else { + return null; + } + }; - function moment__createInZone () { - return local__createLocal.apply(null, arguments).parseZone(); + /** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ + exports.bridgeObject = function (referenceObject) { + if (typeof referenceObject == 'object') { + var objectTo = Object.create(referenceObject); + for (var i in referenceObject) { + if (referenceObject.hasOwnProperty(i)) { + if (typeof referenceObject[i] == 'object') { + objectTo[i] = exports.bridgeObject(referenceObject[i]); + } + } } + return objectTo; + } else { + return null; + } + }; - var defaultCalendar = { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }; + /** + * this is used to set the options of subobjects in the options object. A requirement of these subobjects + * is that they have an 'enabled' element which is optional for the user but mandatory for the program. + * + * @param [object] mergeTarget | this is either this.options or the options used for the groups. + * @param [object] options | options + * @param [String] option | this is the option key in the options argument + * @private + */ + exports.mergeOptions = function (mergeTarget, options, option) { + var allowDeletion = arguments[3] === undefined ? false : arguments[3]; - function locale_calendar__calendar (key, mom, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.call(mom, now) : output; + if (options[option] === null) { + mergeTarget[option] = undefined; + delete mergeTarget[option]; + } else { + if (options[option] !== undefined) { + if (typeof options[option] === 'boolean') { + mergeTarget[option].enabled = options[option]; + } else { + if (options[option].enabled === undefined) { + mergeTarget[option].enabled = true; + } + for (var prop in options[option]) { + if (options[option].hasOwnProperty(prop)) { + mergeTarget[option][prop] = options[option][prop]; + } + } + } } + } + }; - var defaultLongDateFormat = { - LTS : 'h:mm:ss A', - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }; + /** + * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses + * this function will then iterate in both directions over this sorted list to find all visible items. + * + * @param {Item[]} orderedItems | Items ordered by start + * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher + * @param {String} field + * @param {String} field2 + * @returns {number} + * @private + */ + exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) { + var maxIterations = 10000; + var iteration = 0; + var low = 0; + var high = orderedItems.length - 1; - function longDateFormat (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; - } - return output; - } + while (low <= high && iteration < maxIterations) { + var middle = Math.floor((low + high) / 2); - var defaultInvalidDate = 'Invalid date'; + var item = orderedItems[middle]; + var value = field2 === undefined ? item[field] : item[field][field2]; - function invalidDate () { - return this._invalidDate; + var searchResult = searchFunction(value); + if (searchResult == 0) { + // jihaa, found a visible item! + return middle; + } else if (searchResult == -1) { + // it is too small --> increase low + low = middle + 1; + } else { + // it is too big --> decrease high + high = middle - 1; } - var defaultOrdinal = '%d'; - var defaultOrdinalParse = /\d{1,2}/; + iteration++; + } - function ordinal (number) { - return this._ordinal.replace('%d', number); - } + return -1; + }; - function preParsePostFormat (string) { - return string; - } + /** + * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of + * two values, we return either the one before or the one after, depending on user input + * If it is found, we return the index, else -1. + * + * @param {Array} orderedItems + * @param {{start: number, end: number}} target + * @param {String} field + * @param {String} sidePreference 'before' or 'after' + * @returns {number} + * @private + */ + exports.binarySearchValue = function (orderedItems, target, field, sidePreference) { + var maxIterations = 10000; + var iteration = 0; + var low = 0; + var high = orderedItems.length - 1; + var prevValue, value, nextValue, middle; - var defaultRelativeTime = { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' - }; + while (low <= high && iteration < maxIterations) { + // get a new guess + middle = Math.floor(0.5 * (high + low)); + prevValue = orderedItems[Math.max(0, middle - 1)][field]; + value = orderedItems[middle][field]; + nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field]; - function relative__relativeTime (number, withoutSuffix, string, isFuture) { - var output = this._relativeTime[string]; - return (typeof output === 'function') ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); + if (value == target) { + // we found the target + return middle; + } else if (prevValue < target && value > target) { + // target is in between of the previous and the current + return sidePreference == 'before' ? Math.max(0, middle - 1) : middle; + } else if (value < target && nextValue > target) { + // target is in between of the current and the next + return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1); + } else { + // didnt find the target, we need to change our boundaries. + if (value < target) { + // it is too small --> increase low + low = middle + 1; + } else { + // it is too big --> decrease high + high = middle - 1; + } } + iteration++; + } - function pastFuture (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - } + // didnt find anything. Return -1. + return -1; + }; - function locale_set__set (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; - } else { - this['_' + i] = prop; - } - } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); - } + /* + * Easing Functions - inspired from http://gizma.com/easing/ + * only considering the t value for the range [0, 1] => [0, 1] + * https://gist.github.com/gre/1650294 + */ + exports.easingFunctions = { + // no easing, no acceleration + linear: function linear(t) { + return t; + }, + // accelerating from zero velocity + easeInQuad: function easeInQuad(t) { + return t * t; + }, + // decelerating to zero velocity + easeOutQuad: function easeOutQuad(t) { + return t * (2 - t); + }, + // acceleration until halfway, then deceleration + easeInOutQuad: function easeInOutQuad(t) { + return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; + }, + // accelerating from zero velocity + easeInCubic: function easeInCubic(t) { + return t * t * t; + }, + // decelerating to zero velocity + easeOutCubic: function easeOutCubic(t) { + return --t * t * t + 1; + }, + // acceleration until halfway, then deceleration + easeInOutCubic: function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; + }, + // accelerating from zero velocity + easeInQuart: function easeInQuart(t) { + return t * t * t * t; + }, + // decelerating to zero velocity + easeOutQuart: function easeOutQuart(t) { + return 1 - --t * t * t * t; + }, + // acceleration until halfway, then deceleration + easeInOutQuart: function easeInOutQuart(t) { + return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; + }, + // accelerating from zero velocity + easeInQuint: function easeInQuint(t) { + return t * t * t * t * t; + }, + // decelerating to zero velocity + easeOutQuint: function easeOutQuint(t) { + return 1 + --t * t * t * t * t; + }, + // acceleration until halfway, then deceleration + easeInOutQuint: function easeInOutQuint(t) { + return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; + } + }; - var prototype__proto = Locale.prototype; +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { - prototype__proto._calendar = defaultCalendar; - prototype__proto.calendar = locale_calendar__calendar; - prototype__proto._longDateFormat = defaultLongDateFormat; - prototype__proto.longDateFormat = longDateFormat; - prototype__proto._invalidDate = defaultInvalidDate; - prototype__proto.invalidDate = invalidDate; - prototype__proto._ordinal = defaultOrdinal; - prototype__proto.ordinal = ordinal; - prototype__proto._ordinalParse = defaultOrdinalParse; - prototype__proto.preparse = preParsePostFormat; - prototype__proto.postformat = preParsePostFormat; - prototype__proto._relativeTime = defaultRelativeTime; - prototype__proto.relativeTime = relative__relativeTime; - prototype__proto.pastFuture = pastFuture; - prototype__proto.set = locale_set__set; + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. + 'use strict'; - // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; - prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; + module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(16); - // Week - prototype__proto.week = localeWeek; - prototype__proto._week = defaultLocaleWeek; - prototype__proto.firstDayOfYear = localeFirstDayOfYear; - prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { - // Day of Week - prototype__proto.weekdays = localeWeekdays; - prototype__proto._weekdays = defaultLocaleWeekdays; - prototype__proto.weekdaysMin = localeWeekdaysMin; - prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; - prototype__proto.weekdaysShort = localeWeekdaysShort; - prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; - prototype__proto.weekdaysParse = localeWeekdaysParse; + /* WEBPACK VAR INJECTION */(function(module) {//! moment.js + //! version : 2.10.3 + //! authors : Tim Wood, Iskren Chernev, Moment.js contributors + //! license : MIT + //! momentjs.com - // Hours - prototype__proto.isPM = localeIsPM; - prototype__proto._meridiemParse = defaultLocaleMeridiemParse; - prototype__proto.meridiem = localeMeridiem; + (function (global, factory) { + true ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() + }(this, function () { 'use strict'; - function lists__get (format, index, field, setter) { - var locale = locale_locales__getLocale(); - var utc = create_utc__createUTC().set(setter, index); - return locale[field](utc, format); + var hookCallback; + + function utils_hooks__hooks () { + return hookCallback.apply(null, arguments); } - function list (format, index, field, count, setter) { - if (typeof format === 'number') { - index = format; - format = undefined; + // This is done to register the method called with moment() + // without creating circular dependencies. + function setHookCallback (callback) { + hookCallback = callback; + } + + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } + + function isDate(input) { + return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; + } + + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); } + return res; + } - format = format || ''; + function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); + } - if (index != null) { - return lists__get(format, index, field, setter); + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } } - var i; - var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; } - return out; + + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } + + return a; } - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); + function create_utc__createUTC (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); } - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); + function defaultParsingFlags() { + // We need to deep clone this object. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso : false + }; } - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); + function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; } - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); - } + function valid__isValid(m) { + if (m._isValid == null) { + var flags = getParsingFlags(m); + m._isValid = !isNaN(m._d.getTime()) && + flags.overflow < 0 && + !flags.empty && + !flags.invalidMonth && + !flags.nullInput && + !flags.invalidFormat && + !flags.userInvalidated; - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); + if (m._strict) { + m._isValid = m._isValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + } + return m._isValid; } - locale_locales__getSetGlobalLocale('en', { - ordinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; + function valid__createInvalid (flags) { + var m = create_utc__createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } + else { + getParsingFlags(m).userInvalidated = true; } - }); - // Side effect imports - utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); - utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); + return m; + } - var mathAbs = Math.abs; + var momentProperties = utils_hooks__hooks.momentProperties = []; - function duration_abs__abs () { - var data = this._data; + function copyConfig(to, from) { + var i, prop, val; - this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); + if (typeof from._isAMomentObject !== 'undefined') { + to._isAMomentObject = from._isAMomentObject; + } + if (typeof from._i !== 'undefined') { + to._i = from._i; + } + if (typeof from._f !== 'undefined') { + to._f = from._f; + } + if (typeof from._l !== 'undefined') { + to._l = from._l; + } + if (typeof from._strict !== 'undefined') { + to._strict = from._strict; + } + if (typeof from._tzm !== 'undefined') { + to._tzm = from._tzm; + } + if (typeof from._isUTC !== 'undefined') { + to._isUTC = from._isUTC; + } + if (typeof from._offset !== 'undefined') { + to._offset = from._offset; + } + if (typeof from._pf !== 'undefined') { + to._pf = getParsingFlags(from); + } + if (typeof from._locale !== 'undefined') { + to._locale = from._locale; + } - data.milliseconds = mathAbs(data.milliseconds); - data.seconds = mathAbs(data.seconds); - data.minutes = mathAbs(data.minutes); - data.hours = mathAbs(data.hours); - data.months = mathAbs(data.months); - data.years = mathAbs(data.years); + if (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } - return this; + return to; } - function duration_add_subtract__addSubtract (duration, input, value, direction) { - var other = create__createDuration(input, value); - - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; - - return duration._bubble(); - } + var updateInProgress = false; - // supports only 2.0-style add(1, 's') or add(duration) - function duration_add_subtract__add (input, value) { - return duration_add_subtract__addSubtract(this, input, value, 1); + // Moment prototype object + function Moment(config) { + copyConfig(this, config); + this._d = new Date(+config._d); + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + utils_hooks__hooks.updateOffset(this); + updateInProgress = false; + } } - // supports only 2.0-style subtract(1, 's') or subtract(duration) - function duration_add_subtract__subtract (input, value) { - return duration_add_subtract__addSubtract(this, input, value, -1); + function isMoment (obj) { + return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); } - function bubble () { - var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; - var seconds, minutes, hours, years = 0; - - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; - - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; - - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; - - hours = absFloor(minutes / 60); - data.hours = hours % 24; + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - days += absFloor(hours / 24); + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } - // Accurately convert days to years, assume start from year 0. - years = absFloor(daysToYears(days)); - days -= absFloor(yearsToDays(years)); + return value; + } - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absFloor(days / 30); - days %= 30; + function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ((dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { + diffs++; + } + } + return diffs + lengthDiff; + } - // 12 months -> 1 year - years += absFloor(months / 12); - months %= 12; + function Locale() { + } - data.days = days; - data.months = months; - data.years = years; + var locales = {}; + var globalLocale; - return this; + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; } - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; - } + // pick the locale from the array + // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + function chooseLocale(names) { + var i = 0, j, next, locale, split; - function yearsToDays (years) { - // years * 365 + absFloor(years / 4) - - // absFloor(years / 100) + absFloor(years / 400); - return years * 146097 / 400; + while (i < names.length) { + split = normalizeLocale(names[i]).split('-'); + j = split.length; + next = normalizeLocale(names[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + locale = loadLocale(split.slice(0, j).join('-')); + if (locale) { + return locale; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return null; } - function as (units) { - var days; - var months; - var milliseconds = this._milliseconds; + function loadLocale(name) { + var oldLocale = null; + // TODO: Find a better way to register and load all the locales in Node + if (!locales[name] && typeof module !== 'undefined' && + module && module.exports) { + try { + oldLocale = globalLocale._abbr; + !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); + // because defineLocale currently also sets the global locale, we + // want to undo that for lazy loaded locales + locale_locales__getSetGlobalLocale(oldLocale); + } catch (e) { } + } + return locales[name]; + } - units = normalizeUnits(units); + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + function locale_locales__getSetGlobalLocale (key, values) { + var data; + if (key) { + if (typeof values === 'undefined') { + data = locale_locales__getLocale(key); + } + else { + data = defineLocale(key, values); + } - if (units === 'month' || units === 'year') { - days = this._days + milliseconds / 864e5; - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(yearsToDays(this._months / 12)); - switch (units) { - case 'week' : return days / 7 + milliseconds / 6048e5; - case 'day' : return days + milliseconds / 864e5; - case 'hour' : return days * 24 + milliseconds / 36e5; - case 'minute' : return days * 1440 + milliseconds / 6e4; - case 'second' : return days * 86400 + milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 864e5) + milliseconds; - default: throw new Error('Unknown unit ' + units); + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; } } - } - // TODO: Use this.as('ms')? - function duration_as__valueOf () { - return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); + return globalLocale._abbr; } - function makeAs (alias) { - return function () { - return this.as(alias); - }; - } + function defineLocale (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); + } + locales[name].set(values); - var asMilliseconds = makeAs('ms'); - var asSeconds = makeAs('s'); - var asMinutes = makeAs('m'); - var asHours = makeAs('h'); - var asDays = makeAs('d'); - var asWeeks = makeAs('w'); - var asMonths = makeAs('M'); - var asYears = makeAs('y'); + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); - function duration_get__get (units) { - units = normalizeUnits(units); - return this[units + 's'](); + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } } - function makeGetter(name) { - return function () { - return this._data[name]; - }; - } + // returns locale data + function locale_locales__getLocale (key) { + var locale; - var duration_get__milliseconds = makeGetter('milliseconds'); - var seconds = makeGetter('seconds'); - var minutes = makeGetter('minutes'); - var hours = makeGetter('hours'); - var days = makeGetter('days'); - var months = makeGetter('months'); - var years = makeGetter('years'); + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } - function weeks () { - return absFloor(this.days() / 7); - } + if (!key) { + return globalLocale; + } - var round = Math.round; - var thresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }; + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + return chooseLocale(key); } - function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { - var duration = create__createDuration(posNegDuration).abs(); - var seconds = round(duration.as('s')); - var minutes = round(duration.as('m')); - var hours = round(duration.as('h')); - var days = round(duration.as('d')); - var months = round(duration.as('M')); - var years = round(duration.as('y')); - - var a = seconds < thresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; + var aliases = {}; - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); + function addUnitAlias (unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; } - // This function allows you to set a threshold for relative time strings - function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { - if (thresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return thresholds[threshold]; - } - thresholds[threshold] = limit; - return true; + function normalizeUnits(units) { + return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; } - function humanize (withSuffix) { - var locale = this.localeData(); - var output = duration_humanize__relativeTime(this, !withSuffix, locale); + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; - if (withSuffix) { - output = locale.pastFuture(+this, output); + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } } - return locale.postformat(output); + return normalizedInput; } - var iso_string__abs = Math.abs; - - function iso_string__toISOString() { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var Y = iso_string__abs(this.years()); - var M = iso_string__abs(this.months()); - var D = iso_string__abs(this.days()); - var h = iso_string__abs(this.hours()); - var m = iso_string__abs(this.minutes()); - var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); - var total = this.asSeconds(); + function makeGetSet (unit, keepTime) { + return function (value) { + if (value != null) { + get_set__set(this, unit, value); + utils_hooks__hooks.updateOffset(this, keepTime); + return this; + } else { + return get_set__get(this, unit); + } + }; + } - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } + function get_set__get (mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } - return (total < 0 ? '-' : '') + - 'P' + - (Y ? Y + 'Y' : '') + - (M ? M + 'M' : '') + - (D ? D + 'D' : '') + - ((h || m || s) ? 'T' : '') + - (h ? h + 'H' : '') + - (m ? m + 'M' : '') + - (s ? s + 'S' : ''); + function get_set__set (mom, unit, value) { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); } - var duration_prototype__proto = Duration.prototype; + // MOMENTS - duration_prototype__proto.abs = duration_abs__abs; - duration_prototype__proto.add = duration_add_subtract__add; - duration_prototype__proto.subtract = duration_add_subtract__subtract; - duration_prototype__proto.as = as; - duration_prototype__proto.asMilliseconds = asMilliseconds; - duration_prototype__proto.asSeconds = asSeconds; - duration_prototype__proto.asMinutes = asMinutes; - duration_prototype__proto.asHours = asHours; - duration_prototype__proto.asDays = asDays; - duration_prototype__proto.asWeeks = asWeeks; - duration_prototype__proto.asMonths = asMonths; - duration_prototype__proto.asYears = asYears; - duration_prototype__proto.valueOf = duration_as__valueOf; - duration_prototype__proto._bubble = bubble; - duration_prototype__proto.get = duration_get__get; - duration_prototype__proto.milliseconds = duration_get__milliseconds; - duration_prototype__proto.seconds = seconds; - duration_prototype__proto.minutes = minutes; - duration_prototype__proto.hours = hours; - duration_prototype__proto.days = days; - duration_prototype__proto.weeks = weeks; - duration_prototype__proto.months = months; - duration_prototype__proto.years = years; - duration_prototype__proto.humanize = humanize; - duration_prototype__proto.toISOString = iso_string__toISOString; - duration_prototype__proto.toString = iso_string__toISOString; - duration_prototype__proto.toJSON = iso_string__toISOString; - duration_prototype__proto.locale = locale; - duration_prototype__proto.localeData = localeData; + function getSet (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } + } else { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + return this[units](value); + } + } + return this; + } - // Deprecations - duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); - duration_prototype__proto.lang = lang; + function zeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; - // Side effect imports + while (output.length < targetLength) { + output = '0' + output; + } + return (sign ? (forceSign ? '+' : '') : '-') + output; + } - addFormatToken('X', 0, 0, 'unix'); - addFormatToken('x', 0, 0, 'valueOf'); + var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; - // PARSING + var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; - addRegexToken('x', matchSigned); - addRegexToken('X', matchTimestamp); - addParseToken('X', function (input, array, config) { - config._d = new Date(parseFloat(input, 10) * 1000); - }); - addParseToken('x', function (input, array, config) { - config._d = new Date(toInt(input)); - }); + var formatFunctions = {}; - // Side effect imports + var formatTokenFunctions = {}; + // token: 'M' + // padded: ['MM', 2] + // ordinal: 'Mo' + // callback: function () { this.month() + 1 } + function addFormatToken (token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal(func.apply(this, arguments), token); + }; + } + } - utils_hooks__hooks.version = '2.10.3'; + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); + } - setHookCallback(local__createLocal); + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; - utils_hooks__hooks.fn = momentPrototype; - utils_hooks__hooks.min = min; - utils_hooks__hooks.max = max; - utils_hooks__hooks.utc = create_utc__createUTC; - utils_hooks__hooks.unix = moment__createUnix; - utils_hooks__hooks.months = lists__listMonths; - utils_hooks__hooks.isDate = isDate; - utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; - utils_hooks__hooks.invalid = valid__createInvalid; - utils_hooks__hooks.duration = create__createDuration; - utils_hooks__hooks.isMoment = isMoment; - utils_hooks__hooks.weekdays = lists__listWeekdays; - utils_hooks__hooks.parseZone = moment__createInZone; - utils_hooks__hooks.localeData = locale_locales__getLocale; - utils_hooks__hooks.isDuration = isDuration; - utils_hooks__hooks.monthsShort = lists__listMonthsShort; - utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; - utils_hooks__hooks.defineLocale = defineLocale; - utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; - utils_hooks__hooks.normalizeUnits = normalizeUnits; - utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } - var _moment = utils_hooks__hooks; + return function (mom) { + var output = ''; + for (i = 0; i < length; i++) { + output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; + } + return output; + }; + } - return _moment; + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } - })); - /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(12)(module))) + format = expandFormat(format, m.localeData()); -/***/ }, -/* 12 */ -/***/ function(module, exports, __webpack_require__) { + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } - module.exports = function(module) { - if(!module.webpackPolyfill) { - module.deprecate = function() {}; - module.paths = []; - // module.parent = undefined by default - module.children = []; - module.webpackPolyfill = 1; - } - return module; - } + return formatFunctions[format](m); + } + + function expandFormat(format, locale) { + var i = 5; + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } - /* WEBPACK VAR INJECTION */(function(global) {'use strict'; + return format; + } - var _rng; + var match1 = /\d/; // 0 - 9 + var match2 = /\d\d/; // 00 - 99 + var match3 = /\d{3}/; // 000 - 999 + var match4 = /\d{4}/; // 0000 - 9999 + var match6 = /[+-]?\d{6}/; // -999999 - 999999 + var match1to2 = /\d\d?/; // 0 - 99 + var match1to3 = /\d{1,3}/; // 0 - 999 + var match1to4 = /\d{1,4}/; // 0 - 9999 + var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; + var matchUnsigned = /\d+/; // 0 - inf + var matchSigned = /[+-]?\d+/; // -inf - inf - if (globalVar && globalVar.crypto && crypto.getRandomValues) { - // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto - // Moderately fast, high quality - var _rnds8 = new Uint8Array(16); - _rng = function whatwgRNG() { - crypto.getRandomValues(_rnds8); - return _rnds8; - }; - } + var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z - if (!_rng) { - // Math.random()-based (RNG) - // - // If all else fails, use Math.random(). It's fast, but is of unspecified - // quality. - var _rnds = new Array(16); - _rng = function () { - for (var i = 0, r; i < 16; i++) { - if ((i & 3) === 0) r = Math.random() * 4294967296; - _rnds[i] = r >>> ((i & 3) << 3) & 255; + var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + + // any word (or two) characters or numbers including two/three word month in arabic. + var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; + + var regexes = {}; + + function addRegexToken (token, regex, strictRegex) { + regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { + return (isStrict && strictRegex) ? strictRegex : regex; + }; } - return _rnds; - }; - } + function getParseRegexForToken (token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } - // uuid.js - // - // Copyright (c) 2010-2012 Robert Kieffer - // MIT License - http://opensource.org/licenses/mit-license.php + return regexes[token](config._strict, config._locale); + } - // Unique ID creation requires a high quality random # generator. We feature - // detect to determine the best RNG source, normalizing to a function that - // returns 128-bits of randomness, since that's what's usually required + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function unescapeFormat(s) { + return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } - //var _rng = require('./rng'); + var tokens = {}; - // Maps for number <-> hex string conversion - var _byteToHex = []; - var _hexToByte = {}; - for (var i = 0; i < 256; i++) { - _byteToHex[i] = (i + 256).toString(16).substr(1); - _hexToByte[_byteToHex[i]] = i; - } + function addParseToken (token, callback) { + var i, func = callback; + if (typeof token === 'string') { + token = [token]; + } + if (typeof callback === 'number') { + func = function (input, array) { + array[callback] = toInt(input); + }; + } + for (i = 0; i < token.length; i++) { + tokens[token[i]] = func; + } + } - // **`parse()` - Parse a UUID into it's component bytes** - function parse(s, buf, offset) { - var i = buf && offset || 0, - ii = 0; + function addWeekParseToken (token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); + } - buf = buf || []; - s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { - if (ii < 16) { - // Don't overflow! - buf[i + ii++] = _hexToByte[oct]; + function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } } - }); - // Zero out remaining bytes if string was short - while (ii < 16) { - buf[i + ii++] = 0; - } + var YEAR = 0; + var MONTH = 1; + var DATE = 2; + var HOUR = 3; + var MINUTE = 4; + var SECOND = 5; + var MILLISECOND = 6; - return buf; - } + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } - // **`unparse()` - Convert UUID byte array (ala parse()) into a string** - function unparse(buf, offset) { - var i = offset || 0, - bth = _byteToHex; - return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]]; - } + // FORMATTING - // **`v1()` - Generate time-based UUID** - // - // Inspired by https://github.com/LiosK/UUID.js - // and http://docs.python.org/library/uuid.html + addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; + }); - // random #'s we need to init node and clockseq - var _seedBytes = _rng(); + addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); + }); - // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) - var _nodeId = [_seedBytes[0] | 1, _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]]; + addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); + }); - // Per 4.2.2, randomize (14 bit) clockseq - var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; + // ALIASES - // Previous uuid creation time - var _lastMSecs = 0, - _lastNSecs = 0; + addUnitAlias('month', 'M'); - // See https://github.com/broofa/node-uuid for API details - function v1(options, buf, offset) { - var i = buf && offset || 0; - var b = buf || []; + // PARSING - options = options || {}; + addRegexToken('M', match1to2); + addRegexToken('MM', match1to2, match2); + addRegexToken('MMM', matchWord); + addRegexToken('MMMM', matchWord); - var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; + }); - // UUID timestamps are 100 nano-second units since the Gregorian epoch, - // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so - // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' - // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. - var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); + addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + var month = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (month != null) { + array[MONTH] = month; + } else { + getParsingFlags(config).invalidMonth = input; + } + }); - // Per 4.2.1.2, use count of uuid's generated during the current clock - // cycle to simulate higher resolution clock - var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; + // LOCALES - // Time since last uuid creation (in msecs) - var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; + var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); + function localeMonths (m) { + return this._months[m.month()]; + } - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0 && options.clockseq === undefined) { - clockseq = clockseq + 1 & 16383; - } + var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); + function localeMonthsShort (m) { + return this._monthsShort[m.month()]; + } - // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new - // time interval - if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { - nsecs = 0; - } + function localeMonthsParse (monthName, format, strict) { + var i, mom, regex; - // Per 4.2.1.2 Throw error if too many uuids are requested - if (nsecs >= 10000) { - throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); - } + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } - _lastMSecs = msecs; - _lastNSecs = nsecs; - _clockseq = clockseq; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); + this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); + } + if (!strict && !this._monthsParse[i]) { + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { + return i; + } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; + } + } + } - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - msecs += 12219292800000; + // MOMENTS - // `time_low` - var tl = ((msecs & 268435455) * 10000 + nsecs) % 4294967296; - b[i++] = tl >>> 24 & 255; - b[i++] = tl >>> 16 & 255; - b[i++] = tl >>> 8 & 255; - b[i++] = tl & 255; + function setMonth (mom, value) { + var dayOfMonth; - // `time_mid` - var tmh = msecs / 4294967296 * 10000 & 268435455; - b[i++] = tmh >>> 8 & 255; - b[i++] = tmh & 255; + // TODO: Move this out of here! + if (typeof value === 'string') { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } + } - // `time_high_and_version` - b[i++] = tmh >>> 24 & 15 | 16; // include version - b[i++] = tmh >>> 16 & 255; + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - b[i++] = clockseq >>> 8 | 128; + function getSetMonth (value) { + if (value != null) { + setMonth(this, value); + utils_hooks__hooks.updateOffset(this, true); + return this; + } else { + return get_set__get(this, 'Month'); + } + } - // `clock_seq_low` - b[i++] = clockseq & 255; + function getDaysInMonth () { + return daysInMonth(this.year(), this.month()); + } - // `node` - var node = options.node || _nodeId; - for (var n = 0; n < 6; n++) { - b[i + n] = node[n]; - } + function checkOverflow (m) { + var overflow; + var a = m._a; - return buf ? buf : unparse(b); - } + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : + a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : + a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : + a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : + a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : + a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : + -1; - // **`v4()` - Generate random UUID** + if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } - // See https://github.com/broofa/node-uuid for API details - function v4(options, buf, offset) { - // Deprecated - 'format' argument, as supported in v1.2 - var i = buf && offset || 0; + getParsingFlags(m).overflow = overflow; + } - if (typeof options == 'string') { - buf = options == 'binary' ? new Array(16) : null; - options = null; - } - options = options || {}; + return m; + } - var rnds = options.random || (options.rng || _rng)(); + function warn(msg) { + if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); + } + } - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = rnds[6] & 15 | 64; - rnds[8] = rnds[8] & 63 | 128; + function deprecate(msg, fn) { + var firstTime = true, + msgWithStack = msg + '\n' + (new Error()).stack; - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ii++) { - buf[i + ii] = rnds[ii]; + return extend(function () { + if (firstTime) { + warn(msgWithStack); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); } - } - return buf || unparse(rnds); - } + var deprecations = {}; - // Export public API - var uuid = v4; - uuid.v1 = v1; - uuid.v4 = v4; - uuid.parse = parse; - uuid.unparse = unparse; + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } + } - module.exports = uuid; - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) + utils_hooks__hooks.suppressDeprecationWarnings = false; + + var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { + var isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], + ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], + ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], + ['GGGG-[W]WW', /\d{4}-W\d{2}/], + ['YYYY-DDD', /\d{4}-\d{3}/] + ]; - // DOM utility methods + // iso time formats and regexes + var isoTimes = [ + ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], + ['HH:mm', /(T| )\d\d:\d\d/], + ['HH', /(T| )\d\d/] + ]; - /** - * this prepares the JSON container for allocating SVG elements - * @param JSONcontainer - * @private - */ - 'use strict'; + var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; - exports.prepareElements = function (JSONcontainer) { - // cleanup the redundant svgElements; - for (var elementType in JSONcontainer) { - if (JSONcontainer.hasOwnProperty(elementType)) { - JSONcontainer[elementType].redundant = JSONcontainer[elementType].used; - JSONcontainer[elementType].used = []; - } - } - }; + // date from iso format + function configFromISO(config) { + var i, l, + string = config._i, + match = from_string__isoRegex.exec(string); - /** - * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from - * which to remove the redundant elements. - * - * @param JSONcontainer - * @private - */ - exports.cleanupElements = function (JSONcontainer) { - // cleanup the redundant svgElements; - for (var elementType in JSONcontainer) { - if (JSONcontainer.hasOwnProperty(elementType)) { - if (JSONcontainer[elementType].redundant) { - for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) { - JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]); + if (match) { + getParsingFlags(config).iso = true; + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(string)) { + // match[5] should be 'T' or undefined + config._f = isoDates[i][0] + (match[6] || ' '); + break; + } + } + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(string)) { + config._f += isoTimes[i][0]; + break; + } + } + if (string.match(matchOffset)) { + config._f += 'Z'; + } + configFromStringAndFormat(config); + } else { + config._isValid = false; } - JSONcontainer[elementType].redundant = []; - } } - } - }; - /** - * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer - * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. - * - * @param elementType - * @param JSONcontainer - * @param svgContainer - * @returns {*} - * @private - */ - exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) { - var element; - // allocate SVG element, if it doesnt yet exist, create one. - if (JSONcontainer.hasOwnProperty(elementType)) { - // this element has been created before - // check if there is an redundant element - if (JSONcontainer[elementType].redundant.length > 0) { - element = JSONcontainer[elementType].redundant[0]; - JSONcontainer[elementType].redundant.shift(); - } else { - // create a new element and add it to the SVG - element = document.createElementNS('http://www.w3.org/2000/svg', elementType); - svgContainer.appendChild(element); - } - } else { - // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. - element = document.createElementNS('http://www.w3.org/2000/svg', elementType); - JSONcontainer[elementType] = { used: [], redundant: [] }; - svgContainer.appendChild(element); - } - JSONcontainer[elementType].used.push(element); - return element; - }; + // date from iso format or fallback + function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); - /** - * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer - * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. - * - * @param elementType - * @param JSONcontainer - * @param DOMContainer - * @returns {*} - * @private - */ - exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { - var element; - // allocate DOM element, if it doesnt yet exist, create one. - if (JSONcontainer.hasOwnProperty(elementType)) { - // this element has been created before - // check if there is an redundant element - if (JSONcontainer[elementType].redundant.length > 0) { - element = JSONcontainer[elementType].redundant[0]; - JSONcontainer[elementType].redundant.shift(); - } else { - // create a new element and add it to the SVG - element = document.createElement(elementType); - if (insertBefore !== undefined) { - DOMContainer.insertBefore(element, insertBefore); - } else { - DOMContainer.appendChild(element); - } - } - } else { - // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. - element = document.createElement(elementType); - JSONcontainer[elementType] = { used: [], redundant: [] }; - if (insertBefore !== undefined) { - DOMContainer.insertBefore(element, insertBefore); - } else { - DOMContainer.appendChild(element); + if (matched !== null) { + config._d = new Date(+matched[1]); + return; + } + + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + utils_hooks__hooks.createFromInputFallback(config); + } } - } - JSONcontainer[elementType].used.push(element); - return element; - }; - /** - * Draw a point object. This is a separate function because it can also be called by the legend. - * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions - * as well. - * - * @param x - * @param y - * @param groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' } - * @param JSONcontainer - * @param svgContainer - * @param labelObj - * @returns {*} - */ - exports.drawPoint = function (x, y, groupTemplate, JSONcontainer, svgContainer, labelObj) { - var point; - if (groupTemplate.style == 'circle') { - point = exports.getSVGElement('circle', JSONcontainer, svgContainer); - point.setAttributeNS(null, 'cx', x); - point.setAttributeNS(null, 'cy', y); - point.setAttributeNS(null, 'r', 0.5 * groupTemplate.size); - } else { - point = exports.getSVGElement('rect', JSONcontainer, svgContainer); - point.setAttributeNS(null, 'x', x - 0.5 * groupTemplate.size); - point.setAttributeNS(null, 'y', y - 0.5 * groupTemplate.size); - point.setAttributeNS(null, 'width', groupTemplate.size); - point.setAttributeNS(null, 'height', groupTemplate.size); - } + utils_hooks__hooks.createFromInputFallback = deprecate( + 'moment construction falls back to js Date. This is ' + + 'discouraged and will be removed in upcoming major ' + + 'release. Please refer to ' + + 'https://github.com/moment/moment/issues/1407 for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } + ); - if (groupTemplate.style !== undefined) { - point.setAttributeNS(null, 'style', groupTemplate.style); - } - point.setAttributeNS(null, 'class', groupTemplate.className + ' vis-point'); - //handle label + function createDate (y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); - if (labelObj) { - var label = exports.getSVGElement('text', JSONcontainer, svgContainer); - if (labelObj.xOffset) { - x = x + labelObj.xOffset; + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; } - if (labelObj.yOffset) { - y = y + labelObj.yOffset; - } - if (labelObj.content) { - label.textContent = labelObj.content; + function createUTCDate (y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; } - if (labelObj.className) { - label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); - } - label.setAttributeNS(null, 'x', x); - label.setAttributeNS(null, 'y', y); - } + addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; + }); - return point; - }; + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); - /** - * draw a bar SVG element centered on the X coordinate - * - * @param x - * @param y - * @param className - */ - exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer, style) { - if (height != 0) { - if (height < 0) { - height *= -1; - y -= height; - } - var rect = exports.getSVGElement('rect', JSONcontainer, svgContainer); - rect.setAttributeNS(null, 'x', x - 0.5 * width); - rect.setAttributeNS(null, 'y', y); - rect.setAttributeNS(null, 'width', width); - rect.setAttributeNS(null, 'height', height); - rect.setAttributeNS(null, 'class', className); - if (style) { - rect.setAttributeNS(null, 'style', style); - } - } - }; + // ALIASES -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { + addUnitAlias('year', 'y'); - 'use strict'; + // PARSING - var util = __webpack_require__(9); - var Queue = __webpack_require__(16); + addRegexToken('Y', matchSigned); + addRegexToken('YY', match1to2, match2); + addRegexToken('YYYY', match1to4, match4); + addRegexToken('YYYYY', match1to6, match6); + addRegexToken('YYYYYY', match1to6, match6); - /** - * DataSet - * - * Usage: - * var dataSet = new DataSet({ - * fieldId: '_id', - * type: { - * // ... - * } - * }); - * - * dataSet.add(item); - * dataSet.add(data); - * dataSet.update(item); - * dataSet.update(data); - * dataSet.remove(id); - * dataSet.remove(ids); - * var data = dataSet.get(); - * var data = dataSet.get(id); - * var data = dataSet.get(ids); - * var data = dataSet.get(ids, options, data); - * dataSet.clear(); - * - * A data set can: - * - add/remove/update data - * - gives triggers upon changes in the data - * - can import/export data in various data formats - * - * @param {Array} [data] Optional array with initial data - * @param {Object} [options] Available options: - * {String} fieldId Field name of the id in the - * items, 'id' by default. - * {Object. 68 ? 1900 : 2000); + }; - this.setOptions(options); - } + // MOMENTS - /** - * @param {Object} [options] Available options: - * {Object} queue Queue changes to the DataSet, - * flush them all at once. - * Queue options: - * - {number} delay Delay in ms, null by default - * - {number} max Maximum number of entries in the queue, Infinity by default - * @param options - */ - DataSet.prototype.setOptions = function (options) { - if (options && options.queue !== undefined) { - if (options.queue === false) { - // delete queue if loaded - if (this._queue) { - this._queue.destroy(); - delete this._queue; - } - } else { - // create queue and update its options - if (!this._queue) { - this._queue = Queue.extend(this, { - replace: ['add', 'update', 'remove'] - }); - } + var getSetYear = makeGetSet('FullYear', false); - if (typeof options.queue === 'object') { - this._queue.setOptions(options.queue); - } + function getIsLeapYear () { + return isLeapYear(this.year()); } - } - }; - /** - * Subscribe to an event, add an event listener - * @param {String} event Event name. Available events: 'put', 'update', - * 'remove' - * @param {function} callback Callback method. Called with three parameters: - * {String} event - * {Object | null} params - * {String | Number} senderId - */ - DataSet.prototype.on = function (event, callback) { - var subscribers = this._subscribers[event]; - if (!subscribers) { - subscribers = []; - this._subscribers[event] = subscribers; - } + addFormatToken('w', ['ww', 2], 'wo', 'week'); + addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); - subscribers.push({ - callback: callback - }); - }; + // ALIASES - // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) - DataSet.prototype.subscribe = function () { - throw new Error('DataSet.subscribe is deprecated. Use DataSet.on instead.'); - }; + addUnitAlias('week', 'w'); + addUnitAlias('isoWeek', 'W'); - /** - * Unsubscribe from an event, remove an event listener - * @param {String} event - * @param {function} callback - */ - DataSet.prototype.off = function (event, callback) { - var subscribers = this._subscribers[event]; - if (subscribers) { - this._subscribers[event] = subscribers.filter(function (listener) { - return listener.callback != callback; + // PARSING + + addRegexToken('w', match1to2); + addRegexToken('ww', match1to2, match2); + addRegexToken('W', match1to2); + addRegexToken('WW', match1to2, match2); + + addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); }); - } - }; - // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) - DataSet.prototype.unsubscribe = function () { - throw new Error('DataSet.unsubscribe is deprecated. Use DataSet.off instead.'); - }; + // HELPERS - /** - * Trigger an event - * @param {String} event - * @param {Object | null} params - * @param {String} [senderId] Optional id of the sender. - * @private - */ - DataSet.prototype._trigger = function (event, params, senderId) { - if (event == '*') { - throw new Error('Cannot trigger event *'); - } + // firstDayOfWeek 0 = sun, 6 = sat + // the day of the week that starts the week + // (usually sunday or monday) + // firstDayOfWeekOfYear 0 = sun, 6 = sat + // the first week is the week that contains the first + // of this day of the week + // (eg. ISO weeks use thursday (4)) + function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { + var end = firstDayOfWeekOfYear - firstDayOfWeek, + daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), + adjustedMoment; - var subscribers = []; - if (event in this._subscribers) { - subscribers = subscribers.concat(this._subscribers[event]); - } - if ('*' in this._subscribers) { - subscribers = subscribers.concat(this._subscribers['*']); - } - for (var i = 0; i < subscribers.length; i++) { - var subscriber = subscribers[i]; - if (subscriber.callback) { - subscriber.callback(event, params, senderId || null); - } - } - }; + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } - /** - * Add data. - * Adding an item will fail when there already is an item with the same id. - * @param {Object | Array} data - * @param {String} [senderId] Optional sender id - * @return {Array} addedIds Array with the ids of the added items - */ - DataSet.prototype.add = function (data, senderId) { - var addedIds = [], - id, - me = this; + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } - if (Array.isArray(data)) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - id = me._addItem(data[i]); - addedIds.push(id); + adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; } - } else if (data instanceof Object) { - // Single item - id = me._addItem(data); - addedIds.push(id); - } else { - throw new Error('Unknown dataType'); - } - if (addedIds.length) { - this._trigger('add', { items: addedIds }, senderId); - } + // LOCALES - return addedIds; - }; + function localeWeek (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + } - /** - * Update existing items. When an item does not exist, it will be created - * @param {Object | Array} data - * @param {String} [senderId] Optional sender id - * @return {Array} updatedIds The ids of the added or updated items - */ - DataSet.prototype.update = function (data, senderId) { - var addedIds = []; - var updatedIds = []; - var updatedData = []; - var me = this; - var fieldId = me._fieldId; + var defaultLocaleWeek = { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }; - var addOrUpdate = function addOrUpdate(item) { - var id = item[fieldId]; - if (me._data[id]) { - // update item - id = me._updateItem(item); - updatedIds.push(id); - updatedData.push(item); - } else { - // add new item - id = me._addItem(item); - addedIds.push(id); + function localeFirstDayOfWeek () { + return this._week.dow; } - }; - if (Array.isArray(data)) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - addOrUpdate(data[i]); + function localeFirstDayOfYear () { + return this._week.doy; } - } else if (data instanceof Object) { - // Single item - addOrUpdate(data); - } else { - throw new Error('Unknown dataType'); - } - if (addedIds.length) { - this._trigger('add', { items: addedIds }, senderId); - } - if (updatedIds.length) { - this._trigger('update', { items: updatedIds, data: updatedData }, senderId); - } + // MOMENTS - return addedIds.concat(updatedIds); - }; + function getSetWeek (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + } - /** - * Get a data item or multiple items. - * - * Usage: - * - * get() - * get(options: Object) - * - * get(id: Number | String) - * get(id: Number | String, options: Object) - * - * get(ids: Number[] | String[]) - * get(ids: Number[] | String[], options: Object) - * - * Where: - * - * {Number | String} id The id of an item - * {Number[] | String{}} ids An array with ids of items - * {Object} options An Object with options. Available options: - * {String} [returnType] Type of data to be returned. - * Can be 'Array' (default) or 'Object'. - * {Object.} [type] - * {String[]} [fields] field names to be returned - * {function} [filter] filter items - * {String | function} [order] Order the items by a field name or custom sort function. - * @throws Error - */ - DataSet.prototype.get = function (args) { - var me = this; + function getSetISOWeek (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + } - // parse the arguments - var id, ids, options; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number') { - // get(id [, options]) - id = arguments[0]; - options = arguments[1]; - } else if (firstType == 'Array') { - // get(ids [, options]) - ids = arguments[0]; - options = arguments[1]; - } else { - // get([, options]) - options = arguments[0]; - } + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - // determine the return type - var returnType; - if (options && options.returnType) { - var allowedValues = ['Array', 'Object']; - returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType; - } else { - returnType = 'Array'; - } + // ALIASES - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], - item, - itemId, - i, - len; + addUnitAlias('dayOfYear', 'DDD'); - // convert items - if (id != undefined) { - // return a single item - item = me._getItem(id, type); - if (filter && !filter(item)) { - item = null; - } - } else if (ids != undefined) { - // return a subset of items - for (i = 0, len = ids.length; i < len; i++) { - item = me._getItem(ids[i], type); - if (!filter || filter(item)) { - items.push(item); - } - } - } else { - // return all items - for (itemId in this._data) { - if (this._data.hasOwnProperty(itemId)) { - item = me._getItem(itemId, type); - if (!filter || filter(item)) { - items.push(item); - } - } - } - } + // PARSING - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); - // filter fields of the items - if (options && options.fields) { - var fields = options.fields; - if (id != undefined) { - item = this._filterFields(item, fields); - } else { - for (i = 0, len = items.length; i < len; i++) { - items[i] = this._filterFields(items[i], fields); - } - } - } + // HELPERS - // return the results - if (returnType == 'Object') { - var result = {}; - for (i = 0; i < items.length; i++) { - result[items[i].id] = items[i]; - } - return result; - } else { - if (id != undefined) { - // a single item - return item; - } else { - // just return our array - return items; - } - } - }; + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { + var d = createUTCDate(year, 0, 1).getUTCDay(); + var daysToAdd; + var dayOfYear; - /** - * Get ids of all items or from a filtered set of items. - * @param {Object} [options] An Object with options. Available options: - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Array} ids - */ - DataSet.prototype.getIds = function (options) { - var data = this._data, - filter = options && options.filter, - order = options && options.order, - type = options && options.type || this._options.type, - i, - len, - id, - item, - items, - ids = []; + d = d === 0 ? 7 : d; + weekday = weekday != null ? weekday : firstDayOfWeek; + daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); + dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; - if (filter) { - // get filtered items - if (order) { - // create ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - items.push(item); - } - } - } + return { + year : dayOfYear > 0 ? year : year - 1, + dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } - this._sort(items, order); + // MOMENTS - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this._fieldId]; - } - } else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - ids.push(item[this._fieldId]); - } + function getSetDayOfYear (input) { + var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); + } + + // Pick the first defined of two or three arguments. + function defaults(a, b, c) { + if (a != null) { + return a; } - } + if (b != null) { + return b; + } + return c; } - } else { - // get all items - if (order) { - // create an ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - items.push(data[id]); + + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; } - } + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } - this._sort(items, order); + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function configFromArray (config) { + var i, date, input = [], currentDate, yearToUse; - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this._fieldId]; - } - } else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = data[id]; - ids.push(item[this._fieldId]); + if (config._d) { + return; } - } - } - } - return ids; - }; + currentDate = currentDateArray(config); - /** - * Returns the DataSet itself. Is overwritten for example by the DataView, - * which returns the DataSet it is connected to instead. - */ - DataSet.prototype.getDataSet = function () { - return this; - }; + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } - /** - * Execute a callback function for every item in the dataset. - * @param {function} callback - * @param {Object} [options] Available options: - * {Object.} [type] - * {String[]} [fields] filter fields - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - */ - DataSet.prototype.forEach = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - data = this._data, - item, - id; + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); - if (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); + if (config._dayOfYear > daysInYear(yearToUse)) { + getParsingFlags(config)._overflowDayOfYear = true; + } - for (var i = 0, len = items.length; i < len; i++) { - item = items[i]; - id = item[this._fieldId]; - callback(item, id); - } - } else { - // unordered - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - callback(item, id); + date = createUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); } - } - } - } - }; - /** - * Map every item in the dataset. - * @param {function} callback - * @param {Object} [options] Available options: - * {Object.} [type] - * {String[]} [fields] filter fields - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Object[]} mappedItems - */ - DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - mappedItems = [], - data = this._data, - item; + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } - // convert and filter items - for (var id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - mappedItems.push(callback(item, id)); - } - } - } + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + } - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } + // Check for 24:00:00.000 + if (config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0) { + config._nextDay = true; + config._a[HOUR] = 0; + } - return mappedItems; - }; + config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } - /** - * Filter the fields of an item - * @param {Object | null} item - * @param {String[]} fields Field names - * @return {Object | null} filteredItem or null if no item is provided - * @private - */ - DataSet.prototype._filterFields = function (item, fields) { - if (!item) { - // item is null - return item; - } + if (config._nextDay) { + config._a[HOUR] = 24; + } + } - var filteredItem = {}; + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; - if (Array.isArray(fields)) { - for (var field in item) { - if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { - filteredItem[field] = item[field]; - } - } - } else { - for (var field in item) { - if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { - filteredItem[fields[field]] = item[field]; - } - } - } + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - return filteredItem; - }; + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); + week = defaults(w.W, 1); + weekday = defaults(w.E, 1); + } else { + dow = config._locale._week.dow; + doy = config._locale._week.doy; - /** - * Sort the provided array with items - * @param {Object[]} items - * @param {String | function} order A field name or custom sort function. - * @private - */ - DataSet.prototype._sort = function (items, order) { - if (util.isString(order)) { - // order by provided field name - var name = order; // field name - items.sort(function (a, b) { - var av = a[name]; - var bv = b[name]; - return av > bv ? 1 : av < bv ? -1 : 0; - }); - } else if (typeof order === 'function') { - // order by sort function - items.sort(order); - } - // TODO: extend order by an Object {field:String, direction:String} - // where direction can be 'asc' or 'desc' - else { - throw new TypeError('Order must be a function or a string'); - } - }; + weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); + week = defaults(w.w, 1); - /** - * Remove an object by pointer or by id - * @param {String | Number | Object | Array} id Object or id, or an array with - * objects or ids to be removed - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds - */ - DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, - len, - removedId; + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < dow) { + ++week; + } + } else if (w.e != null) { + // local weekday -- counting starts from begining of week + weekday = w.e + dow; + } else { + // default to begining of week + weekday = dow; + } + } + temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); - if (Array.isArray(id)) { - for (i = 0, len = id.length; i < len; i++) { - removedId = this._remove(id[i]); - if (removedId != null) { - removedIds.push(removedId); - } - } - } else { - removedId = this._remove(id); - if (removedId != null) { - removedIds.push(removedId); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; } - } - if (removedIds.length) { - this._trigger('remove', { items: removedIds }, senderId); - } + utils_hooks__hooks.ISO_8601 = function () {}; - return removedIds; - }; + // date from string and format string + function configFromStringAndFormat(config) { + // TODO: Move this to another part of the creation flow to prevent circular deps + if (config._f === utils_hooks__hooks.ISO_8601) { + configFromISO(config); + return; + } - /** - * Remove an item by its id - * @param {Number | String | Object} id id or item - * @returns {Number | String | null} id - * @private - */ - DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this._data[id]) { - delete this._data[id]; - this.length--; - return id; - } - } else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - this.length--; - return itemId; - } - } - return null; - }; + config._a = []; + getParsingFlags(config).empty = true; - /** - * Clear the data - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds The ids of all removed items - */ - DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this._data); + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; - this._data = {}; - this.length = 0; + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - this._trigger('remove', { items: ids }, senderId); + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + getParsingFlags(config).unusedInput.push(skipped); + } + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + getParsingFlags(config).empty = false; + } + else { + getParsingFlags(config).unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + getParsingFlags(config).unusedTokens.push(token); + } + } - return ids; - }; + // add remaining unparsed input length to the string + getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + getParsingFlags(config).unusedInput.push(string); + } - /** - * Find the item with maximum value of a specified field - * @param {String} field - * @return {Object | null} item Item containing max value, or null if no items - */ - DataSet.prototype.max = function (field) { - var data = this._data, - max = null, - maxField = null; + // clear _12h flag if hour is <= 12 + if (getParsingFlags(config).bigHour === true && + config._a[HOUR] <= 12 && + config._a[HOUR] > 0) { + getParsingFlags(config).bigHour = undefined; + } + // handle meridiem + config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!max || itemField > maxField)) { - max = item; - maxField = itemField; - } + configFromArray(config); + checkOverflow(config); } - } - return max; - }; - /** - * Find the item with minimum value of a specified field - * @param {String} field - * @return {Object | null} item Item containing max value, or null if no items - */ - DataSet.prototype.min = function (field) { - var data = this._data, - min = null, - minField = null; + function meridiemFixWrap (locale, hour, meridiem) { + var isPm; - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!min || itemField < minField)) { - min = item; - minField = itemField; - } + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // this is not supposed to happen + return hour; + } } - } - return min; - }; + function configFromStringAndArray(config) { + var tempConfig, + bestMoment, - /** - * Find all distinct values of a specified field - * @param {String} field - * @return {Array} values Array containing all distinct values. If data items - * do not contain the specified field are ignored. - * The returned array is unordered. - */ - DataSet.prototype.distinct = function (field) { - var data = this._data; - var values = []; - var fieldType = this._options.type && this._options.type[field] || null; - var count = 0; - var i; + scoreToBeat, + i, + currentScore; - for (var prop in data) { - if (data.hasOwnProperty(prop)) { - var item = data[prop]; - var value = item[field]; - var exists = false; - for (i = 0; i < count; i++) { - if (values[i] == value) { - exists = true; - break; + if (config._f.length === 0) { + getParsingFlags(config).invalidFormat = true; + config._d = new Date(NaN); + return; } - } - if (!exists && value !== undefined) { - values[count] = value; - count++; - } - } - } - if (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); - } - } + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = copyConfig({}, config); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } + tempConfig._f = config._f[i]; + configFromStringAndFormat(tempConfig); - return values; - }; + if (!valid__isValid(tempConfig)) { + continue; + } - /** - * Add a single item. Will fail when an item with the same id already exists. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._addItem = function (item) { - var id = item[this._fieldId]; + // if there is any input that was not parsed add a penalty for that format + currentScore += getParsingFlags(tempConfig).charsLeftOver; - if (id != undefined) { - // check whether this id is already taken - if (this._data[id]) { - // item already exists - throw new Error('Cannot add item: item with id ' + id + ' already exists'); + //or tokens + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + + getParsingFlags(tempConfig).score = currentScore; + + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } + + extend(config, bestMoment || tempConfig); } - } else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; - } - var d = {}; - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); + function configFromObject(config) { + if (config._d) { + return; + } + + var i = normalizeObjectUnits(config._i); + config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; + + configFromArray(config); } - } - this._data[id] = d; - this.length++; - return id; - }; + function createFromConfig (config) { + var input = config._i, + format = config._f, + res; - /** - * Get an item. Fields can be converted to a specific type - * @param {String} id - * @param {Object.} [types] field types to convert - * @return {Object | null} item - * @private - */ - DataSet.prototype._getItem = function (id, types) { - var field, value; + config._locale = config._locale || locale_locales__getLocale(config._l); - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; - } + if (input === null || (format === undefined && input === '')) { + return valid__createInvalid({nullInput: true}); + } - // convert the items field types - var converted = {}; - if (types) { - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - converted[field] = util.convert(value, types[field]); - } - } - } else { - // no field types specified, no converting needed - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - converted[field] = value; - } - } - } - return converted; - }; + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } - /** - * Update a single item: merge with existing item. - * Will fail when the item has no id, or when there does not exist an item - * with the same id. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._updateItem = function (item) { - var id = item[this._fieldId]; - if (id == undefined) { - throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); - } - var d = this._data[id]; - if (!d) { - // item doesn't exist - throw new Error('Cannot update item: no item with id ' + id + ' found'); - } + if (isMoment(input)) { + return new Moment(checkOverflow(input)); + } else if (isArray(format)) { + configFromStringAndArray(config); + } else if (format) { + configFromStringAndFormat(config); + } else if (isDate(input)) { + config._d = input; + } else { + configFromInput(config); + } - // merge with current item - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } - } + res = new Moment(checkOverflow(config)); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } - return id; - }; + return res; + } - module.exports = DataSet; + function configFromInput(config) { + var input = config._i; + if (input === undefined) { + config._d = new Date(); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof input === 'string') { + configFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + configFromArray(config); + } else if (typeof(input) === 'object') { + configFromObject(config); + } else if (typeof(input) === 'number') { + // from milliseconds + config._d = new Date(input); + } else { + utils_hooks__hooks.createFromInputFallback(config); + } + } -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { + function createLocalOrUTC (input, format, locale, strict, isUTC) { + var c = {}; - /** - * A queue - * @param {Object} options - * Available options: - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @constructor - */ - 'use strict'; + if (typeof(locale) === 'boolean') { + strict = locale; + locale = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c._isAMomentObject = true; + c._useUTC = c._isUTC = isUTC; + c._l = locale; + c._i = input; + c._f = format; + c._strict = strict; - function Queue(options) { - // options - this.delay = null; - this.max = Infinity; + return createFromConfig(c); + } - // properties - this._queue = []; - this._timeout = null; - this._extended = null; + function local__createLocal (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); + } - this.setOptions(options); - } + var prototypeMin = deprecate( + 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + return other < this ? this : other; + } + ); - /** - * Update the configuration of the queue - * @param {Object} options - * Available options: - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @param options - */ - Queue.prototype.setOptions = function (options) { - if (options && typeof options.delay !== 'undefined') { - this.delay = options.delay; - } - if (options && typeof options.max !== 'undefined') { - this.max = options.max; - } + var prototypeMax = deprecate( + 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + return other > this ? this : other; + } + ); - this._flushIfNeeded(); - }; + // Pick a moment m from moments so that m[fn](other) is true for all + // other. This relies on the function fn to be transitive. + // + // moments should either be an array of moment objects or an array, whose + // first element is an array of moment objects. + function pickBy(fn, moments) { + var res, i; + if (moments.length === 1 && isArray(moments[0])) { + moments = moments[0]; + } + if (!moments.length) { + return local__createLocal(); + } + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (moments[i][fn](res)) { + res = moments[i]; + } + } + return res; + } - /** - * Extend an object with queuing functionality. - * The object will be extended with a function flush, and the methods provided - * in options.replace will be replaced with queued ones. - * @param {Object} object - * @param {Object} options - * Available options: - * - replace: Array. - * A list with method names of the methods - * on the object to be replaced with queued ones. - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @return {Queue} Returns the created queue - */ - Queue.extend = function (object, options) { - var queue = new Queue(options); + // TODO: Use [].sort instead? + function min () { + var args = [].slice.call(arguments, 0); - if (object.flush !== undefined) { - throw new Error('Target object already has a property flush'); - } - object.flush = function () { - queue.flush(); - }; + return pickBy('isBefore', args); + } - var methods = [{ - name: 'flush', - original: undefined - }]; + function max () { + var args = [].slice.call(arguments, 0); - if (options && options.replace) { - for (var i = 0; i < options.replace.length; i++) { - var name = options.replace[i]; - methods.push({ - name: name, - original: object[name] - }); - queue.replace(object, name); + return pickBy('isAfter', args); } - } - queue._extended = { - object: object, - methods: methods - }; + function Duration (duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; - return queue; - }; + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + quarters * 3 + + years * 12; - /** - * Destroy the queue. The queue will first flush all queued actions, and in - * case it has extended an object, will restore the original object. - */ - Queue.prototype.destroy = function () { - this.flush(); + this._data = {}; - if (this._extended) { - var object = this._extended.object; - var methods = this._extended.methods; - for (var i = 0; i < methods.length; i++) { - var method = methods[i]; - if (method.original) { - object[method.name] = method.original; - } else { - delete object[method.name]; - } + this._locale = locale_locales__getLocale(); + + this._bubble(); } - this._extended = null; - } - }; - /** - * Replace a method on an object with a queued version - * @param {Object} object Object having the method - * @param {string} method The method name - */ - Queue.prototype.replace = function (object, method) { - var me = this; - var original = object[method]; - if (!original) { - throw new Error('Method ' + method + ' undefined'); - } + function isDuration (obj) { + return obj instanceof Duration; + } - object[method] = function () { - // create an Array with the arguments - var args = []; - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; + function offset (token, separator) { + addFormatToken(token, 0, 0, function () { + var offset = this.utcOffset(); + var sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); + }); } - // add this call to the queue - me.queue({ - args: args, - fn: original, - context: this + offset('Z', ':'); + offset('ZZ', ''); + + // PARSING + + addRegexToken('Z', matchOffset); + addRegexToken('ZZ', matchOffset); + addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(input); }); - }; - }; - /** - * Queue a call - * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry - */ - Queue.prototype.queue = function (entry) { - if (typeof entry === 'function') { - this._queue.push({ fn: entry }); - } else { - this._queue.push(entry); - } + // HELPERS - this._flushIfNeeded(); - }; + // timezone chunker + // '+10:00' > ['10', '00'] + // '-1530' > ['-15', '30'] + var chunkOffset = /([\+\-]|\d\d)/gi; - /** - * Check whether the queue needs to be flushed - * @private - */ - Queue.prototype._flushIfNeeded = function () { - // flush when the maximum is exceeded. - if (this._queue.length > this.max) { - this.flush(); - } + function offsetFromString(string) { + var matches = ((string || '').match(matchOffset) || []); + var chunk = matches[matches.length - 1] || []; + var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + var minutes = +(parts[1] * 60) + toInt(parts[2]); - // flush after a period of inactivity when a delay is configured - clearTimeout(this._timeout); - if (this.queue.length > 0 && typeof this.delay === 'number') { - var me = this; - this._timeout = setTimeout(function () { - me.flush(); - }, this.delay); - } - }; + return parts[0] === '+' ? minutes : -minutes; + } - /** - * Flush all queued calls - */ - Queue.prototype.flush = function () { - while (this._queue.length > 0) { - var entry = this._queue.shift(); - entry.fn.apply(entry.context || entry.fn, entry.args || []); - } - }; + // Return a moment from input, that is local/utc/zone equivalent to model. + function cloneWithOffset(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); + // Use low-level api, because this fn is low-level api. + res._d.setTime(+res._d + diff); + utils_hooks__hooks.updateOffset(res, false); + return res; + } else { + return local__createLocal(input).local(); + } + return model._isUTC ? local__createLocal(input).zone(model._offset || 0) : local__createLocal(input).local(); + } - module.exports = Queue; + function getDateOffset (m) { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(m._d.getTimezoneOffset() / 15) * 15; + } -/***/ }, -/* 17 */ -/***/ function(module, exports, __webpack_require__) { + // HOOKS - 'use strict'; + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + utils_hooks__hooks.updateOffset = function () {}; - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); + // MOMENTS - /** - * DataView - * - * a dataview offers a filtered view on a dataset or an other dataview. - * - * @param {DataSet | DataView} data - * @param {Object} [options] Available options: see method get - * - * @constructor DataView - */ - function DataView(data, options) { - this._data = null; - this._ids = {}; // ids of the items currently in memory (just contains a boolean true) - this.length = 0; // number of items in the DataView - this._options = options || {}; - this._fieldId = 'id'; // name of the field containing id - this._subscribers = {}; // event subscribers + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + function getSetOffset (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + utils_hooks__hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } + } - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + function getSetZone (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } - this.setData(data); - } + this.utcOffset(input, keepLocalTime); - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly + return this; + } else { + return -this.utcOffset(); + } + } - /** - * Set a data source for the view - * @param {DataSet | DataView} data - */ - DataView.prototype.setData = function (data) { - var ids, i, len; + function setOffsetToUTC (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + } - if (this._data) { - // unsubscribe from current dataset - if (this._data.off) { - this._data.off('*', this.listener); + function setOffsetToLocal (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; } - // trigger a remove of all items in memory - ids = []; - for (var id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - ids.push(id); - } + function setOffsetToParsedOffset () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(offsetFromString(this._i)); + } + return this; } - this._ids = {}; - this.length = 0; - this._trigger('remove', { items: ids }); - } - this._data = data; + function hasAlignedHourOffset (input) { + if (!input) { + input = 0; + } + else { + input = local__createLocal(input).utcOffset(); + } - if (this._data) { - // update fieldId - this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; + return (this.utcOffset() - input) % 60 === 0; + } - // trigger an add of all added items - ids = this._data.getIds({ filter: this._options && this._options.filter }); - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - this._ids[id] = true; + function isDaylightSavingTime () { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); } - this.length = ids.length; - this._trigger('add', { items: ids }); - // subscribe to new dataset - if (this._data.on) { - this._data.on('*', this.listener); + function isDaylightSavingTimeShifted () { + if (this._a) { + var other = this._isUTC ? create_utc__createUTC(this._a) : local__createLocal(this._a); + return this.isValid() && compareArrays(this._a, other.toArray()) > 0; + } + + return false; } - } - }; - /** - * Refresh the DataView. Useful when the DataView has a filter function - * containing a variable parameter. - */ - DataView.prototype.refresh = function () { - var id; - var ids = this._data.getIds({ filter: this._options && this._options.filter }); - var newIds = {}; - var added = []; - var removed = []; + function isLocal () { + return !this._isUTC; + } - // check for additions - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - newIds[id] = true; - if (!this._ids[id]) { - added.push(id); - this._ids[id] = true; - this.length++; + function isUtcOffset () { + return this._isUTC; } - } - // check for removals - for (id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - if (!newIds[id]) { - removed.push(id); - delete this._ids[id]; - this.length--; - } + function isUtc () { + return this._isUTC && this._offset === 0; } - } - // trigger events - if (added.length) { - this._trigger('add', { items: added }); - } - if (removed.length) { - this._trigger('remove', { items: removed }); - } - }; + var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; - /** - * Get data from the data view - * - * Usage: - * - * get() - * get(options: Object) - * get(options: Object, data: Array | DataTable) - * - * get(id: Number) - * get(id: Number, options: Object) - * get(id: Number, options: Object, data: Array | DataTable) - * - * get(ids: Number[]) - * get(ids: Number[], options: Object) - * get(ids: Number[], options: Object, data: Array | DataTable) - * - * Where: - * - * {Number | String} id The id of an item - * {Number[] | String{}} ids An array with ids of items - * {Object} options An Object with options. Available options: - * {String} [type] Type of data to be returned. Can - * be 'DataTable' or 'Array' (default) - * {Object.} [convert] - * {String[]} [fields] field names to be returned - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * {Array | DataTable} [data] If provided, items will be appended to this - * array or table. Required in case of Google - * DataTable. - * @param args - */ - DataView.prototype.get = function (args) { - var me = this; + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; - // parse the arguments - var ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { - // get(id(s) [, options] [, data]) - ids = arguments[0]; // can be a single id or an array with ids - options = arguments[1]; - data = arguments[2]; - } else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; - } + function create__createDuration (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + diffRes; - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); + if (isDuration(input)) { + duration = { + ms : input._milliseconds, + d : input._days, + M : input._months + }; + } else if (typeof input === 'number') { + duration = {}; + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } else if (!!(match = aspNetRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : 1; + duration = { + y : 0, + d : toInt(match[DATE]) * sign, + h : toInt(match[HOUR]) * sign, + m : toInt(match[MINUTE]) * sign, + s : toInt(match[SECOND]) * sign, + ms : toInt(match[MILLISECOND]) * sign + }; + } else if (!!(match = create__isoRegex.exec(input))) { + sign = (match[1] === '-') ? -1 : 1; + duration = { + y : parseIso(match[2], sign), + M : parseIso(match[3], sign), + d : parseIso(match[4], sign), + h : parseIso(match[5], sign), + m : parseIso(match[6], sign), + s : parseIso(match[7], sign), + w : parseIso(match[8], sign) + }; + } else if (duration == null) {// checks for null or undefined + duration = {}; + } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { + diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); - // create a combined filter method when needed - if (this._options.filter && options && options.filter) { - viewOptions.filter = function (item) { - return me._options.filter(item) && options.filter(item); - }; - } + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); + ret = new Duration(duration); - return this._data && this._data.get.apply(this._data, getArguments); - }; + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } - /** - * Get ids of all items or from a filtered set of items. - * @param {Object} [options] An Object with options. Available options: - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Array} ids - */ - DataView.prototype.getIds = function (options) { - var ids; + return ret; + } - if (this._data) { - var defaultFilter = this._options.filter; - var filter; + create__createDuration.fn = Duration.prototype; - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); - }; - } else { - filter = options.filter; - } - } else { - filter = defaultFilter; + function parseIso (inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; } - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); - } else { - ids = []; - } + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - return ids; - }; + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } - /** - * Get the DataSet to which this DataView is connected. In case there is a chain - * of multiple DataViews, the root DataSet of this chain is returned. - * @return {DataSet} dataSet - */ - DataView.prototype.getDataSet = function () { - var dataSet = this; - while (dataSet instanceof DataView) { - dataSet = dataSet._data; - } - return dataSet || null; - }; + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - /** - * Event listener. Will propagate all events from the connected data set to - * the subscribers of the DataView, but will filter the items and only trigger - * when there are changes in the filtered data set. - * @param {String} event - * @param {Object | null} params - * @param {String} senderId - * @private - */ - DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item; - var ids = params && params.items; - var data = this._data; - var updatedData = []; - var added = []; - var updated = []; - var removed = []; + return res; + } - if (ids && data) { - switch (event) { - case 'add': - // filter the ids of the added items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - if (item) { - this._ids[id] = true; - added.push(id); - } + function momentsDifference(base, other) { + var res; + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; } - break; - - case 'update': - // determine the event from the views viewpoint: an updated - // item can be added, updated, or removed from this view. - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); + return res; + } - if (item) { - if (this._ids[id]) { - updated.push(id); - updatedData.push(params.data[i]); - } else { - this._ids[id] = true; - added.push(id); + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); + tmp = val; val = period; period = tmp; } - } else { - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } else {} - } - } - break; + val = typeof val === 'string' ? +val : val; + dur = create__createDuration(val, period); + add_subtract__addSubtract(this, dur, direction); + return this; + }; + } - case 'remove': - // filter the ids of the removed items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } - } + function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; - break; + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + if (days) { + get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); + } + if (months) { + setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); + } + if (updateOffset) { + utils_hooks__hooks.updateOffset(mom, days || months); + } } - this.length += added.length - removed.length; + var add_subtract__add = createAdder(1, 'add'); + var add_subtract__subtract = createAdder(-1, 'subtract'); - if (added.length) { - this._trigger('add', { items: added }, senderId); + function moment_calendar__calendar (time) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || local__createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + diff = this.diff(sod, 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + return this.format(this.localeData().calendar(format, this, local__createLocal(now))); } - if (updated.length) { - this._trigger('update', { items: updated, data: updatedData }, senderId); + + function clone () { + return new Moment(this); } - if (removed.length) { - this._trigger('remove', { items: removed }, senderId); + + function isAfter (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = isMoment(input) ? input : local__createLocal(input); + return +this > +input; + } else { + inputMs = isMoment(input) ? +input : +local__createLocal(input); + return inputMs < +this.clone().startOf(units); + } } - } - }; - // copy subscription functionality from DataSet - DataView.prototype.on = DataSet.prototype.on; - DataView.prototype.off = DataSet.prototype.off; - DataView.prototype._trigger = DataSet.prototype._trigger; + function isBefore (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = isMoment(input) ? input : local__createLocal(input); + return +this < +input; + } else { + inputMs = isMoment(input) ? +input : +local__createLocal(input); + return +this.clone().endOf(units) < inputMs; + } + } - // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) - DataView.prototype.subscribe = DataView.prototype.on; - DataView.prototype.unsubscribe = DataView.prototype.off; + function isBetween (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + } - module.exports = DataView; + function isSame (input, units) { + var inputMs; + units = normalizeUnits(units || 'millisecond'); + if (units === 'millisecond') { + input = isMoment(input) ? input : local__createLocal(input); + return +this === +input; + } else { + inputMs = +local__createLocal(input); + return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + } + } - // nothing interesting for me :-( + function absFloor (number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } -/***/ }, -/* 18 */ -/***/ function(module, exports, __webpack_require__) { + function diff (input, units, asFloat) { + var that = cloneWithOffset(input, this), + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, + delta, output; - 'use strict'; + units = normalizeUnits(units); - var Emitter = __webpack_require__(20); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - var util = __webpack_require__(9); - var Point3d = __webpack_require__(21); - var Point2d = __webpack_require__(19); - var Camera = __webpack_require__(22); - var Filter = __webpack_require__(23); - var Slider = __webpack_require__(24); - var StepNumber = __webpack_require__(25); + if (units === 'year' || units === 'month' || units === 'quarter') { + output = monthDiff(this, that); + if (units === 'quarter') { + output = output / 3; + } else if (units === 'year') { + output = output / 12; + } + } else { + delta = this - that; + output = units === 'second' ? delta / 1e3 : // 1000 + units === 'minute' ? delta / 6e4 : // 1000 * 60 + units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + delta; + } + return asFloat ? output : absFloor(output); + } - /** - * @constructor Graph3d - * Graph3d displays data in 3d. - * - * Graph3d is developed in javascript as a Google Visualization Chart. - * - * @param {Element} container The DOM element in which the Graph3d will - * be created. Normally a div element. - * @param {DataSet | DataView | Array} [data] - * @param {Object} [options] - */ - function Graph3d(container, data, options) { - if (!(this instanceof Graph3d)) { - throw new SyntaxError('Constructor must be called with the new operator'); - } + function monthDiff (a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; - // create variables and set default values - this.containerElement = container; - this.width = '400px'; - this.height = '400px'; - this.margin = 10; // px - this.defaultXCenter = '55%'; - this.defaultYCenter = '50%'; + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } - this.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; + return -(wholeMonthDiff + adjust); + } - var passValueFn = function passValueFn(v) { - return v; - }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; + utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; - this.filterLabel = 'time'; - this.legendLabel = 'value'; + function toString () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + } - this.style = Graph3d.STYLE.DOT; - this.showPerspective = true; - this.showGrid = true; - this.keepAspectRatio = true; - this.showShadow = false; - this.showGrayBottom = false; // TODO: this does not work correctly - this.showTooltip = false; - this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' + function moment_format__toISOString () { + var m = this.clone().utc(); + if (0 < m.year() && m.year() <= 9999) { + if ('function' === typeof Date.prototype.toISOString) { + // native implementation is ~50x faster, use it when we can + return this.toDate().toISOString(); + } else { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + function format (inputString) { + var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); + return this.localeData().postformat(output); + } - this.camera = new Camera(); - this.camera.setArmRotation(1, 0.5); - this.camera.setArmLength(1.7); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + function from (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + } - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + function fromNow (withoutSuffix) { + return this.from(local__createLocal(), withoutSuffix); + } - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; + function to (time, withoutSuffix) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); + } - this.xMin = 0; - this.xStep = undefined; // auto by default - this.xMax = 1; - this.yMin = 0; - this.yStep = undefined; // auto by default - this.yMax = 1; - this.zMin = 0; - this.zStep = undefined; // auto by default - this.zMax = 1; - this.valueMin = 0; - this.valueMax = 1; - this.xBarWidth = 1; - this.yBarWidth = 1; - // TODO: customize axis range + function toNow (withoutSuffix) { + return this.to(local__createLocal(), withoutSuffix); + } - // colors - this.axisColor = '#4D4D4D'; - this.gridColor = '#D3D3D3'; - this.dataColor = { - fill: '#7DC1FF', - stroke: '#3267D2', - strokeWidth: 1 // px - }; + function locale (key) { + var newLocaleData; - // create a frame and canvas - this.create(); + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = locale_locales__getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + } - // apply options (also when undefined) - this.setOptions(options); + var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ); - // apply data - if (data) { - this.setData(data); - } - } + function localeData () { + return this._locale; + } + + function startOf (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + } - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } + if (units === 'isoWeek') { + this.isoWeekday(1); + } - /** - * Calculate the scaling values, dependent on the range in x, y, and z direction - */ - Graph3d.prototype._setScale = function () { - this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } - // keep aspect ration between x and y scale if desired - if (this.keepAspectRatio) { - if (this.scale.x < this.scale.y) { - //noinspection JSSuspiciousNameCombination - this.scale.y = this.scale.x; - } else { - //noinspection JSSuspiciousNameCombination - this.scale.x = this.scale.y; + return this; } - } - // scale the vertical axis - this.scale.z *= this.verticalRatio; - // TODO: can this be automated? verticalRatio? + function endOf (units) { + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond') { + return this; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + } - // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); + function to_type__valueOf () { + return +this._d - ((this._offset || 0) * 60000); + } - // position the camera arm - var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; - var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; - var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; - this.camera.setArmLocation(xCenter, yCenter, zCenter); - }; + function unix () { + return Math.floor(+this / 1000); + } - /** - * Convert a 3D location to a 2D location on screen - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point2d} point2d A 2D point with parameters x, y - */ - Graph3d.prototype._convert3Dto2D = function (point3d) { - var translation = this._convertPointToTranslation(point3d); - return this._convertTranslationToScreen(translation); - }; + function toDate () { + return this._offset ? new Date(+this) : this._d; + } - /** - * Convert a 3D location its translation seen from the camera - * http://en.wikipedia.org/wiki/3D_projection - * @param {Point3d} point3d A 3D point with parameters x, y, z - * @return {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera - */ - Graph3d.prototype._convertPointToTranslation = function (point3d) { - var ax = point3d.x * this.scale.x, - ay = point3d.y * this.scale.y, - az = point3d.z * this.scale.z, - cx = this.camera.getCameraLocation().x, - cy = this.camera.getCameraLocation().y, - cz = this.camera.getCameraLocation().z, + function toArray () { + var m = this; + return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; + } - // calculate angles - sinTx = Math.sin(this.camera.getCameraRotation().x), - cosTx = Math.cos(this.camera.getCameraRotation().x), - sinTy = Math.sin(this.camera.getCameraRotation().y), - cosTy = Math.cos(this.camera.getCameraRotation().y), - sinTz = Math.sin(this.camera.getCameraRotation().z), - cosTz = Math.cos(this.camera.getCameraRotation().z), + function moment_valid__isValid () { + return valid__isValid(this); + } - // calculate translation - dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), - dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), - dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); + function parsingFlags () { + return extend({}, getParsingFlags(this)); + } - return new Point3d(dx, dy, dz); - }; + function invalidAt () { + return getParsingFlags(this).overflow; + } - /** - * Convert a translation point to a point on the screen - * @param {Point3d} translation A 3D point with parameters x, y, z This is - * the translation of the point, seen from the - * camera - * @return {Point2d} point2d A 2D point with parameters x, y - */ - Graph3d.prototype._convertTranslationToScreen = function (translation) { - var ex = this.eye.x, - ey = this.eye.y, - ez = this.eye.z, - dx = translation.x, - dy = translation.y, - dz = translation.z; + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); - // calculate position on screen from translation - var bx; - var by; - if (this.showPerspective) { - bx = (dx - ex) * (ez / dz); - by = (dy - ey) * (ez / dz); - } else { - bx = dx * -(ez / this.camera.getArmLength()); - by = dy * -(ez / this.camera.getArmLength()); - } + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); - // shift and scale the point to the center of the screen - // use the width of the graph to scale both horizontally and vertically. - return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); - }; + function addWeekYearFormatToken (token, getter) { + addFormatToken(0, [token, token.length], 0, getter); + } - /** - * Set the background styling for the graph - * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor - */ - Graph3d.prototype._setBackgroundColor = function (backgroundColor) { - var fill = 'white'; - var stroke = 'gray'; - var strokeWidth = 1; + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); - if (typeof backgroundColor === 'string') { - fill = backgroundColor; - stroke = 'none'; - strokeWidth = 0; - } else if (typeof backgroundColor === 'object') { - if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; - if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; - if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; - } else if (backgroundColor === undefined) {} else { - throw 'Unsupported type of backgroundColor'; - } + // ALIASES - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + 'px'; - this.frame.style.borderStyle = 'solid'; - }; + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); - /// enumerate the available styles - Graph3d.STYLE = { - BAR: 0, - BARCOLOR: 1, - BARSIZE: 2, - DOT: 3, - DOTLINE: 4, - DOTCOLOR: 5, - DOTSIZE: 6, - GRID: 7, - LINE: 8, - SURFACE: 9 - }; + // PARSING - /** - * Retrieve the style index from given styleName - * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' - * @return {Number} styleNumber Enumeration value representing the style, or -1 - * when not found - */ - Graph3d.prototype._getStyleNumber = function (styleName) { - switch (styleName) { - case 'dot': - return Graph3d.STYLE.DOT; - case 'dot-line': - return Graph3d.STYLE.DOTLINE; - case 'dot-color': - return Graph3d.STYLE.DOTCOLOR; - case 'dot-size': - return Graph3d.STYLE.DOTSIZE; - case 'line': - return Graph3d.STYLE.LINE; - case 'grid': - return Graph3d.STYLE.GRID; - case 'surface': - return Graph3d.STYLE.SURFACE; - case 'bar': - return Graph3d.STYLE.BAR; - case 'bar-color': - return Graph3d.STYLE.BARCOLOR; - case 'bar-size': - return Graph3d.STYLE.BARSIZE; - } + addRegexToken('G', matchSigned); + addRegexToken('g', matchSigned); + addRegexToken('GG', match1to2, match2); + addRegexToken('gg', match1to2, match2); + addRegexToken('GGGG', match1to4, match4); + addRegexToken('gggg', match1to4, match4); + addRegexToken('GGGGG', match1to6, match6); + addRegexToken('ggggg', match1to6, match6); - return -1; - }; + addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); + }); - /** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style - */ - Graph3d.prototype._determineColumnIndexes = function (data, style) { - if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) { - // 3 columns expected, and optionally a 4th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = undefined; + addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = utils_hooks__hooks.parseTwoDigitYear(input); + }); - if (data.getNumberOfColumns() > 3) { - this.colFilter = 3; - } - } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - // 4 columns expected, and optionally a 5th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = 3; + // HELPERS - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; + function weeksInYear(year, dow, doy) { + return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; } - } else { - throw 'Unknown style "' + this.style + '"'; - } - }; - Graph3d.prototype.getNumberOfRows = function (data) { - return data.length; - }; + // MOMENTS - Graph3d.prototype.getNumberOfColumns = function (data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; + function getSetWeekYear (input) { + var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; + return input == null ? year : this.add((input - year), 'y'); } - } - return counter; - }; - Graph3d.prototype.getDistinctValues = function (data, column) { - var distinctValues = []; - for (var i = 0; i < data.length; i++) { - if (distinctValues.indexOf(data[i][column]) == -1) { - distinctValues.push(data[i][column]); + function getSetISOWeekYear (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add((input - year), 'y'); } - } - return distinctValues; - }; - Graph3d.prototype.getColumnRange = function (data, column) { - var minMax = { min: data[0][column], max: data[0][column] }; - for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { - minMax.min = data[i][column]; + function getISOWeeksInYear () { + return weeksInYear(this.year(), 1, 4); } - if (minMax.max < data[i][column]) { - minMax.max = data[i][column]; + + function getWeeksInYear () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); } - } - return minMax; - }; - /** - * Initialize the data from the data table. Calculate minimum and maximum values - * and column index values - * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. - * @param {Number} style Style Number - */ - Graph3d.prototype._dataInitialize = function (rawData, style) { - var me = this; + addFormatToken('Q', 0, 0, 'quarter'); - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off('*', this._onChange); - } + // ALIASES - if (rawData === undefined) return; + addUnitAlias('quarter', 'Q'); - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); - } + // PARSING - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } else { - throw new Error('Array, DataSet, or DataView expected'); - } + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); - if (data.length == 0) return; + // MOMENTS - this.dataSet = rawData; - this.dataTable = data; + function getSetQuarter (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + } - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); - }; - this.dataSet.on('*', this._onChange); + addFormatToken('D', ['DD', 2], 'Do', 'date'); - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange + // ALIASES - // determine the location of x,y,z,value,filter columns - this.colX = 'x'; - this.colY = 'y'; - this.colZ = 'z'; - this.colValue = 'style'; - this.colFilter = 'filter'; + addUnitAlias('date', 'D'); - // check if a filter column is provided - if (data[0].hasOwnProperty('filter')) { - if (this.dataFilter === undefined) { - this.dataFilter = new Filter(rawData, this.colFilter, this); - this.dataFilter.setOnLoadCallback(function () { - me.redraw(); - }); - } - } + // PARSING - var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + }); - // determine barWidth from data - if (withBars) { - if (this.defaultXBarWidth !== undefined) { - this.xBarWidth = this.defaultXBarWidth; - } else { - var dataX = this.getDistinctValues(data, this.colX); - this.xBarWidth = dataX[1] - dataX[0] || 1; - } + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0], 10); + }); - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; - } else { - var dataY = this.getDistinctValues(data, this.colY); - this.yBarWidth = dataY[1] - dataY[0] || 1; - } - } + // MOMENTS - // calculate minimums and maximums - var xRange = this.getColumnRange(data, this.colX); - if (withBars) { - xRange.min -= this.xBarWidth / 2; - xRange.max += this.xBarWidth / 2; - } - this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min; - this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max; - if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; - this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5; + var getSetDayOfMonth = makeGetSet('Date', true); - var yRange = this.getColumnRange(data, this.colY); - if (withBars) { - yRange.min -= this.yBarWidth / 2; - yRange.max += this.yBarWidth / 2; - } - this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min; - this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max; - if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; - this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5; + addFormatToken('d', 0, 'do', 'day'); - var zRange = this.getColumnRange(data, this.colZ); - this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; - this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; - if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; - this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); - if (this.colValue !== undefined) { - var valueRange = this.getColumnRange(data, this.colValue); - this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; - this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; - if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; - } + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); - // set the scale dependent on the ranges. - this._setScale(); - }; + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); - /** - * Filter the data based on the current filter - * @param {Array} data - * @return {Array} dataPoints Array with point objects which can be drawn on screen - */ - Graph3d.prototype._getDataPoints = function (data) { - // TODO: store the created matrix dataPoints in the filters instead of reloading each time - var x, y, i, z, obj, point; + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); - var dataPoints = []; + // ALIASES - if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { - // copy all values from the google data table to a matrix - // the provided values are supposed to form a grid of (x,y) positions + addUnitAlias('day', 'd'); + addUnitAlias('weekday', 'e'); + addUnitAlias('isoWeekday', 'E'); - // create two lists with all present x and y values - var dataX = []; - var dataY = []; - for (i = 0; i < this.getNumberOfRows(data); i++) { - x = data[i][this.colX] || 0; - y = data[i][this.colY] || 0; + // PARSING - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); - } + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', matchWord); + addRegexToken('ddd', matchWord); + addRegexToken('dddd', matchWord); + + addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { + var weekday = config._locale.weekdaysParse(input); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + getParsingFlags(config).invalidWeekday = input; + } + }); + + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); + + // HELPERS + + function parseWeekday(input, locale) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = locale.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; } - var sortNumber = function sortNumber(a, b) { - return a - b; - }; - dataX.sort(sortNumber); - dataY.sort(sortNumber); - - // create a grid, a 2d matrix, with all values. - var dataMatrix = []; // temporary data matrix - for (i = 0; i < data.length; i++) { - x = data[i][this.colX] || 0; - y = data[i][this.colY] || 0; - z = data[i][this.colZ] || 0; + // LOCALES - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); + function localeWeekdays (m) { + return this._weekdays[m.day()]; + } - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } + var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); + function localeWeekdaysShort (m) { + return this._weekdaysShort[m.day()]; + } - var point3d = new Point3d(); - point3d.x = x; - point3d.y = y; - point3d.z = z; + var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + function localeWeekdaysMin (m) { + return this._weekdaysMin[m.day()]; + } - obj = {}; - obj.point = point3d; - obj.trans = undefined; - obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + function localeWeekdaysParse (weekdayName) { + var i, mom, regex; - dataMatrix[xIndex][yIndex] = obj; + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } - dataPoints.push(obj); + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + if (!this._weekdaysParse[i]) { + mom = local__createLocal([2000, 1]).day(i); + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } } - // fill in the pointers to the neighbors. - for (x = 0; x < dataMatrix.length; x++) { - for (y = 0; y < dataMatrix[x].length; y++) { - if (dataMatrix[x][y]) { - dataMatrix[x][y].pointRight = x < dataMatrix.length - 1 ? dataMatrix[x + 1][y] : undefined; - dataMatrix[x][y].pointTop = y < dataMatrix[x].length - 1 ? dataMatrix[x][y + 1] : undefined; - dataMatrix[x][y].pointCross = x < dataMatrix.length - 1 && y < dataMatrix[x].length - 1 ? dataMatrix[x + 1][y + 1] : undefined; + // MOMENTS + + function getSetDayOfWeek (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; } - } } - } else { - // 'dot', 'dot-line', etc. - // copy all values from the google data table to a list with Point3d objects - for (i = 0; i < data.length; i++) { - point = new Point3d(); - point.x = data[i][this.colX] || 0; - point.y = data[i][this.colY] || 0; - point.z = data[i][this.colZ] || 0; - - if (this.colValue !== undefined) { - point.value = data[i][this.colValue] || 0; - } - obj = {}; - obj.point = point; - obj.bottom = new Point3d(point.x, point.y, this.zMin); - obj.trans = undefined; - obj.screen = undefined; + function getSetLocaleDayOfWeek (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } - dataPoints.push(obj); + function getSetISODayOfWeek (input) { + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); } - } - return dataPoints; - }; + addFormatToken('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, function () { + return this.hours() % 12 || 12; + }); - /** - * Create the main frame for the Graph3d. - * This function is executed once when a Graph3d object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. - */ - Graph3d.prototype.create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } + function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); + } - this.frame = document.createElement('div'); - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + meridiem('a', true); + meridiem('A', false); - // create the graph canvas (HTML canvas element) - this.frame.canvas = document.createElement('canvas'); - this.frame.canvas.style.position = 'relative'; - this.frame.appendChild(this.frame.canvas); - //if (!this.frame.canvas.getContext) { - { - var noCanvas = document.createElement('DIV'); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold'; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.frame.canvas.appendChild(noCanvas); - } + // ALIASES - this.frame.filter = document.createElement('div'); - this.frame.filter.style.position = 'absolute'; - this.frame.filter.style.bottom = '0px'; - this.frame.filter.style.left = '0px'; - this.frame.filter.style.width = '100%'; - this.frame.appendChild(this.frame.filter); + addUnitAlias('hour', 'h'); - // add event listeners to handle moving and zooming the contents - var me = this; - var onmousedown = function onmousedown(event) { - me._onMouseDown(event); - }; - var ontouchstart = function ontouchstart(event) { - me._onTouchStart(event); - }; - var onmousewheel = function onmousewheel(event) { - me._onWheel(event); - }; - var ontooltip = function ontooltip(event) { - me._onTooltip(event); - }; - // TODO: these events are never cleaned up... can give a 'memory leakage' + // PARSING - util.addEventListener(this.frame.canvas, 'keydown', onkeydown); - util.addEventListener(this.frame.canvas, 'mousedown', onmousedown); - util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart); - util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel); - util.addEventListener(this.frame.canvas, 'mousemove', ontooltip); + function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; + } - // add the new graph to the container element - this.containerElement.appendChild(this.frame); - }; + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); - /** - * Set a new size for the graph - * @param {string} width Width in pixels or percentage (for example '800px' - * or '50%') - * @param {string} height Height in pixels or percentage (for example '400px' - * or '30%') - */ - Graph3d.prototype.setSize = function (width, height) { - this.frame.style.width = width; - this.frame.style.height = height; + addParseToken(['H', 'HH'], HOUR); + addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; + }); + addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + getParsingFlags(config).bigHour = true; + }); - this._resizeCanvas(); - }; + // LOCALES - /** - * Resize the canvas to the current size of the frame - */ - Graph3d.prototype._resizeCanvas = function () { - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + function localeIsPM (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + } - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; + var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; + function localeMeridiem (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + } - // adjust with for margin - this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; - }; - /** - * Start animation - */ - Graph3d.prototype.animationStart = function () { - if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; + // MOMENTS - this.frame.filter.slider.play(); - }; + // Setting the hour should keep the time, because the user explicitly + // specified which hour he wants. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + var getSetHour = makeGetSet('Hours', true); - /** - * Stop animation - */ - Graph3d.prototype.animationStop = function () { - if (!this.frame.filter || !this.frame.filter.slider) return; + addFormatToken('m', ['mm', 2], 0, 'minute'); - this.frame.filter.slider.stop(); - }; + // ALIASES - /** - * Resize the center position based on the current values in this.defaultXCenter - * and this.defaultYCenter (which are strings with a percentage or a value - * in pixels). The center positions are the variables this.xCenter - * and this.yCenter - */ - Graph3d.prototype._resizeCenter = function () { - // calculate the horizontal center position - if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === '%') { - this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; - } else { - this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px - } + addUnitAlias('minute', 'm'); - // calculate the vertical center position - if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === '%') { - this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); - } else { - this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px - } - }; + // PARSING - /** - * Set the rotation and distance of the camera - * @param {Object} pos An object with the camera position. The object - * contains three parameters: - * - horizontal {Number} - * The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * - vertical {Number} - * The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. - * - distance {Number} - * The (normalized) distance of the camera to the - * center of the graph, a value between 0.71 and 5.0. - * Optional, can be left undefined. - */ - Graph3d.prototype.setCameraPosition = function (pos) { - if (pos === undefined) { - return; - } + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } + // MOMENTS - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } + var getSetMinute = makeGetSet('Minutes', false); - this.redraw(); - }; + addFormatToken('s', ['ss', 2], 0, 'second'); - /** - * Retrieve the current camera rotation - * @return {object} An object with parameters horizontal, vertical, and - * distance - */ - Graph3d.prototype.getCameraPosition = function () { - var pos = this.camera.getArmRotation(); - pos.distance = this.camera.getArmLength(); - return pos; - }; + // ALIASES - /** - * Load data into the 3D Graph - */ - Graph3d.prototype._readData = function (data) { - // read the data - this._dataInitialize(data, this.style); + addUnitAlias('second', 's'); - if (this.dataFilter) { - // apply filtering - this.dataPoints = this.dataFilter._getDataPoints(); - } else { - // no filtering. load all data - this.dataPoints = this._getDataPoints(this.dataTable); - } + // PARSING - // draw the filter - this._redrawFilter(); - }; + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); - /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data - */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); + // MOMENTS - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + var getSetSecond = makeGetSet('Seconds', false); - /** - * Update the options. Options will be merged with current options - * @param {Object} options - */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); + }); - this.animationStop(); + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + function millisecond__milliseconds (token) { + addFormatToken(0, [token, 3], 0, 'millisecond'); + } - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + millisecond__milliseconds('SSS'); + millisecond__milliseconds('SSSS'); - if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; - if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; - if (options.xLabel !== undefined) this.xLabel = options.xLabel; - if (options.yLabel !== undefined) this.yLabel = options.yLabel; - if (options.zLabel !== undefined) this.zLabel = options.zLabel; + // ALIASES - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; + addUnitAlias('millisecond', 'ms'); - if (options.style !== undefined) { - var styleNumber = this._getStyleNumber(options.style); - if (styleNumber !== -1) { - this.style = styleNumber; - } - } - if (options.showGrid !== undefined) this.showGrid = options.showGrid; - if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; - if (options.showShadow !== undefined) this.showShadow = options.showShadow; - if (options.tooltip !== undefined) this.showTooltip = options.tooltip; - if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; - if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; - if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; + // PARSING - if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); + addRegexToken('SSSS', matchUnsigned); + addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); + }); - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; + // MOMENTS - if (options.xMin !== undefined) this.defaultXMin = options.xMin; - if (options.xStep !== undefined) this.defaultXStep = options.xStep; - if (options.xMax !== undefined) this.defaultXMax = options.xMax; - if (options.yMin !== undefined) this.defaultYMin = options.yMin; - if (options.yStep !== undefined) this.defaultYStep = options.yStep; - if (options.yMax !== undefined) this.defaultYMax = options.yMax; - if (options.zMin !== undefined) this.defaultZMin = options.zMin; - if (options.zStep !== undefined) this.defaultZStep = options.zStep; - if (options.zMax !== undefined) this.defaultZMax = options.zMax; - if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; - if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; + var getSetMillisecond = makeGetSet('Milliseconds', false); - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); + // MOMENTS + + function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; } - // colors - if (options.axisColor !== undefined) this.axisColor = options.axisColor; - if (options.gridColor !== undefined) this.gridColor = options.gridColor; - if (options.dataColor) { - if (typeof options.dataColor === 'string') { - this.dataColor.fill = options.dataColor; - this.dataColor.stroke = options.dataColor; - } else { - if (options.dataColor.fill) { - this.dataColor.fill = options.dataColor.fill; - } - if (options.dataColor.stroke) { - this.dataColor.stroke = options.dataColor.stroke; - } - if (options.dataColor.strokeWidth !== undefined) { - this.dataColor.strokeWidth = options.dataColor.strokeWidth; - } - } + function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; } - this._setBackgroundColor(options.backgroundColor); - } - this.setSize(this.width, this.height); + var momentPrototype__proto = Moment.prototype; - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); - } + momentPrototype__proto.add = add_subtract__add; + momentPrototype__proto.calendar = moment_calendar__calendar; + momentPrototype__proto.clone = clone; + momentPrototype__proto.diff = diff; + momentPrototype__proto.endOf = endOf; + momentPrototype__proto.format = format; + momentPrototype__proto.from = from; + momentPrototype__proto.fromNow = fromNow; + momentPrototype__proto.to = to; + momentPrototype__proto.toNow = toNow; + momentPrototype__proto.get = getSet; + momentPrototype__proto.invalidAt = invalidAt; + momentPrototype__proto.isAfter = isAfter; + momentPrototype__proto.isBefore = isBefore; + momentPrototype__proto.isBetween = isBetween; + momentPrototype__proto.isSame = isSame; + momentPrototype__proto.isValid = moment_valid__isValid; + momentPrototype__proto.lang = lang; + momentPrototype__proto.locale = locale; + momentPrototype__proto.localeData = localeData; + momentPrototype__proto.max = prototypeMax; + momentPrototype__proto.min = prototypeMin; + momentPrototype__proto.parsingFlags = parsingFlags; + momentPrototype__proto.set = getSet; + momentPrototype__proto.startOf = startOf; + momentPrototype__proto.subtract = add_subtract__subtract; + momentPrototype__proto.toArray = toArray; + momentPrototype__proto.toDate = toDate; + momentPrototype__proto.toISOString = moment_format__toISOString; + momentPrototype__proto.toJSON = moment_format__toISOString; + momentPrototype__proto.toString = toString; + momentPrototype__proto.unix = unix; + momentPrototype__proto.valueOf = to_type__valueOf; + + // Year + momentPrototype__proto.year = getSetYear; + momentPrototype__proto.isLeapYear = getIsLeapYear; + + // Week Year + momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + + // Quarter + momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + // Month + momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.daysInMonth = getDaysInMonth; - /** - * Redraw the Graph. - */ - Graph3d.prototype.redraw = function () { - if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; - } + // Week + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); + // Day + momentPrototype__proto.date = getSetDayOfMonth; + momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; + momentPrototype__proto.weekday = getSetLocaleDayOfWeek; + momentPrototype__proto.isoWeekday = getSetISODayOfWeek; + momentPrototype__proto.dayOfYear = getSetDayOfYear; - if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { - this._redrawDataGrid(); - } else if (this.style === Graph3d.STYLE.LINE) { - this._redrawDataLine(); - } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - this._redrawDataBar(); - } else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); - } + // Hour + momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; - this._redrawInfo(); - this._redrawLegend(); - }; + // Minute + momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; - /** - * Clear the canvas before redrawing - */ - Graph3d.prototype._redrawClear = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + // Second + momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; - ctx.clearRect(0, 0, canvas.width, canvas.height); - }; + // Millisecond + momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; - /** - * Redraw the legend showing the colors - */ - Graph3d.prototype._redrawLegend = function () { - var y; + // Offset + momentPrototype__proto.utcOffset = getSetOffset; + momentPrototype__proto.utc = setOffsetToUTC; + momentPrototype__proto.local = setOffsetToLocal; + momentPrototype__proto.parseZone = setOffsetToParsedOffset; + momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; + momentPrototype__proto.isDST = isDaylightSavingTime; + momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; + momentPrototype__proto.isLocal = isLocal; + momentPrototype__proto.isUtcOffset = isUtcOffset; + momentPrototype__proto.isUtc = isUtc; + momentPrototype__proto.isUTC = isUtc; - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + // Timezone + momentPrototype__proto.zoneAbbr = getZoneAbbr; + momentPrototype__proto.zoneName = getZoneName; - var dotSize = this.frame.clientWidth * 0.02; + // Deprecations + momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); + momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); + momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); + momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); - var widthMin, widthMax; - if (this.style === Graph3d.STYLE.DOTSIZE) { - widthMin = dotSize / 2; // px - widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function - } else { - widthMin = 20; // px - widthMax = 20; // px + var momentPrototype = momentPrototype__proto; + + function moment__createUnix (input) { + return local__createLocal(input * 1000); } - var height = Math.max(this.frame.clientHeight * 0.25, 100); - var top = this.margin; - var right = this.frame.clientWidth - this.margin; - var left = right - widthMax; - var bottom = top + height; - } + function moment__createInZone () { + return local__createLocal.apply(null, arguments).parseZone(); + } - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - ctx.lineWidth = 1; - ctx.font = '14px arial'; // TODO: put in options + var defaultCalendar = { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }; - if (this.style === Graph3d.STYLE.DOTCOLOR) { - // draw the color bar - var ymin = 0; - var ymax = height; // Todo: make height customizable - for (y = ymin; y < ymax; y++) { - var f = (y - ymin) / (ymax - ymin); + function locale_calendar__calendar (key, mom, now) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.call(mom, now) : output; + } - //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function - var hue = f * 240; - var color = this._hsv2rgb(hue, 1, 1); + var defaultLongDateFormat = { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY LT', + LLLL : 'dddd, MMMM D, YYYY LT' + }; - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); + function longDateFormat (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; + } + return output; } - ctx.strokeStyle = this.axisColor; - ctx.strokeRect(left, top, widthMax, height); - } + var defaultInvalidDate = 'Invalid date'; - if (this.style === Graph3d.STYLE.DOTSIZE) { - // draw border around color bar - ctx.strokeStyle = this.axisColor; - ctx.fillStyle = this.dataColor.fill; - ctx.beginPath(); - ctx.moveTo(left, top); - ctx.lineTo(right, top); - ctx.lineTo(right - widthMax + widthMin, bottom); - ctx.lineTo(left, bottom); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } + function invalidDate () { + return this._invalidDate; + } - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { - // print values along the color bar - var gridLineLen = 5; // px - var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true); - step.start(); - if (step.getCurrent() < this.valueMin) { - step.next(); + var defaultOrdinal = '%d'; + var defaultOrdinalParse = /\d{1,2}/; + + function ordinal (number) { + return this._ordinal.replace('%d', number); } - while (!step.end()) { - y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + function preParsePostFormat (string) { + return string; + } - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + var defaultRelativeTime = { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' + }; - step.next(); + function relative__relativeTime (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); } - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); - } - }; + function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + } - /** - * Redraw the filter - */ - Graph3d.prototype._redrawFilter = function () { - this.frame.filter.innerHTML = ''; + function locale_set__set (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); + } - if (this.dataFilter) { - var options = { - 'visible': this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; + var prototype__proto = Locale.prototype; - // TODO: css here is not nice here... - this.frame.filter.style.padding = '10px'; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; + prototype__proto._calendar = defaultCalendar; + prototype__proto.calendar = locale_calendar__calendar; + prototype__proto._longDateFormat = defaultLongDateFormat; + prototype__proto.longDateFormat = longDateFormat; + prototype__proto._invalidDate = defaultInvalidDate; + prototype__proto.invalidDate = invalidDate; + prototype__proto._ordinal = defaultOrdinal; + prototype__proto.ordinal = ordinal; + prototype__proto._ordinalParse = defaultOrdinalParse; + prototype__proto.preparse = preParsePostFormat; + prototype__proto.postformat = preParsePostFormat; + prototype__proto._relativeTime = defaultRelativeTime; + prototype__proto.relativeTime = relative__relativeTime; + prototype__proto.pastFuture = pastFuture; + prototype__proto.set = locale_set__set; - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); + // Month + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; + prototype__proto._monthsShort = defaultLocaleMonthsShort; + prototype__proto.monthsParse = localeMonthsParse; - // create an event handler - var me = this; - var onchange = function onchange() { - var index = slider.getIndex(); + // Week + prototype__proto.week = localeWeek; + prototype__proto._week = defaultLocaleWeek; + prototype__proto.firstDayOfYear = localeFirstDayOfYear; + prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + // Day of Week + prototype__proto.weekdays = localeWeekdays; + prototype__proto._weekdays = defaultLocaleWeekdays; + prototype__proto.weekdaysMin = localeWeekdaysMin; + prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; + prototype__proto.weekdaysShort = localeWeekdaysShort; + prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; + prototype__proto.weekdaysParse = localeWeekdaysParse; - me.redraw(); - }; - slider.setOnChangeCallback(onchange); - } else { - this.frame.filter.slider = undefined; - } - }; + // Hours + prototype__proto.isPM = localeIsPM; + prototype__proto._meridiemParse = defaultLocaleMeridiemParse; + prototype__proto.meridiem = localeMeridiem; - /** - * Redraw the slider - */ - Graph3d.prototype._redrawSlider = function () { - if (this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); - } - }; + function lists__get (format, index, field, setter) { + var locale = locale_locales__getLocale(); + var utc = create_utc__createUTC().set(setter, index); + return locale[field](utc, format); + } - /** - * Redraw common information - */ - Graph3d.prototype._redrawInfo = function () { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); + function list (format, index, field, count, setter) { + if (typeof format === 'number') { + index = format; + format = undefined; + } - ctx.font = '14px arial'; // TODO: put in options - ctx.lineStyle = 'gray'; - ctx.fillStyle = 'gray'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; + format = format || ''; - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); - } - }; + if (index != null) { + return lists__get(format, index, field, setter); + } - /** - * Redraw the axis - */ - Graph3d.prototype._redrawAxis = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - from, - to, - step, - prettyStep, - text, - xText, - yText, - zText, - offset, - xOffset, - yOffset, - xMin2d, - xMax2d; + var i; + var out = []; + for (i = 0; i < count; i++) { + out[i] = lists__get(format, i, field, setter); + } + return out; + } - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + 'px arial'; + function lists__listMonths (format, index) { + return list(format, index, 'months', 12, 'month'); + } - // calculate the length for the short grid lines - var gridLenX = 0.025 / this.scale.x; - var gridLenY = 0.025 / this.scale.y; - var textMargin = 5 / this.camera.getArmLength(); // px - var armAngle = this.camera.getArmRotation().horizontal; + function lists__listMonthsShort (format, index) { + return list(format, index, 'monthsShort', 12, 'month'); + } - // draw x-grid lines - ctx.lineWidth = 1; - prettyStep = this.defaultXStep === undefined; - step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); - step.start(); - if (step.getCurrent() < this.xMin) { - step.next(); - } - while (!step.end()) { - var x = step.getCurrent(); + function lists__listWeekdays (format, index) { + return list(format, index, 'weekdays', 7, 'day'); + } - if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - ctx.strokeStyle = this.gridColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } else { - from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + function lists__listWeekdaysShort (format, index) { + return list(format, index, 'weekdaysShort', 7, 'day'); + } - from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + function lists__listWeekdaysMin (format, index) { + return list(format, index, 'weekdaysMin', 7, 'day'); } - yText = Math.cos(armAngle) > 0 ? this.yMin : this.yMax; - text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } else if (Math.sin(armAngle * 2) < 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + locale_locales__getSetGlobalLocale('en', { + ordinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); + + // Side effect imports + utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); + utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); + + var mathAbs = Math.abs; + + function duration_abs__abs () { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); + + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; } - ctx.fillStyle = this.axisColor; - ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); - step.next(); - } + function duration_add_subtract__addSubtract (duration, input, value, direction) { + var other = create__createDuration(input, value); - // draw y-grid lines - ctx.lineWidth = 1; - prettyStep = this.defaultYStep === undefined; - step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); - step.start(); - if (step.getCurrent() < this.yMin) { - step.next(); - } - while (!step.end()) { - if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.gridColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } else { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); + } - from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + // supports only 2.0-style add(1, 's') or add(duration) + function duration_add_subtract__add (input, value) { + return duration_add_subtract__addSubtract(this, input, value, 1); } - xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax; - text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - text.y += textMargin; - } else if (Math.sin(armAngle * 2) > 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + // supports only 2.0-style subtract(1, 's') or subtract(duration) + function duration_add_subtract__subtract (input, value) { + return duration_add_subtract__addSubtract(this, input, value, -1); } - ctx.fillStyle = this.axisColor; - ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); - step.next(); - } + function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years = 0; - // draw z-grid lines and axis - ctx.lineWidth = 1; - prettyStep = this.defaultZStep === undefined; - step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); - step.start(); - if (step.getCurrent() < this.zMin) { - step.next(); - } - xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; - yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; - while (!step.end()) { - // TODO: make z-grid lines really 3d? - from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(from.x - textMargin, from.y); - ctx.stroke(); + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; - step.next(); - } - ctx.lineWidth = 1; - from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; - // draw x-axis - ctx.lineWidth = 1; - // line at yMin - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); - // line at ymax - xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); + hours = absFloor(minutes / 60); + data.hours = hours % 24; - // draw y-axis - ctx.lineWidth = 1; - // line at xMin - from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // line at xMax - from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.axisColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + days += absFloor(hours / 24); - // draw x-label - var xLabel = this.xLabel; - if (xLabel.length > 0) { - yOffset = 0.1 / this.scale.y; - xText = (this.xMin + this.xMax) / 2; - yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } else if (Math.sin(armAngle * 2) < 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + // Accurately convert days to years, assume start from year 0. + years = absFloor(daysToYears(days)); + days -= absFloor(yearsToDays(years)); + + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absFloor(days / 30); + days %= 30; + + // 12 months -> 1 year + years += absFloor(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + + return this; } - ctx.fillStyle = this.axisColor; - ctx.fillText(xLabel, text.x, text.y); - } - // draw y-label - var yLabel = this.yLabel; - if (yLabel.length > 0) { - xOffset = 0.1 / this.scale.x; - xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset; - yText = (this.yMin + this.yMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - } else if (Math.sin(armAngle * 2) > 0) { - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - } else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; } - ctx.fillStyle = this.axisColor; - ctx.fillText(yLabel, text.x, text.y); - } - // draw z-label - var zLabel = this.zLabel; - if (zLabel.length > 0) { - offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? - xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; - yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; - zText = (this.zMin + this.zMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, zText)); - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.axisColor; - ctx.fillText(zLabel, text.x - offset, text.y); - } - }; + function yearsToDays (years) { + // years * 365 + absFloor(years / 4) - + // absFloor(years / 100) + absFloor(years / 400); + return years * 146097 / 400; + } - /** - * Calculate the color based on the given value. - * @param {Number} H Hue, a value be between 0 and 360 - * @param {Number} S Saturation, a value between 0 and 1 - * @param {Number} V Value, a value between 0 and 1 - */ - Graph3d.prototype._hsv2rgb = function (H, S, V) { - var R, G, B, C, Hi, X; + function as (units) { + var days; + var months; + var milliseconds = this._milliseconds; - C = V * S; - Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(H / 60 % 2 - 1)); + units = normalizeUnits(units); - switch (Hi) { - case 0: - R = C;G = X;B = 0;break; - case 1: - R = X;G = C;B = 0;break; - case 2: - R = 0;G = C;B = X;break; - case 3: - R = 0;G = X;B = C;break; - case 4: - R = X;G = 0;B = C;break; - case 5: - R = C;G = 0;B = X;break; + if (units === 'month' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToYears(days) * 12; + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(yearsToDays(this._months / 12)); + switch (units) { + case 'week' : return days / 7 + milliseconds / 6048e5; + case 'day' : return days + milliseconds / 864e5; + case 'hour' : return days * 24 + milliseconds / 36e5; + case 'minute' : return days * 1440 + milliseconds / 6e4; + case 'second' : return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 864e5) + milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } + } - default: - R = 0;G = 0;B = 0;break; - } + // TODO: Use this.as('ms')? + function duration_as__valueOf () { + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } - return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; - }; + function makeAs (alias) { + return function () { + return this.as(alias); + }; + } - /** - * Draw all datapoints as a grid - * This function can be used when the style is 'grid' - */ - Graph3d.prototype._redrawDataGrid = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, - right, - top, - cross, - i, - topSideVisible, - fillStyle, - strokeStyle, - lineWidth, - h, - s, - v, - zAvg; + var asMilliseconds = makeAs('ms'); + var asSeconds = makeAs('s'); + var asMinutes = makeAs('m'); + var asHours = makeAs('h'); + var asDays = makeAs('d'); + var asWeeks = makeAs('w'); + var asMonths = makeAs('M'); + var asYears = makeAs('y'); - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; + function duration_get__get (units) { + units = normalizeUnits(units); + return this[units + 's'](); + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + function makeGetter(name) { + return function () { + return this._data[name]; + }; + } - // calculate the translations and screen position of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); + var duration_get__milliseconds = makeGetter('milliseconds'); + var seconds = makeGetter('seconds'); + var minutes = makeGetter('minutes'); + var hours = makeGetter('hours'); + var days = makeGetter('days'); + var months = makeGetter('months'); + var years = makeGetter('years'); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + function weeks () { + return absFloor(this.days() / 7); + } - // calculate the translation of the point at the bottom (needed for sorting) - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } + var round = Math.round; + var thresholds = { + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year + }; - // sort the points on depth of their (x,y) position (not on z) - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } - if (this.style === Graph3d.STYLE.SURFACE) { - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - cross = this.dataPoints[i].pointCross; + function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { + var duration = create__createDuration(posNegDuration).abs(); + var seconds = round(duration.as('s')); + var minutes = round(duration.as('m')); + var hours = round(duration.as('h')); + var days = round(duration.as('d')); + var months = round(duration.as('M')); + var years = round(duration.as('y')); - if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + var a = seconds < thresholds.s && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < thresholds.m && ['mm', minutes] || + hours === 1 && ['h'] || + hours < thresholds.h && ['hh', hours] || + days === 1 && ['d'] || + days < thresholds.d && ['dd', days] || + months === 1 && ['M'] || + months < thresholds.M && ['MM', months] || + years === 1 && ['y'] || ['yy', years]; - if (this.showGrayBottom || this.showShadow) { - // calculate the cross product of the two vectors from center - // to left and right, in order to know whether we are looking at the - // bottom or at the top side. We can also use the cross product - // for calculating light intensity - var aDiff = Point3d.subtract(cross.trans, point.trans); - var bDiff = Point3d.subtract(top.trans, right.trans); - var crossproduct = Point3d.crossProduct(aDiff, bDiff); - var len = crossproduct.length(); - // FIXME: there is a bug with determining the surface side (shadow or colored) + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); + } - topSideVisible = crossproduct.z > 0; - } else { - topSideVisible = true; + // This function allows you to set a threshold for relative time strings + function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + return true; + } - if (topSideVisible) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - s = 1; // saturation + function humanize (withSuffix) { + var locale = this.localeData(); + var output = duration_humanize__relativeTime(this, !withSuffix, locale); - if (this.showShadow) { - v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = fillStyle; - } else { - v = 1; - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = this.axisColor; - } - } else { - fillStyle = 'gray'; - strokeStyle = this.axisColor; + if (withSuffix) { + output = locale.pastFuture(+this, output); } - ctx.lineWidth = this._getStrokeWidth(point); - ctx.fillStyle = fillStyle; - ctx.strokeStyle = strokeStyle; - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.lineTo(cross.screen.x, cross.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.closePath(); - ctx.fill(); - ctx.stroke(); - } + return locale.postformat(output); } - } else { - // grid style - for (i = 0; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - right = this.dataPoints[i].pointRight; - top = this.dataPoints[i].pointTop; - if (point !== undefined && right !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + var iso_string__abs = Math.abs; - ctx.lineWidth = this._getStrokeWidth(point) * 2; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(right.screen.x, right.screen.y); - ctx.stroke(); - } + function iso_string__toISOString() { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var Y = iso_string__abs(this.years()); + var M = iso_string__abs(this.months()); + var D = iso_string__abs(this.days()); + var h = iso_string__abs(this.hours()); + var m = iso_string__abs(this.minutes()); + var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); + var total = this.asSeconds(); - if (point !== undefined && top !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + top.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } - ctx.lineWidth = this._getStrokeWidth(point) * 2; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.stroke(); - } + return (total < 0 ? '-' : '') + + 'P' + + (Y ? Y + 'Y' : '') + + (M ? M + 'M' : '') + + (D ? D + 'D' : '') + + ((h || m || s) ? 'T' : '') + + (h ? h + 'H' : '') + + (m ? m + 'M' : '') + + (s ? s + 'S' : ''); } - } - }; - Graph3d.prototype._getStrokeWidth = function (point) { - if (point !== undefined) { - if (this.showPerspective) { - return 1 / -point.trans.z * this.dataColor.strokeWidth; - } else { - return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth; - } - } + var duration_prototype__proto = Duration.prototype; + + duration_prototype__proto.abs = duration_abs__abs; + duration_prototype__proto.add = duration_add_subtract__add; + duration_prototype__proto.subtract = duration_add_subtract__subtract; + duration_prototype__proto.as = as; + duration_prototype__proto.asMilliseconds = asMilliseconds; + duration_prototype__proto.asSeconds = asSeconds; + duration_prototype__proto.asMinutes = asMinutes; + duration_prototype__proto.asHours = asHours; + duration_prototype__proto.asDays = asDays; + duration_prototype__proto.asWeeks = asWeeks; + duration_prototype__proto.asMonths = asMonths; + duration_prototype__proto.asYears = asYears; + duration_prototype__proto.valueOf = duration_as__valueOf; + duration_prototype__proto._bubble = bubble; + duration_prototype__proto.get = duration_get__get; + duration_prototype__proto.milliseconds = duration_get__milliseconds; + duration_prototype__proto.seconds = seconds; + duration_prototype__proto.minutes = minutes; + duration_prototype__proto.hours = hours; + duration_prototype__proto.days = days; + duration_prototype__proto.weeks = weeks; + duration_prototype__proto.months = months; + duration_prototype__proto.years = years; + duration_prototype__proto.humanize = humanize; + duration_prototype__proto.toISOString = iso_string__toISOString; + duration_prototype__proto.toString = iso_string__toISOString; + duration_prototype__proto.toJSON = iso_string__toISOString; + duration_prototype__proto.locale = locale; + duration_prototype__proto.localeData = localeData; + + // Deprecations + duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); + duration_prototype__proto.lang = lang; + + // Side effect imports - return this.dataColor.strokeWidth; - }; + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); - /** - * Draw all datapoints as dots. - * This function can be used when the style is 'dot' or 'dot-line' - */ - Graph3d.prototype._redrawDataDot = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i; + // PARSING - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + addRegexToken('x', matchSigned); + addRegexToken('X', matchTimestamp); + addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input, 10) * 1000); + }); + addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); + }); - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + // Side effect imports - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + utils_hooks__hooks.version = '2.10.3'; - // draw the datapoints as colored circles - var dotSize = this.frame.clientWidth * 0.02; // px - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; + setHookCallback(local__createLocal); - if (this.style === Graph3d.STYLE.DOTLINE) { - // draw a vertical line from the bottom to the graph value - //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); - var from = this._convert3Dto2D(point.bottom); - ctx.lineWidth = 1; - ctx.strokeStyle = this.gridColor; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(point.screen.x, point.screen.y); - ctx.stroke(); - } + utils_hooks__hooks.fn = momentPrototype; + utils_hooks__hooks.min = min; + utils_hooks__hooks.max = max; + utils_hooks__hooks.utc = create_utc__createUTC; + utils_hooks__hooks.unix = moment__createUnix; + utils_hooks__hooks.months = lists__listMonths; + utils_hooks__hooks.isDate = isDate; + utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; + utils_hooks__hooks.invalid = valid__createInvalid; + utils_hooks__hooks.duration = create__createDuration; + utils_hooks__hooks.isMoment = isMoment; + utils_hooks__hooks.weekdays = lists__listWeekdays; + utils_hooks__hooks.parseZone = moment__createInZone; + utils_hooks__hooks.localeData = locale_locales__getLocale; + utils_hooks__hooks.isDuration = isDuration; + utils_hooks__hooks.monthsShort = lists__listMonthsShort; + utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; + utils_hooks__hooks.defineLocale = defineLocale; + utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; + utils_hooks__hooks.normalizeUnits = normalizeUnits; + utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; - // calculate radius for the circle - var size; - if (this.style === Graph3d.STYLE.DOTSIZE) { - size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); - } else { - size = dotSize; - } + var _moment = utils_hooks__hooks; - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } else { - radius = size * -(this.eye.z / this.camera.getArmLength()); - } - if (radius < 0) { - radius = 0; - } + return _moment; - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.DOTCOLOR) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } else if (this.style === Graph3d.STYLE.DOTSIZE) { - color = this.dataColor.fill; - borderColor = this.dataColor.stroke; - } else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } + })); + /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(17)(module))) - // draw the circle - ctx.lineWidth = this._getStrokeWidth(point); - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI * 2, true); - ctx.fill(); - ctx.stroke(); - } - }; +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' - */ - Graph3d.prototype._redrawDataBar = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - var i, j, surface, corners; + module.exports = function(module) { + if(!module.webpackPolyfill) { + module.deprecate = function() {}; + module.paths = []; + // module.parent = undefined by default + module.children = []; + module.webpackPolyfill = 1; + } + return module; + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; +/***/ }, +/* 18 */ +/***/ function(module, exports, __webpack_require__) { - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } + /* WEBPACK VAR INJECTION */(function(global) {'use strict'; - // order the translated points by depth - var sortDepth = function sortDepth(a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + var _rng; - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; + var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null; - // draw the datapoints as bars - var xWidth = this.xBarWidth / 2; - var yWidth = this.yBarWidth / 2; - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; + if (globalVar && globalVar.crypto && crypto.getRandomValues) { + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // Moderately fast, high quality + var _rnds8 = new Uint8Array(16); + _rng = function whatwgRNG() { + crypto.getRandomValues(_rnds8); + return _rnds8; + }; + } - // determine color - var hue, color, borderColor; - if (this.style === Graph3d.STYLE.BARCOLOR) { - // calculate the color based on the value - hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); - } else if (this.style === Graph3d.STYLE.BARSIZE) { - color = this.dataColor.fill; - borderColor = this.dataColor.stroke; - } else { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; - color = this._hsv2rgb(hue, 1, 1); - borderColor = this._hsv2rgb(hue, 1, 0.8); + if (!_rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + _rng = function () { + for (var i = 0, r; i < 16; i++) { + if ((i & 3) === 0) r = Math.random() * 4294967296; + _rnds[i] = r >>> ((i & 3) << 3) & 255; } - // calculate size for the bar - if (this.style === Graph3d.STYLE.BARSIZE) { - xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - } + return _rnds; + }; + } - // calculate all corner points - var me = this; - var point3d = point.point; - var top = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z) }]; - var bottom = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin) }]; + // uuid.js + // + // Copyright (c) 2010-2012 Robert Kieffer + // MIT License - http://opensource.org/licenses/mit-license.php - // calculate screen location of the points - top.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); - bottom.forEach(function (obj) { - obj.screen = me._convert3Dto2D(obj.point); - }); + // Unique ID creation requires a high quality random # generator. We feature + // detect to determine the best RNG source, normalizing to a function that + // returns 128-bits of randomness, since that's what's usually required - // create five sides, calculate both corner points and center points - var surfaces = [{ corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point) }, { corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point) }, { corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point) }, { corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point) }, { corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point) }]; - point.surfaces = surfaces; + //var _rng = require('./rng'); - // calculate the distance of each of the surface centers to the camera - for (j = 0; j < surfaces.length; j++) { - surface = surfaces[j]; - var transCenter = this._convertPointToTranslation(surface.center); - surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; - // TODO: this dept calculation doesn't work 100% of the cases due to perspective, - // but the current solution is fast/simple and works in 99.9% of all cases - // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + // Maps for number <-> hex string conversion + var _byteToHex = []; + var _hexToByte = {}; + for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 256).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; + } + + // **`parse()` - Parse a UUID into it's component bytes** + function parse(s, buf, offset) { + var i = buf && offset || 0, + ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { + if (ii < 16) { + // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; } + }); - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + return buf; + } - // both are equal - return 0; - }); + // **`unparse()` - Convert UUID byte array (ala parse()) into a string** + function unparse(buf, offset) { + var i = offset || 0, + bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]]; + } - // draw the ordered surfaces - ctx.lineWidth = this._getStrokeWidth(point); - ctx.strokeStyle = borderColor; - ctx.fillStyle = color; - // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside - for (j = 2; j < surfaces.length; j++) { - surface = surfaces[j]; - corners = surface.corners; - ctx.beginPath(); - ctx.moveTo(corners[3].screen.x, corners[3].screen.y); - ctx.lineTo(corners[0].screen.x, corners[0].screen.y); - ctx.lineTo(corners[1].screen.x, corners[1].screen.y); - ctx.lineTo(corners[2].screen.x, corners[2].screen.y); - ctx.lineTo(corners[3].screen.x, corners[3].screen.y); - ctx.fill(); - ctx.stroke(); - } - } - }; + // **`v1()` - Generate time-based UUID** + // + // Inspired by https://github.com/LiosK/UUID.js + // and http://docs.python.org/library/uuid.html - /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' - */ - Graph3d.prototype._redrawDataLine = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext('2d'), - point, - i; + // random #'s we need to init node and clockseq + var _seedBytes = _rng(); - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) + var _nodeId = [_seedBytes[0] | 1, _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]]; - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); + // Per 4.2.2, randomize (14 bit) clockseq + var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 16383; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } + // Previous uuid creation time + var _lastMSecs = 0, + _lastNSecs = 0; - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; + // See https://github.com/broofa/node-uuid for API details + function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; - ctx.lineWidth = this._getStrokeWidth(point); - ctx.lineJoin = 'round'; - ctx.lineCap = 'round'; - ctx.strokeStyle = this.dataColor.stroke; - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); + options = options || {}; - // draw the datapoints as colored circles - for (i = 1; i < this.dataPoints.length; i++) { - point = this.dataPoints[i]; - ctx.lineTo(point.screen.x, point.screen.y); - } + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; - // finish the line - ctx.stroke(); - } - }; + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); - /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) - */ - Graph3d.prototype._onMouseDown = function (event) { - event = event || window.event; + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; - // check if mouse is still down (may be up when focus is lost for example - // in an iframe) - if (this.leftButtonDown) { - this._onMouseUp(event); + // Time since last uuid creation (in msecs) + var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 16383; } - // only react on left mouse button down - this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!this.leftButtonDown && !this.touchDown) return; + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; - this.frame.style.cursor = 'move'; + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the graph, so we can - // remove the eventlisteners lateron in the function mouseUp() - var me = this; - this.onmousemove = function (event) { - me._onMouseMove(event); - }; - this.onmouseup = function (event) { - me._onMouseUp(event); - }; - util.addEventListener(document, 'mousemove', me.onmousemove); - util.addEventListener(document, 'mouseup', me.onmouseup); - util.preventDefault(event); - }; + // `time_low` + var tl = ((msecs & 268435455) * 10000 + nsecs) % 4294967296; + b[i++] = tl >>> 24 & 255; + b[i++] = tl >>> 16 & 255; + b[i++] = tl >>> 8 & 255; + b[i++] = tl & 255; - /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event - */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.event; + // `time_mid` + var tmh = msecs / 4294967296 * 10000 & 268435455; + b[i++] = tmh >>> 8 & 255; + b[i++] = tmh & 255; - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; + // `time_high_and_version` + b[i++] = tmh >>> 24 & 15 | 16; // include version + b[i++] = tmh >>> 16 & 255; - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 128; - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + // `clock_seq_low` + b[i++] = clockseq & 255; - // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... - // the -0.001 is to take care that the vertical axis is always drawn at the left front corner - if (Math.abs(Math.sin(horizontalNew)) < snapValue) { - horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001; - } - if (Math.abs(Math.cos(horizontalNew)) < snapValue) { - horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; } - // snap vertically to nice angles - if (Math.abs(Math.sin(verticalNew)) < snapValue) { - verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; + return buf ? buf : unparse(b); + } + + // **`v4()` - Generate random UUID** + + // See https://github.com/broofa/node-uuid for API details + function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof options == 'string') { + buf = options == 'binary' ? new Array(16) : null; + options = null; } - if (Math.abs(Math.cos(verticalNew)) < snapValue) { - verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = rnds[6] & 15 | 64; + rnds[8] = rnds[8] & 63 | 128; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } } - this.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); + return buf || unparse(rnds); + } + + // Export public API + var uuid = v4; + uuid.v1 = v1; + uuid.v4 = v4; + uuid.parse = parse; + uuid.unparse = unparse; + + module.exports = uuid; + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); +/***/ }, +/* 19 */ +/***/ function(module, exports, __webpack_require__) { - util.preventDefault(event); - }; + // DOM utility methods /** - * Stop moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {event} event The event + * this prepares the JSON container for allocating SVG elements + * @param JSONcontainer + * @private */ - Graph3d.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; - this.leftButtonDown = false; + 'use strict'; - // remove event listeners here - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); + exports.prepareElements = function (JSONcontainer) { + // cleanup the redundant svgElements; + for (var elementType in JSONcontainer) { + if (JSONcontainer.hasOwnProperty(elementType)) { + JSONcontainer[elementType].redundant = JSONcontainer[elementType].used; + JSONcontainer[elementType].used = []; + } + } }; /** - * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point - * @param {Event} event A mouse move event + * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from + * which to remove the redundant elements. + * + * @param JSONcontainer + * @private */ - Graph3d.prototype._onTooltip = function (event) { - var delay = 300; // ms - var boundingRect = this.frame.getBoundingClientRect(); - var mouseX = getMouseX(event) - boundingRect.left; - var mouseY = getMouseY(event) - boundingRect.top; - - if (!this.showTooltip) { - return; - } - - if (this.tooltipTimeout) { - clearTimeout(this.tooltipTimeout); + exports.cleanupElements = function (JSONcontainer) { + // cleanup the redundant svgElements; + for (var elementType in JSONcontainer) { + if (JSONcontainer.hasOwnProperty(elementType)) { + if (JSONcontainer[elementType].redundant) { + for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) { + JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]); + } + JSONcontainer[elementType].redundant = []; + } + } } + }; - // (delayed) display of a tooltip only if no mouse button is down - if (this.leftButtonDown) { - this._hideTooltip(); - return; + /** + * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer + * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. + * + * @param elementType + * @param JSONcontainer + * @param svgContainer + * @returns {*} + * @private + */ + exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) { + var element; + // allocate SVG element, if it doesnt yet exist, create one. + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before + // check if there is an redundant element + if (JSONcontainer[elementType].redundant.length > 0) { + element = JSONcontainer[elementType].redundant[0]; + JSONcontainer[elementType].redundant.shift(); + } else { + // create a new element and add it to the SVG + element = document.createElementNS('http://www.w3.org/2000/svg', elementType); + svgContainer.appendChild(element); + } + } else { + // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. + element = document.createElementNS('http://www.w3.org/2000/svg', elementType); + JSONcontainer[elementType] = { used: [], redundant: [] }; + svgContainer.appendChild(element); } + JSONcontainer[elementType].used.push(element); + return element; + }; - if (this.tooltip && this.tooltip.dataPoint) { - // tooltip is currently visible - var dataPoint = this._dataPointFromXY(mouseX, mouseY); - if (dataPoint !== this.tooltip.dataPoint) { - // datapoint changed - if (dataPoint) { - this._showTooltip(dataPoint); + /** + * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer + * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. + * + * @param elementType + * @param JSONcontainer + * @param DOMContainer + * @returns {*} + * @private + */ + exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { + var element; + // allocate DOM element, if it doesnt yet exist, create one. + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before + // check if there is an redundant element + if (JSONcontainer[elementType].redundant.length > 0) { + element = JSONcontainer[elementType].redundant[0]; + JSONcontainer[elementType].redundant.shift(); + } else { + // create a new element and add it to the SVG + element = document.createElement(elementType); + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); } else { - this._hideTooltip(); + DOMContainer.appendChild(element); } } } else { - // tooltip is currently not visible - var me = this; - this.tooltipTimeout = setTimeout(function () { - me.tooltipTimeout = null; - - // show a tooltip if we have a data point - var dataPoint = me._dataPointFromXY(mouseX, mouseY); - if (dataPoint) { - me._showTooltip(dataPoint); - } - }, delay); + // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. + element = document.createElement(elementType); + JSONcontainer[elementType] = { used: [], redundant: [] }; + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); + } else { + DOMContainer.appendChild(element); + } } + JSONcontainer[elementType].used.push(element); + return element; }; /** - * Event handler for touchstart event on mobile devices + * Draw a point object. This is a separate function because it can also be called by the legend. + * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions + * as well. + * + * @param x + * @param y + * @param groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' } + * @param JSONcontainer + * @param svgContainer + * @param labelObj + * @returns {*} */ - Graph3d.prototype._onTouchStart = function (event) { - this.touchDown = true; + exports.drawPoint = function (x, y, groupTemplate, JSONcontainer, svgContainer, labelObj) { + var point; + if (groupTemplate.style == 'circle') { + point = exports.getSVGElement('circle', JSONcontainer, svgContainer); + point.setAttributeNS(null, 'cx', x); + point.setAttributeNS(null, 'cy', y); + point.setAttributeNS(null, 'r', 0.5 * groupTemplate.size); + } else { + point = exports.getSVGElement('rect', JSONcontainer, svgContainer); + point.setAttributeNS(null, 'x', x - 0.5 * groupTemplate.size); + point.setAttributeNS(null, 'y', y - 0.5 * groupTemplate.size); + point.setAttributeNS(null, 'width', groupTemplate.size); + point.setAttributeNS(null, 'height', groupTemplate.size); + } - var me = this; - this.ontouchmove = function (event) { - me._onTouchMove(event); - }; - this.ontouchend = function (event) { - me._onTouchEnd(event); - }; - util.addEventListener(document, 'touchmove', me.ontouchmove); - util.addEventListener(document, 'touchend', me.ontouchend); + if (groupTemplate.style !== undefined) { + point.setAttributeNS(null, 'style', groupTemplate.style); + } + point.setAttributeNS(null, 'class', groupTemplate.className + ' vis-point'); + //handle label - this._onMouseDown(event); + if (labelObj) { + var label = exports.getSVGElement('text', JSONcontainer, svgContainer); + if (labelObj.xOffset) { + x = x + labelObj.xOffset; + } + + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; + } + + if (labelObj.className) { + label.setAttributeNS(null, 'class', labelObj.className + ' vis-label'); + } + label.setAttributeNS(null, 'x', x); + label.setAttributeNS(null, 'y', y); + } + + return point; }; /** - * Event handler for touchmove event on mobile devices + * draw a bar SVG element centered on the X coordinate + * + * @param x + * @param y + * @param className */ - Graph3d.prototype._onTouchMove = function (event) { - this._onMouseMove(event); + exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer, style) { + if (height != 0) { + if (height < 0) { + height *= -1; + y -= height; + } + var rect = exports.getSVGElement('rect', JSONcontainer, svgContainer); + rect.setAttributeNS(null, 'x', x - 0.5 * width); + rect.setAttributeNS(null, 'y', y); + rect.setAttributeNS(null, 'width', width); + rect.setAttributeNS(null, 'height', height); + rect.setAttributeNS(null, 'class', className); + if (style) { + rect.setAttributeNS(null, 'style', style); + } + } }; - /** - * Event handler for touchend event on mobile devices - */ - Graph3d.prototype._onTouchEnd = function (event) { - this.touchDown = false; +/***/ }, +/* 20 */ +/***/ function(module, exports, __webpack_require__) { - util.removeEventListener(document, 'touchmove', this.ontouchmove); - util.removeEventListener(document, 'touchend', this.ontouchend); + 'use strict'; - this._onMouseUp(event); - }; + var util = __webpack_require__(14); + var Queue = __webpack_require__(21); /** - * Event handler for mouse wheel event, used to zoom the graph - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {event} event The event + * DataSet + * + * Usage: + * var dataSet = new DataSet({ + * fieldId: '_id', + * type: { + * // ... + * } + * }); + * + * dataSet.add(item); + * dataSet.add(data); + * dataSet.update(item); + * dataSet.update(data); + * dataSet.remove(id); + * dataSet.remove(ids); + * var data = dataSet.get(); + * var data = dataSet.get(id); + * var data = dataSet.get(ids); + * var data = dataSet.get(ids, options, data); + * dataSet.clear(); + * + * A data set can: + * - add/remove/update data + * - gives triggers upon changes in the data + * - can import/export data in various data formats + * + * @param {Array} [data] Optional array with initial data + * @param {Object} [options] Available options: + * {String} fieldId Field name of the id in the + * items, 'id' by default. + * {Object. 0 ? 1 : x < 0 ? -1 : 0; + DataSet.prototype.on = function (event, callback) { + var subscribers = this._subscribers[event]; + if (!subscribers) { + subscribers = []; + this._subscribers[event] = subscribers; } - var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)); - var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x)); - var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); + subscribers.push({ + callback: callback + }); + }; - // each of the three signs must be either equal to each other or zero - return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs); + // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) + DataSet.prototype.subscribe = function () { + throw new Error('DataSet.subscribe is deprecated. Use DataSet.on instead.'); }; /** - * Find a data point close to given screen position (x, y) - * @param {Number} x - * @param {Number} y - * @return {Object | null} The closest data point or null if not close to any data point - * @private + * Unsubscribe from an event, remove an event listener + * @param {String} event + * @param {function} callback */ - Graph3d.prototype._dataPointFromXY = function (x, y) { - var i, - distMax = 100, - // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); + DataSet.prototype.off = function (event, callback) { + var subscribers = this._subscribers[event]; + if (subscribers) { + this._subscribers[event] = subscribers.filter(function (listener) { + return listener.callback != callback; + }); + } + }; - if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - // the data points are ordered from far away to closest - for (i = this.dataPoints.length - 1; i >= 0; i--) { - dataPoint = this.dataPoints[i]; - var surfaces = dataPoint.surfaces; - if (surfaces) { - for (var s = surfaces.length - 1; s >= 0; s--) { - // split each surface in two triangles, and see if the center point is inside one of these - var surface = surfaces[s]; - var corners = surface.corners; - var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; - var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; - if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) { - // return immediately at the first hit - return dataPoint; - } - } - } - } - } else { - // find the closest data point, using distance to the center of the point on 2d screen - for (i = 0; i < this.dataPoints.length; i++) { - dataPoint = this.dataPoints[i]; - var point = dataPoint.screen; - if (point) { - var distX = Math.abs(x - point.x); - var distY = Math.abs(y - point.y); - var dist = Math.sqrt(distX * distX + distY * distY); + // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0) + DataSet.prototype.unsubscribe = function () { + throw new Error('DataSet.unsubscribe is deprecated. Use DataSet.off instead.'); + }; - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; - } - } - } + /** + * Trigger an event + * @param {String} event + * @param {Object | null} params + * @param {String} [senderId] Optional id of the sender. + * @private + */ + DataSet.prototype._trigger = function (event, params, senderId) { + if (event == '*') { + throw new Error('Cannot trigger event *'); + } + + var subscribers = []; + if (event in this._subscribers) { + subscribers = subscribers.concat(this._subscribers[event]); + } + if ('*' in this._subscribers) { + subscribers = subscribers.concat(this._subscribers['*']); } - return closestDataPoint; + for (var i = 0; i < subscribers.length; i++) { + var subscriber = subscribers[i]; + if (subscriber.callback) { + subscriber.callback(event, params, senderId || null); + } + } }; /** - * Display a tooltip for given data point - * @param {Object} dataPoint - * @private + * Add data. + * Adding an item will fail when there already is an item with the same id. + * @param {Object | Array} data + * @param {String} [senderId] Optional sender id + * @return {Array} addedIds Array with the ids of the added items */ - Graph3d.prototype._showTooltip = function (dataPoint) { - var content, line, dot; - - if (!this.tooltip) { - content = document.createElement('div'); - content.style.position = 'absolute'; - content.style.padding = '10px'; - content.style.border = '1px solid #4d4d4d'; - content.style.color = '#1a1a1a'; - content.style.background = 'rgba(255,255,255,0.7)'; - content.style.borderRadius = '2px'; - content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; - - line = document.createElement('div'); - line.style.position = 'absolute'; - line.style.height = '40px'; - line.style.width = '0'; - line.style.borderLeft = '1px solid #4d4d4d'; - - dot = document.createElement('div'); - dot.style.position = 'absolute'; - dot.style.height = '0'; - dot.style.width = '0'; - dot.style.border = '5px solid #4d4d4d'; - dot.style.borderRadius = '5px'; + DataSet.prototype.add = function (data, senderId) { + var addedIds = [], + id, + me = this; - this.tooltip = { - dataPoint: null, - dom: { - content: content, - line: line, - dot: dot - } - }; + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + id = me._addItem(data[i]); + addedIds.push(id); + } + } else if (data instanceof Object) { + // Single item + id = me._addItem(data); + addedIds.push(id); } else { - content = this.tooltip.dom.content; - line = this.tooltip.dom.line; - dot = this.tooltip.dom.dot; + throw new Error('Unknown dataType'); } - this._hideTooltip(); - - this.tooltip.dataPoint = dataPoint; - if (typeof this.showTooltip === 'function') { - content.innerHTML = this.showTooltip(dataPoint.point); - } else { - content.innerHTML = '' + '' + '' + '' + '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; + if (addedIds.length) { + this._trigger('add', { items: addedIds }, senderId); } - content.style.left = '0'; - content.style.top = '0'; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); - - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; - - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); - - line.style.left = dataPoint.screen.x + 'px'; - line.style.top = dataPoint.screen.y - lineHeight + 'px'; - content.style.left = left + 'px'; - content.style.top = dataPoint.screen.y - lineHeight - contentHeight + 'px'; - dot.style.left = dataPoint.screen.x - dotWidth / 2 + 'px'; - dot.style.top = dataPoint.screen.y - dotHeight / 2 + 'px'; + return addedIds; }; /** - * Hide the tooltip when displayed - * @private + * Update existing items. When an item does not exist, it will be created + * @param {Object | Array} data + * @param {String} [senderId] Optional sender id + * @return {Array} updatedIds The ids of the added or updated items */ - Graph3d.prototype._hideTooltip = function () { - if (this.tooltip) { - this.tooltip.dataPoint = null; + DataSet.prototype.update = function (data, senderId) { + var addedIds = []; + var updatedIds = []; + var updatedData = []; + var me = this; + var fieldId = me._fieldId; - for (var prop in this.tooltip.dom) { - if (this.tooltip.dom.hasOwnProperty(prop)) { - var elem = this.tooltip.dom[prop]; - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } - } + var addOrUpdate = function addOrUpdate(item) { + var id = item[fieldId]; + if (me._data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); + updatedData.push(item); + } else { + // add new item + id = me._addItem(item); + addedIds.push(id); + } + }; + + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + addOrUpdate(data[i]); } + } else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } else { + throw new Error('Unknown dataType'); } - }; - /**--------------------------------------------------------------------------**/ + if (addedIds.length) { + this._trigger('add', { items: addedIds }, senderId); + } + if (updatedIds.length) { + this._trigger('update', { items: updatedIds, data: updatedData }, senderId); + } - /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x - */ - function getMouseX(event) { - if ('clientX' in event) return event.clientX; - return event.targetTouches[0] && event.targetTouches[0].clientX || 0; - } + return addedIds.concat(updatedIds); + }; /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y + * Get a data item or multiple items. + * + * Usage: + * + * get() + * get(options: Object) + * + * get(id: Number | String) + * get(id: Number | String, options: Object) + * + * get(ids: Number[] | String[]) + * get(ids: Number[] | String[], options: Object) + * + * Where: + * + * {Number | String} id The id of an item + * {Number[] | String{}} ids An array with ids of items + * {Object} options An Object with options. Available options: + * {String} [returnType] Type of data to be returned. + * Can be 'Array' (default) or 'Object'. + * {Object.} [type] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by a field name or custom sort function. + * @throws Error */ - function getMouseY(event) { - if ('clientY' in event) return event.clientY; - return event.targetTouches[0] && event.targetTouches[0].clientY || 0; - } + DataSet.prototype.get = function (args) { + var me = this; - module.exports = Graph3d; + // parse the arguments + var id, ids, options; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number') { + // get(id [, options]) + id = arguments[0]; + options = arguments[1]; + } else if (firstType == 'Array') { + // get(ids [, options]) + ids = arguments[0]; + options = arguments[1]; + } else { + // get([, options]) + options = arguments[0]; + } - // use use defaults + // determine the return type + var returnType; + if (options && options.returnType) { + var allowedValues = ['Array', 'Object']; + returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType; + } else { + returnType = 'Array'; + } -/***/ }, -/* 19 */ -/***/ function(module, exports, __webpack_require__) { + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], + item, + itemId, + i, + len; - /** - * @prototype Point2d - * @param {Number} [x] - * @param {Number} [y] - */ - "use strict"; + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, type); + if (filter && !filter(item)) { + item = null; + } + } else if (ids != undefined) { + // return a subset of items + for (i = 0, len = ids.length; i < len; i++) { + item = me._getItem(ids[i], type); + if (!filter || filter(item)) { + items.push(item); + } + } + } else { + // return all items + for (itemId in this._data) { + if (this._data.hasOwnProperty(itemId)) { + item = me._getItem(itemId, type); + if (!filter || filter(item)) { + items.push(item); + } + } + } + } - function Point2d(x, y) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - } + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } - module.exports = Point2d; + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; + if (id != undefined) { + item = this._filterFields(item, fields); + } else { + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } + } + } -/***/ }, -/* 20 */ -/***/ function(module, exports, __webpack_require__) { + // return the results + if (returnType == 'Object') { + var result = {}; + for (i = 0; i < items.length; i++) { + result[items[i].id] = items[i]; + } + return result; + } else { + if (id != undefined) { + // a single item + return item; + } else { + // just return our array + return items; + } + } + }; - /** - * Expose `Emitter`. + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids */ + DataSet.prototype.getIds = function (options) { + var data = this._data, + filter = options && options.filter, + order = options && options.order, + type = options && options.type || this._options.type, + i, + len, + id, + item, + items, + ids = []; - module.exports = Emitter; + if (filter) { + // get filtered items + if (order) { + // create ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (filter(item)) { + items.push(item); + } + } + } - /** - * Initialize a new `Emitter`. - * - * @api public - */ + this._sort(items, order); - function Emitter(obj) { - if (obj) return mixin(obj); - }; + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this._fieldId]; + } + } else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (filter(item)) { + ids.push(item[this._fieldId]); + } + } + } + } + } else { + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); + } + } - /** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ + this._sort(items, order); - function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this._fieldId]; + } + } else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = data[id]; + ids.push(item[this._fieldId]); + } + } + } } - return obj; - } + + return ids; + }; /** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * Returns the DataSet itself. Is overwritten for example by the DataView, + * which returns the DataSet it is connected to instead. */ - - Emitter.prototype.on = - Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); + DataSet.prototype.getDataSet = function () { return this; }; /** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * Execute a callback function for every item in the dataset. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [type] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. */ + DataSet.prototype.forEach = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + data = this._data, + item, + id; - Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); - function on() { - self.off(event, on); - fn.apply(this, arguments); + for (var i = 0, len = items.length; i < len; i++) { + item = items[i]; + id = item[this._fieldId]; + callback(item, id); + } + } else { + // unordered + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (!filter || filter(item)) { + callback(item, id); + } + } + } } - - on.fn = fn; - this.on(event, on); - return this; }; /** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * Map every item in the dataset. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [type] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Object[]} mappedItems */ + DataSet.prototype.map = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + mappedItems = [], + data = this._data, + item; - Emitter.prototype.off = - Emitter.prototype.removeListener = - Emitter.prototype.removeAllListeners = - Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } - - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; - - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; + // convert and filter items + for (var id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (!filter || filter(item)) { + mappedItems.push(callback(item, id)); + } + } } - // remove specific handler - var cb; - for (var i = 0; i < callbacks.length; i++) { - cb = callbacks[i]; - if (cb === fn || cb.fn === fn) { - callbacks.splice(i, 1); - break; - } + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); } - return this; + + return mappedItems; }; /** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} + * Filter the fields of an item + * @param {Object | null} item + * @param {String[]} fields Field names + * @return {Object | null} filteredItem or null if no item is provided + * @private */ + DataSet.prototype._filterFields = function (item, fields) { + if (!item) { + // item is null + return item; + } - Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; + var filteredItem = {}; - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); + if (Array.isArray(fields)) { + for (var field in item) { + if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { + filteredItem[field] = item[field]; + } + } + } else { + for (var field in item) { + if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) { + filteredItem[fields[field]] = item[field]; + } } } - return this; + return filteredItem; }; /** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public + * Sort the provided array with items + * @param {Object[]} items + * @param {String | function} order A field name or custom sort function. + * @private */ - - Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; + DataSet.prototype._sort = function (items, order) { + if (util.isString(order)) { + // order by provided field name + var name = order; // field name + items.sort(function (a, b) { + var av = a[name]; + var bv = b[name]; + return av > bv ? 1 : av < bv ? -1 : 0; + }); + } else if (typeof order === 'function') { + // order by sort function + items.sort(order); + } + // TODO: extend order by an Object {field:String, direction:String} + // where direction can be 'asc' or 'desc' + else { + throw new TypeError('Order must be a function or a string'); + } }; /** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public + * Remove an object by pointer or by id + * @param {String | Number | Object | Array} id Object or id, or an array with + * objects or ids to be removed + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds */ + DataSet.prototype.remove = function (id, senderId) { + var removedIds = [], + i, + len, + removedId; - Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; - }; - - -/***/ }, -/* 21 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * @prototype Point3d - * @param {Number} [x] - * @param {Number} [y] - * @param {Number} [z] - */ - "use strict"; + if (Array.isArray(id)) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } + } + } else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); + } + } - function Point3d(x, y, z) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - this.z = z !== undefined ? z : 0; - }; + if (removedIds.length) { + this._trigger('remove', { items: removedIds }, senderId); + } - /** - * Subtract the two provided points, returns a-b - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} a-b - */ - Point3d.subtract = function (a, b) { - var sub = new Point3d(); - sub.x = a.x - b.x; - sub.y = a.y - b.y; - sub.z = a.z - b.z; - return sub; + return removedIds; }; /** - * Add the two provided points, returns a+b - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} a+b + * Remove an item by its id + * @param {Number | String | Object} id id or item + * @returns {Number | String | null} id + * @private */ - Point3d.add = function (a, b) { - var sum = new Point3d(); - sum.x = a.x + b.x; - sum.y = a.y + b.y; - sum.z = a.z + b.z; - return sum; + DataSet.prototype._remove = function (id) { + if (util.isNumber(id) || util.isString(id)) { + if (this._data[id]) { + delete this._data[id]; + this.length--; + return id; + } + } else if (id instanceof Object) { + var itemId = id[this._fieldId]; + if (itemId && this._data[itemId]) { + delete this._data[itemId]; + this.length--; + return itemId; + } + } + return null; }; /** - * Calculate the average of two 3d points - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} The average, (a+b)/2 + * Clear the data + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds The ids of all removed items */ - Point3d.avg = function (a, b) { - return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); - }; + DataSet.prototype.clear = function (senderId) { + var ids = Object.keys(this._data); - /** - * Calculate the cross product of the two provided points, returns axb - * Documentation: http://en.wikipedia.org/wiki/Cross_product - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} cross product axb - */ - Point3d.crossProduct = function (a, b) { - var crossproduct = new Point3d(); + this._data = {}; + this.length = 0; - crossproduct.x = a.y * b.z - a.z * b.y; - crossproduct.y = a.z * b.x - a.x * b.z; - crossproduct.z = a.x * b.y - a.y * b.x; + this._trigger('remove', { items: ids }, senderId); - return crossproduct; + return ids; }; /** - * Rtrieve the length of the vector (or the distance from this point to the origin - * @return {Number} length + * Find the item with maximum value of a specified field + * @param {String} field + * @return {Object | null} item Item containing max value, or null if no items */ - Point3d.prototype.length = function () { - return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - }; - - module.exports = Point3d; - -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { + DataSet.prototype.max = function (field) { + var data = this._data, + max = null, + maxField = null; - 'use strict'; + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!max || itemField > maxField)) { + max = item; + maxField = itemField; + } + } + } - var Point3d = __webpack_require__(21); + return max; + }; /** - * @class Camera - * The camera is mounted on a (virtual) camera arm. The camera arm can rotate - * The camera is always looking in the direction of the origin of the arm. - * This way, the camera always rotates around one fixed point, the location - * of the camera arm. - * - * Documentation: - * http://en.wikipedia.org/wiki/3D_projection + * Find the item with minimum value of a specified field + * @param {String} field + * @return {Object | null} item Item containing max value, or null if no items */ - function Camera() { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; - - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - - this.calculateCameraOrientation(); - } + DataSet.prototype.min = function (field) { + var data = this._data, + min = null, + minField = null; - /** - * Set the location (origin) of the arm - * @param {Number} x Normalized value of x - * @param {Number} y Normalized value of y - * @param {Number} z Normalized value of z - */ - Camera.prototype.setArmLocation = function (x, y, z) { - this.armLocation.x = x; - this.armLocation.y = y; - this.armLocation.z = z; + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!min || itemField < minField)) { + min = item; + minField = itemField; + } + } + } - this.calculateCameraOrientation(); + return min; }; /** - * Set the rotation of the camera arm - * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. + * Find all distinct values of a specified field + * @param {String} field + * @return {Array} values Array containing all distinct values. If data items + * do not contain the specified field are ignored. + * The returned array is unordered. */ - Camera.prototype.setArmRotation = function (horizontal, vertical) { - if (horizontal !== undefined) { - this.armRotation.horizontal = horizontal; - } + DataSet.prototype.distinct = function (field) { + var data = this._data; + var values = []; + var fieldType = this._options.type && this._options.type[field] || null; + var count = 0; + var i; - if (vertical !== undefined) { - this.armRotation.vertical = vertical; - if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; - if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI; + for (var prop in data) { + if (data.hasOwnProperty(prop)) { + var item = data[prop]; + var value = item[field]; + var exists = false; + for (i = 0; i < count; i++) { + if (values[i] == value) { + exists = true; + break; + } + } + if (!exists && value !== undefined) { + values[count] = value; + count++; + } + } } - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); + } } - }; - /** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical - */ - Camera.prototype.getArmRotation = function () { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; - - return rot; + return values; }; /** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 + * Add a single item. Will fail when an item with the same id already exists. + * @param {Object} item + * @return {String} id + * @private */ - Camera.prototype.setArmLength = function (length) { - if (length === undefined) return; + DataSet.prototype._addItem = function (item) { + var id = item[this._fieldId]; - this.armLength = length; + if (id != undefined) { + // check whether this id is already taken + if (this._data[id]) { + // item already exists + throw new Error('Cannot add item: item with id ' + id + ' already exists'); + } + } else { + // generate an id + id = util.randomUUID(); + item[this._fieldId] = id; + } - // Radius must be larger than the corner of the graph, - // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the - // graph - if (this.armLength < 0.71) this.armLength = 0.71; - if (this.armLength > 5) this.armLength = 5; + var d = {}; + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + this._data[id] = d; + this.length++; - this.calculateCameraOrientation(); + return id; }; /** - * Retrieve the arm length - * @return {Number} length + * Get an item. Fields can be converted to a specific type + * @param {String} id + * @param {Object.} [types] field types to convert + * @return {Object | null} item + * @private */ - Camera.prototype.getArmLength = function () { - return this.armLength; - }; + DataSet.prototype._getItem = function (id, types) { + var field, value; - /** - * Retrieve the camera location - * @return {Point3d} cameraLocation - */ - Camera.prototype.getCameraLocation = function () { - return this.cameraLocation; - }; + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; + } - /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation - */ - Camera.prototype.getCameraRotation = function () { - return this.cameraRotation; + // convert the items field types + var converted = {}; + if (types) { + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = util.convert(value, types[field]); + } + } + } else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = value; + } + } + } + return converted; }; /** - * Calculate the location and rotation of the camera based on the - * position and orientation of the camera arm + * Update a single item: merge with existing item. + * Will fail when the item has no id, or when there does not exist an item + * with the same id. + * @param {Object} item + * @return {String} id + * @private */ - Camera.prototype.calculateCameraOrientation = function () { - // calculate location of the camera - this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); - this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); + DataSet.prototype._updateItem = function (item) { + var id = item[this._fieldId]; + if (id == undefined) { + throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); + } + var d = this._data[id]; + if (!d) { + // item doesn't exist + throw new Error('Cannot update item: no item with id ' + id + ' found'); + } - // calculate rotation of the camera - this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; - this.cameraRotation.y = 0; - this.cameraRotation.z = -this.armRotation.horizontal; + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + + return id; }; - module.exports = Camera; + module.exports = DataSet; /***/ }, -/* 23 */ +/* 21 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; - - var DataView = __webpack_require__(17); - /** - * @class Filter - * - * @param {DataSet} data The google data table - * @param {Number} column The index of the column to be filtered - * @param {Graph} graph The graph + * A queue + * @param {Object} options + * Available options: + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @constructor */ - function Filter(data, column, graph) { - this.data = data; - this.column = column; - this.graph = graph; // the parent graph - - this.index = undefined; - this.value = undefined; - - // read all distinct values and select the first one - this.values = graph.getDistinctValues(data.get(), this.column); - - // sort both numeric and string values correctly - this.values.sort(function (a, b) { - return a > b ? 1 : a < b ? -1 : 0; - }); - - if (this.values.length > 0) { - this.selectValue(0); - } + 'use strict'; - // create an array with the filtered datapoints. this will be loaded afterwards - this.dataPoints = []; + function Queue(options) { + // options + this.delay = null; + this.max = Infinity; - this.loaded = false; - this.onLoadCallback = undefined; + // properties + this._queue = []; + this._timeout = null; + this._extended = null; - if (graph.animationPreload) { - this.loaded = false; - this.loadInBackground(); - } else { - this.loaded = true; - } - }; + this.setOptions(options); + } /** - * Return the label - * @return {string} label + * Update the configuration of the queue + * @param {Object} options + * Available options: + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @param options */ - Filter.prototype.isLoaded = function () { - return this.loaded; + Queue.prototype.setOptions = function (options) { + if (options && typeof options.delay !== 'undefined') { + this.delay = options.delay; + } + if (options && typeof options.max !== 'undefined') { + this.max = options.max; + } + + this._flushIfNeeded(); }; /** - * Return the loaded progress - * @return {Number} percentage between 0 and 100 + * Extend an object with queuing functionality. + * The object will be extended with a function flush, and the methods provided + * in options.replace will be replaced with queued ones. + * @param {Object} object + * @param {Object} options + * Available options: + * - replace: Array. + * A list with method names of the methods + * on the object to be replaced with queued ones. + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @return {Queue} Returns the created queue */ - Filter.prototype.getLoadedProgress = function () { - var len = this.values.length; + Queue.extend = function (object, options) { + var queue = new Queue(options); - var i = 0; - while (this.dataPoints[i]) { - i++; + if (object.flush !== undefined) { + throw new Error('Target object already has a property flush'); } + object.flush = function () { + queue.flush(); + }; - return Math.round(i / len * 100); - }; + var methods = [{ + name: 'flush', + original: undefined + }]; - /** - * Return the label - * @return {string} label - */ - Filter.prototype.getLabel = function () { - return this.graph.filterLabel; - }; + if (options && options.replace) { + for (var i = 0; i < options.replace.length; i++) { + var name = options.replace[i]; + methods.push({ + name: name, + original: object[name] + }); + queue.replace(object, name); + } + } - /** - * Return the columnIndex of the filter - * @return {Number} columnIndex - */ - Filter.prototype.getColumn = function () { - return this.column; + queue._extended = { + object: object, + methods: methods + }; + + return queue; }; /** - * Return the currently selected value. Returns undefined if there is no selection - * @return {*} value + * Destroy the queue. The queue will first flush all queued actions, and in + * case it has extended an object, will restore the original object. */ - Filter.prototype.getSelectedValue = function () { - if (this.index === undefined) return undefined; + Queue.prototype.destroy = function () { + this.flush(); - return this.values[this.index]; + if (this._extended) { + var object = this._extended.object; + var methods = this._extended.methods; + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + if (method.original) { + object[method.name] = method.original; + } else { + delete object[method.name]; + } + } + this._extended = null; + } }; /** - * Retrieve all values of the filter - * @return {Array} values + * Replace a method on an object with a queued version + * @param {Object} object Object having the method + * @param {string} method The method name */ - Filter.prototype.getValues = function () { - return this.values; - }; + Queue.prototype.replace = function (object, method) { + var me = this; + var original = object[method]; + if (!original) { + throw new Error('Method ' + method + ' undefined'); + } - /** - * Retrieve one value of the filter - * @param {Number} index - * @return {*} value - */ - Filter.prototype.getValue = function (index) { - if (index >= this.values.length) throw 'Error: index out of range'; + object[method] = function () { + // create an Array with the arguments + var args = []; + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } - return this.values[index]; + // add this call to the queue + me.queue({ + args: args, + fn: original, + context: this + }); + }; }; /** - * Retrieve the (filtered) dataPoints for the currently selected filter index - * @param {Number} [index] (optional) - * @return {Array} dataPoints + * Queue a call + * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry */ - Filter.prototype._getDataPoints = function (index) { - if (index === undefined) index = this.index; - - if (index === undefined) return []; - - var dataPoints; - if (this.dataPoints[index]) { - dataPoints = this.dataPoints[index]; + Queue.prototype.queue = function (entry) { + if (typeof entry === 'function') { + this._queue.push({ fn: entry }); } else { - var f = {}; - f.column = this.column; - f.value = this.values[index]; - - var dataView = new DataView(this.data, { filter: function filter(item) { - return item[f.column] == f.value; - } }).get(); - dataPoints = this.graph._getDataPoints(dataView); - - this.dataPoints[index] = dataPoints; + this._queue.push(entry); } - return dataPoints; - }; - - /** - * Set a callback function when the filter is fully loaded. - */ - Filter.prototype.setOnLoadCallback = function (callback) { - this.onLoadCallback = callback; + this._flushIfNeeded(); }; /** - * Add a value to the list with available values for this filter - * No double entries will be created. - * @param {Number} index + * Check whether the queue needs to be flushed + * @private */ - Filter.prototype.selectValue = function (index) { - if (index >= this.values.length) throw 'Error: index out of range'; + Queue.prototype._flushIfNeeded = function () { + // flush when the maximum is exceeded. + if (this._queue.length > this.max) { + this.flush(); + } - this.index = index; - this.value = this.values[index]; + // flush after a period of inactivity when a delay is configured + clearTimeout(this._timeout); + if (this.queue.length > 0 && typeof this.delay === 'number') { + var me = this; + this._timeout = setTimeout(function () { + me.flush(); + }, this.delay); + } }; /** - * Load all filtered rows in the background one by one - * Start this method without providing an index! + * Flush all queued calls */ - Filter.prototype.loadInBackground = function (index) { - if (index === undefined) index = 0; - - var frame = this.graph.frame; - - if (index < this.values.length) { - var dataPointsTemp = this._getDataPoints(index); - //this.graph.redrawInfo(); // TODO: not neat - - // create a progress box - if (frame.progress === undefined) { - frame.progress = document.createElement('DIV'); - frame.progress.style.position = 'absolute'; - frame.progress.style.color = 'gray'; - frame.appendChild(frame.progress); - } - var progress = this.getLoadedProgress(); - frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; - // TODO: this is no nice solution... - frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider - frame.progress.style.left = 10 + 'px'; - - var me = this; - setTimeout(function () { - me.loadInBackground(index + 1); - }, 10); - this.loaded = false; - } else { - this.loaded = true; - - // remove the progress box - if (frame.progress !== undefined) { - frame.removeChild(frame.progress); - frame.progress = undefined; - } - - if (this.onLoadCallback) this.onLoadCallback(); + Queue.prototype.flush = function () { + while (this._queue.length > 0) { + var entry = this._queue.shift(); + entry.fn.apply(entry.context || entry.fn, entry.args || []); } }; - module.exports = Filter; + module.exports = Queue; /***/ }, -/* 24 */ +/* 22 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(9); + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); /** - * @constructor Slider + * DataView * - * An html slider control with start/stop/prev/next buttons - * @param {Element} container The element where the slider will be created - * @param {Object} options Available options: - * {boolean} visible If true (default) the - * slider is visible. + * a dataview offers a filtered view on a dataset or an other dataview. + * + * @param {DataSet | DataView} data + * @param {Object} [options] Available options: see method get + * + * @constructor DataView */ - function Slider(container, options) { - if (container === undefined) { - throw 'Error: No container element defined'; - } - this.container = container; - this.visible = options && options.visible != undefined ? options.visible : true; - - if (this.visible) { - this.frame = document.createElement('DIV'); - //this.frame.style.backgroundColor = '#E5E5E5'; - this.frame.style.width = '100%'; - this.frame.style.position = 'relative'; - this.container.appendChild(this.frame); + function DataView(data, options) { + this._data = null; + this._ids = {}; // ids of the items currently in memory (just contains a boolean true) + this.length = 0; // number of items in the DataView + this._options = options || {}; + this._fieldId = 'id'; // name of the field containing id + this._subscribers = {}; // event subscribers - this.frame.prev = document.createElement('INPUT'); - this.frame.prev.type = 'BUTTON'; - this.frame.prev.value = 'Prev'; - this.frame.appendChild(this.frame.prev); + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); + }; - this.frame.play = document.createElement('INPUT'); - this.frame.play.type = 'BUTTON'; - this.frame.play.value = 'Play'; - this.frame.appendChild(this.frame.play); + this.setData(data); + } - this.frame.next = document.createElement('INPUT'); - this.frame.next.type = 'BUTTON'; - this.frame.next.value = 'Next'; - this.frame.appendChild(this.frame.next); + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly - this.frame.bar = document.createElement('INPUT'); - this.frame.bar.type = 'BUTTON'; - this.frame.bar.style.position = 'absolute'; - this.frame.bar.style.border = '1px solid red'; - this.frame.bar.style.width = '100px'; - this.frame.bar.style.height = '6px'; - this.frame.bar.style.borderRadius = '2px'; - this.frame.bar.style.MozBorderRadius = '2px'; - this.frame.bar.style.border = '1px solid #7F7F7F'; - this.frame.bar.style.backgroundColor = '#E5E5E5'; - this.frame.appendChild(this.frame.bar); + /** + * Set a data source for the view + * @param {DataSet | DataView} data + */ + DataView.prototype.setData = function (data) { + var ids, i, len; - this.frame.slide = document.createElement('INPUT'); - this.frame.slide.type = 'BUTTON'; - this.frame.slide.style.margin = '0px'; - this.frame.slide.value = ' '; - this.frame.slide.style.position = 'relative'; - this.frame.slide.style.left = '-100px'; - this.frame.appendChild(this.frame.slide); + if (this._data) { + // unsubscribe from current dataset + if (this._data.off) { + this._data.off('*', this.listener); + } - // create events - var me = this; - this.frame.slide.onmousedown = function (event) { - me._onMouseDown(event); - }; - this.frame.prev.onclick = function (event) { - me.prev(event); - }; - this.frame.play.onclick = function (event) { - me.togglePlay(event); - }; - this.frame.next.onclick = function (event) { - me.next(event); - }; + // trigger a remove of all items in memory + ids = []; + for (var id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + ids.push(id); + } + } + this._ids = {}; + this.length = 0; + this._trigger('remove', { items: ids }); } - this.onChangeCallback = undefined; + this._data = data; - this.values = []; - this.index = undefined; + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id'; - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; - } + // trigger an add of all added items + ids = this._data.getIds({ filter: this._options && this._options.filter }); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this._ids[id] = true; + } + this.length = ids.length; + this._trigger('add', { items: ids }); + + // subscribe to new dataset + if (this._data.on) { + this._data.on('*', this.listener); + } + } + }; /** - * Select the previous index + * Refresh the DataView. Useful when the DataView has a filter function + * containing a variable parameter. */ - Slider.prototype.prev = function () { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); + DataView.prototype.refresh = function () { + var id; + var ids = this._data.getIds({ filter: this._options && this._options.filter }); + var newIds = {}; + var added = []; + var removed = []; + + // check for additions + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + newIds[id] = true; + if (!this._ids[id]) { + added.push(id); + this._ids[id] = true; + this.length++; + } + } + + // check for removals + for (id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + if (!newIds[id]) { + removed.push(id); + delete this._ids[id]; + this.length--; + } + } + } + + // trigger events + if (added.length) { + this._trigger('add', { items: added }); + } + if (removed.length) { + this._trigger('remove', { items: removed }); } }; /** - * Select the next index + * Get data from the data view + * + * Usage: + * + * get() + * get(options: Object) + * get(options: Object, data: Array | DataTable) + * + * get(id: Number) + * get(id: Number, options: Object) + * get(id: Number, options: Object, data: Array | DataTable) + * + * get(ids: Number[]) + * get(ids: Number[], options: Object) + * get(ids: Number[], options: Object, data: Array | DataTable) + * + * Where: + * + * {Number | String} id The id of an item + * {Number[] | String{}} ids An array with ids of items + * {Object} options An Object with options. Available options: + * {String} [type] Type of data to be returned. Can + * be 'DataTable' or 'Array' (default) + * {Object.} [convert] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * {Array | DataTable} [data] If provided, items will be appended to this + * array or table. Required in case of Google + * DataTable. + * @param args */ - Slider.prototype.next = function () { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); + DataView.prototype.get = function (args) { + var me = this; + + // parse the arguments + var ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { + // get(id(s) [, options] [, data]) + ids = arguments[0]; // can be a single id or an array with ids + options = arguments[1]; + data = arguments[2]; + } else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } + + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); + + // create a combined filter method when needed + if (this._options.filter && options && options.filter) { + viewOptions.filter = function (item) { + return me._options.filter(item) && options.filter(item); + }; + } + + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); } + getArguments.push(viewOptions); + getArguments.push(data); + + return this._data && this._data.get.apply(this._data, getArguments); }; /** - * Select the next index + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids */ - Slider.prototype.playNext = function () { - var start = new Date(); + DataView.prototype.getIds = function (options) { + var ids; - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } else if (this.playLoop) { - // jump to the start - index = 0; - this.setIndex(index); - } + if (this._data) { + var defaultFilter = this._options.filter; + var filter; - var end = new Date(); - var diff = end - start; + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + }; + } else { + filter = options.filter; + } + } else { + filter = defaultFilter; + } - // calculate how much time it to to set the index and to execute the callback - // function. - var interval = Math.max(this.playInterval - diff, 0); - // document.title = diff // TODO: cleanup + ids = this._data.getIds({ + filter: filter, + order: options && options.order + }); + } else { + ids = []; + } - var me = this; - this.playTimeout = setTimeout(function () { - me.playNext(); - }, interval); + return ids; }; /** - * Toggle start or stop playing + * Get the DataSet to which this DataView is connected. In case there is a chain + * of multiple DataViews, the root DataSet of this chain is returned. + * @return {DataSet} dataSet */ - Slider.prototype.togglePlay = function () { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); + DataView.prototype.getDataSet = function () { + var dataSet = this; + while (dataSet instanceof DataView) { + dataSet = dataSet._data; } + return dataSet || null; }; /** - * Start playing + * Event listener. Will propagate all events from the connected data set to + * the subscribers of the DataView, but will filter the items and only trigger + * when there are changes in the filtered data set. + * @param {String} event + * @param {Object | null} params + * @param {String} senderId + * @private */ - Slider.prototype.play = function () { - // Test whether already playing - if (this.playTimeout) return; + DataView.prototype._onEvent = function (event, params, senderId) { + var i, len, id, item; + var ids = params && params.items; + var data = this._data; + var updatedData = []; + var added = []; + var updated = []; + var removed = []; - this.playNext(); + if (ids && data) { + switch (event) { + case 'add': + // filter the ids of the added items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + if (item) { + this._ids[id] = true; + added.push(id); + } + } - if (this.frame) { - this.frame.play.value = 'Stop'; - } - }; + break; - /** - * Stop playing - */ - Slider.prototype.stop = function () { - clearInterval(this.playTimeout); - this.playTimeout = undefined; + case 'update': + // determine the event from the views viewpoint: an updated + // item can be added, updated, or removed from this view. + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); - if (this.frame) { - this.frame.play.value = 'Play'; - } - }; + if (item) { + if (this._ids[id]) { + updated.push(id); + updatedData.push(params.data[i]); + } else { + this._ids[id] = true; + added.push(id); + } + } else { + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } else {} + } + } - /** - * Set a callback function which will be triggered when the value of the - * slider bar has changed. - */ - Slider.prototype.setOnChangeCallback = function (callback) { - this.onChangeCallback = callback; - }; + break; - /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds - */ - Slider.prototype.setPlayInterval = function (interval) { - this.playInterval = interval; - }; + case 'remove': + // filter the ids of the removed items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } + } - /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds - */ - Slider.prototype.getPlayInterval = function (interval) { - return this.playInterval; - }; + break; + } - /** - * Set looping on or off - * @pararm {boolean} doLoop If true, the slider will jump to the start when - * the end is passed, and will jump to the end - * when the start is passed. - */ - Slider.prototype.setPlayLoop = function (doLoop) { - this.playLoop = doLoop; - }; + this.length += added.length - removed.length; - /** - * Execute the onchange callback function - */ - Slider.prototype.onChange = function () { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); + if (added.length) { + this._trigger('add', { items: added }, senderId); + } + if (updated.length) { + this._trigger('update', { items: updated, data: updatedData }, senderId); + } + if (removed.length) { + this._trigger('remove', { items: removed }, senderId); + } } }; - /** - * redraw the slider on the correct place - */ - Slider.prototype.redraw = function () { - if (this.frame) { - // resize the bar - this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + 'px'; - this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + 'px'; + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = left + 'px'; - } - }; + // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) + DataView.prototype.subscribe = DataView.prototype.on; + DataView.prototype.unsubscribe = DataView.prototype.off; - /** - * Set the list with values for the slider - * @param {Array} values A javascript array with values (any type) - */ - Slider.prototype.setValues = function (values) { - this.values = values; + module.exports = DataView; - if (this.values.length > 0) this.setIndex(0);else this.index = undefined; - }; + // nothing interesting for me :-( - /** - * Select a value by its index - * @param {Number} index - */ - Slider.prototype.setIndex = function (index) { - if (index < this.values.length) { - this.index = index; +/***/ }, +/* 23 */ +/***/ function(module, exports, __webpack_require__) { - this.redraw(); - this.onChange(); - } else { - throw 'Error: index out of range'; - } - }; + 'use strict'; - /** - * retrieve the index of the currently selected vaue - * @return {Number} index - */ - Slider.prototype.getIndex = function () { - return this.index; - }; + var Emitter = __webpack_require__(25); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + var util = __webpack_require__(14); + var Point3d = __webpack_require__(26); + var Point2d = __webpack_require__(24); + var Camera = __webpack_require__(27); + var Filter = __webpack_require__(28); + var Slider = __webpack_require__(29); + var StepNumber = __webpack_require__(30); /** - * retrieve the currently selected value - * @return {*} value + * @constructor Graph3d + * Graph3d displays data in 3d. + * + * Graph3d is developed in javascript as a Google Visualization Chart. + * + * @param {Element} container The DOM element in which the Graph3d will + * be created. Normally a div element. + * @param {DataSet | DataView | Array} [data] + * @param {Object} [options] */ - Slider.prototype.get = function () { - return this.values[this.index]; - }; - - Slider.prototype._onMouseDown = function (event) { - // only react on left mouse button down - var leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!leftButtonDown) return; + function Graph3d(container, data, options) { + if (!(this instanceof Graph3d)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } - this.startClientX = event.clientX; - this.startSlideX = parseFloat(this.frame.slide.style.left); + // create variables and set default values + this.containerElement = container; + this.width = '400px'; + this.height = '400px'; + this.margin = 10; // px + this.defaultXCenter = '55%'; + this.defaultYCenter = '50%'; - this.frame.style.cursor = 'move'; + this.xLabel = 'x'; + this.yLabel = 'y'; + this.zLabel = 'z'; - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the graph, so we can - // remove the eventlisteners lateron in the function mouseUp() - var me = this; - this.onmousemove = function (event) { - me._onMouseMove(event); - }; - this.onmouseup = function (event) { - me._onMouseUp(event); + var passValueFn = function passValueFn(v) { + return v; }; - util.addEventListener(document, 'mousemove', this.onmousemove); - util.addEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; - - Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - var x = left - 3; - - var index = Math.round(x / width * (this.values.length - 1)); - if (index < 0) index = 0; - if (index > this.values.length - 1) index = this.values.length - 1; - - return index; - }; + this.xValueLabel = passValueFn; + this.yValueLabel = passValueFn; + this.zValueLabel = passValueFn; - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + this.filterLabel = 'time'; + this.legendLabel = 'value'; - var x = index / (this.values.length - 1) * width; - var left = x + 3; + this.style = Graph3d.STYLE.DOT; + this.showPerspective = true; + this.showGrid = true; + this.keepAspectRatio = true; + this.showShadow = false; + this.showGrayBottom = false; // TODO: this does not work correctly + this.showTooltip = false; + this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube' - return left; - }; + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; + this.camera = new Camera(); + this.camera.setArmRotation(1, 0.5); + this.camera.setArmLength(1.7); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - var index = this.leftToIndex(x); + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects - this.setIndex(index); + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; - util.preventDefault(); - }; + this.xMin = 0; + this.xStep = undefined; // auto by default + this.xMax = 1; + this.yMin = 0; + this.yStep = undefined; // auto by default + this.yMax = 1; + this.zMin = 0; + this.zStep = undefined; // auto by default + this.zMax = 1; + this.valueMin = 0; + this.valueMax = 1; + this.xBarWidth = 1; + this.yBarWidth = 1; + // TODO: customize axis range - Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; + // colors + this.axisColor = '#4D4D4D'; + this.gridColor = '#D3D3D3'; + this.dataColor = { + fill: '#7DC1FF', + stroke: '#3267D2', + strokeWidth: 1 // px + }; - // remove event listeners - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); + // create a frame and canvas + this.create(); - util.preventDefault(); - }; + // apply options (also when undefined) + this.setOptions(options); - module.exports = Slider; + // apply data + if (data) { + this.setData(data); + } + } -/***/ }, -/* 25 */ -/***/ function(module, exports, __webpack_require__) { + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); /** - * @prototype StepNumber - * The class StepNumber is an iterator for Numbers. You provide a start and end - * value, and a best step size. StepNumber itself rounds to fixed values and - * a finds the step that best fits the provided step. - * - * If prettyStep is true, the step size is chosen as close as possible to the - * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... - * - * Example usage: - * var step = new StepNumber(0, 10, 2.5, true); - * step.start(); - * while (!step.end()) { - * alert(step.getCurrent()); - * step.next(); - * } - * - * Version: 1.0 - * - * @param {Number} start The start value - * @param {Number} end The end value - * @param {Number} step Optional. Step size. Must be a positive value. - * @param {boolean} prettyStep Optional. If true, the step size is rounded - * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + * Calculate the scaling values, dependent on the range in x, y, and z direction */ - "use strict"; + Graph3d.prototype._setScale = function () { + this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); - function StepNumber(start, end, step, prettyStep) { - // set default values - this._start = 0; - this._end = 0; - this._step = 1; - this.prettyStep = true; - this.precision = 5; + // keep aspect ration between x and y scale if desired + if (this.keepAspectRatio) { + if (this.scale.x < this.scale.y) { + //noinspection JSSuspiciousNameCombination + this.scale.y = this.scale.x; + } else { + //noinspection JSSuspiciousNameCombination + this.scale.x = this.scale.y; + } + } - this._current = 0; - this.setRange(start, end, step, prettyStep); + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? + + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); + + // position the camera arm + var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; + var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; + var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; + this.camera.setArmLocation(xCenter, yCenter, zCenter); }; /** - * Set a new range: start, end and step. - * - * @param {Number} start The start value - * @param {Number} end The end value - * @param {Number} step Optional. Step size. Must be a positive value. - * @param {boolean} prettyStep Optional. If true, the step size is rounded - * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + * Convert a 3D location to a 2D location on screen + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point2d} point2d A 2D point with parameters x, y */ - StepNumber.prototype.setRange = function (start, end, step, prettyStep) { - this._start = start ? start : 0; - this._end = end ? end : 0; - - this.setStep(step, prettyStep); + Graph3d.prototype._convert3Dto2D = function (point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); }; /** - * Set a new step size - * @param {Number} step New step size. Must be a positive value - * @param {boolean} prettyStep Optional. If true, the provided step is rounded - * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + * Convert a 3D location its translation seen from the camera + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera */ - StepNumber.prototype.setStep = function (step, prettyStep) { - if (step === undefined || step <= 0) return; + Graph3d.prototype._convertPointToTranslation = function (point3d) { + var ax = point3d.x * this.scale.x, + ay = point3d.y * this.scale.y, + az = point3d.z * this.scale.z, + cx = this.camera.getCameraLocation().x, + cy = this.camera.getCameraLocation().y, + cz = this.camera.getCameraLocation().z, - if (prettyStep !== undefined) this.prettyStep = prettyStep; + // calculate angles + sinTx = Math.sin(this.camera.getCameraRotation().x), + cosTx = Math.cos(this.camera.getCameraRotation().x), + sinTy = Math.sin(this.camera.getCameraRotation().y), + cosTy = Math.cos(this.camera.getCameraRotation().y), + sinTz = Math.sin(this.camera.getCameraRotation().z), + cosTz = Math.cos(this.camera.getCameraRotation().z), + + // calculate translation + dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), + dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), + dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); - if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; + return new Point3d(dx, dy, dz); }; /** - * Calculate a nice step size, closest to the desired step size. - * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an - * integer Number. For example 1, 2, 5, 10, 20, 50, etc... - * @param {Number} step Desired step size - * @return {Number} Nice step size + * Convert a translation point to a point on the screen + * @param {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera + * @return {Point2d} point2d A 2D point with parameters x, y */ - StepNumber.calculatePrettyStep = function (step) { - var log10 = function log10(x) { - return Math.log(x) / Math.LN10; - }; - - // try three steps (multiple of 1, 2, or 5 - var step1 = Math.pow(10, Math.round(log10(step))), - step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), - step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); - - // choose the best step (closest to minimum step) - var prettyStep = step1; - if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; - if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; + Graph3d.prototype._convertTranslationToScreen = function (translation) { + var ex = this.eye.x, + ey = this.eye.y, + ez = this.eye.z, + dx = translation.x, + dy = translation.y, + dz = translation.z; - // for safety - if (prettyStep <= 0) { - prettyStep = 1; + // calculate position on screen from translation + var bx; + var by; + if (this.showPerspective) { + bx = (dx - ex) * (ez / dz); + by = (dy - ey) * (ez / dz); + } else { + bx = dx * -(ez / this.camera.getArmLength()); + by = dy * -(ez / this.camera.getArmLength()); } - return prettyStep; + // shift and scale the point to the center of the screen + // use the width of the graph to scale both horizontally and vertically. + return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); }; /** - * returns the current value of the step - * @return {Number} current value + * Set the background styling for the graph + * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ - StepNumber.prototype.getCurrent = function () { - return parseFloat(this._current.toPrecision(this.precision)); - }; + Graph3d.prototype._setBackgroundColor = function (backgroundColor) { + var fill = 'white'; + var stroke = 'gray'; + var strokeWidth = 1; - /** - * returns the current step size - * @return {Number} current step size - */ - StepNumber.prototype.getStep = function () { - return this._step; + if (typeof backgroundColor === 'string') { + fill = backgroundColor; + stroke = 'none'; + strokeWidth = 0; + } else if (typeof backgroundColor === 'object') { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } else if (backgroundColor === undefined) {} else { + throw 'Unsupported type of backgroundColor'; + } + + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + 'px'; + this.frame.style.borderStyle = 'solid'; }; - /** - * Set the current value to the largest value smaller than start, which - * is a multiple of the step size - */ - StepNumber.prototype.start = function () { - this._current = this._start - this._start % this._step; + /// enumerate the available styles + Graph3d.STYLE = { + BAR: 0, + BARCOLOR: 1, + BARSIZE: 2, + DOT: 3, + DOTLINE: 4, + DOTCOLOR: 5, + DOTSIZE: 6, + GRID: 7, + LINE: 8, + SURFACE: 9 }; /** - * Do a step, add the step size to the current value + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found */ - StepNumber.prototype.next = function () { - this._current += this._step; + Graph3d.prototype._getStyleNumber = function (styleName) { + switch (styleName) { + case 'dot': + return Graph3d.STYLE.DOT; + case 'dot-line': + return Graph3d.STYLE.DOTLINE; + case 'dot-color': + return Graph3d.STYLE.DOTCOLOR; + case 'dot-size': + return Graph3d.STYLE.DOTSIZE; + case 'line': + return Graph3d.STYLE.LINE; + case 'grid': + return Graph3d.STYLE.GRID; + case 'surface': + return Graph3d.STYLE.SURFACE; + case 'bar': + return Graph3d.STYLE.BAR; + case 'bar-color': + return Graph3d.STYLE.BARCOLOR; + case 'bar-size': + return Graph3d.STYLE.BARSIZE; + } + + return -1; }; /** - * Returns true whether the end is reached - * @return {boolean} True if the current value has passed the end value. + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ - StepNumber.prototype.end = function () { - return this._current > this._end; - }; + Graph3d.prototype._determineColumnIndexes = function (data, style) { + if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) { + // 3 columns expected, and optionally a 4th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = undefined; - module.exports = StepNumber; + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; + } + } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + // 4 columns expected, and optionally a 5th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = 3; -/***/ }, -/* 26 */ -/***/ function(module, exports, __webpack_require__) { + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; + } + } else { + throw 'Unknown style "' + this.style + '"'; + } + }; - 'use strict'; + Graph3d.prototype.getNumberOfRows = function (data) { + return data.length; + }; - var Emitter = __webpack_require__(20); - var Hammer = __webpack_require__(5); - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - var Range = __webpack_require__(30); - var Core = __webpack_require__(33); - var TimeAxis = __webpack_require__(42); - var CurrentTime = __webpack_require__(27); - var CustomTime = __webpack_require__(45); - var ItemSet = __webpack_require__(34); - - var Configurator = __webpack_require__(46); - var Validator = __webpack_require__(48)['default']; - var printStyle = __webpack_require__(48).printStyle; - var allOptions = __webpack_require__(49).allOptions; - var configureOptions = __webpack_require__(49).configureOptions; + Graph3d.prototype.getNumberOfColumns = function (data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; + } + } + return counter; + }; - /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | vis.DataView | Array} [items] - * @param {vis.DataSet | vis.DataView | Array} [groups] - * @param {Object} [options] See Timeline.setOptions for the available options. - * @constructor - * @extends Core - */ - function Timeline(container, items, groups, options) { - if (!(this instanceof Timeline)) { - throw new SyntaxError('Constructor must be called with the new operator'); + Graph3d.prototype.getDistinctValues = function (data, column) { + var distinctValues = []; + for (var i = 0; i < data.length; i++) { + if (distinctValues.indexOf(data[i][column]) == -1) { + distinctValues.push(data[i][column]); + } } + return distinctValues; + }; - // if the third element is options, the forth is groups (optionally); - if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { - var forthArgument = options; - options = groups; - groups = forthArgument; + Graph3d.prototype.getColumnRange = function (data, column) { + var minMax = { min: data[0][column], max: data[0][column] }; + for (var i = 0; i < data.length; i++) { + if (minMax.min > data[i][column]) { + minMax.min = data[i][column]; + } + if (minMax.max < data[i][column]) { + minMax.max = data[i][column]; + } } + return minMax; + }; + /** + * Initialize the data from the data table. Calculate minimum and maximum values + * and column index values + * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. + * @param {Number} style Style Number + */ + Graph3d.prototype._dataInitialize = function (rawData, style) { var me = this; - this.defaultOptions = { - start: null, - end: null, - autoResize: true, + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off('*', this._onChange); + } - orientation: { - axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' - item: 'bottom' // not relevant - }, + if (rawData === undefined) return; - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); + } - // Create the DOM, props, and emitter - this._create(container); + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } else { + throw new Error('Array, DataSet, or DataView expected'); + } - // all components listed here will be repainted automatically - this.components = []; + if (data.length == 0) return; - this.body = { - dom: this.dom, - domProps: this.props, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this) - }, - hiddenDates: [], - util: { - getScale: function getScale() { - return me.timeAxis.step.scale; - }, - getStep: function getStep() { - return me.timeAxis.step.step; - }, + this.dataSet = rawData; + this.dataTable = data; - 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) - } + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); }; + this.dataSet.on('*', this._onChange); - // 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.timeAxis2 = null; // used in case of orientation option 'both' - this.components.push(this.timeAxis); + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + // determine the location of x,y,z,value,filter columns + this.colX = 'x'; + this.colY = 'y'; + this.colZ = 'z'; + this.colValue = 'style'; + this.colFilter = 'filter'; - // item set - this.itemSet = new ItemSet(this.body); - this.components.push(this.itemSet); + // check if a filter column is provided + if (data[0].hasOwnProperty('filter')) { + if (this.dataFilter === undefined) { + this.dataFilter = new Filter(rawData, this.colFilter, this); + this.dataFilter.setOnLoadCallback(function () { + me.redraw(); + }); + } + } - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; - this.on('tap', function (event) { - me.emit('click', me.getEventProperties(event)); - }); - this.on('doubletap', function (event) { - me.emit('doubleClick', me.getEventProperties(event)); - }); - this.dom.root.oncontextmenu = function (event) { - me.emit('contextmenu', me.getEventProperties(event)); - }; + // determine barWidth from data + if (withBars) { + if (this.defaultXBarWidth !== undefined) { + this.xBarWidth = this.defaultXBarWidth; + } else { + var dataX = this.getDistinctValues(data, this.colX); + this.xBarWidth = dataX[1] - dataX[0] || 1; + } - // apply options - if (options) { - this.setOptions(options); + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } else { + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 1; + } } - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); + // calculate minimums and maximums + var xRange = this.getColumnRange(data, this.colX); + if (withBars) { + xRange.min -= this.xBarWidth / 2; + xRange.max += this.xBarWidth / 2; } + this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min; + this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max; + if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; + this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5; - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); + var yRange = this.getColumnRange(data, this.colY); + if (withBars) { + yRange.min -= this.yBarWidth / 2; + yRange.max += this.yBarWidth / 2; } - } + this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min; + this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max; + if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; + this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5; - // Extend the functionality from Core - Timeline.prototype = new Core(); + var zRange = this.getColumnRange(data, this.colZ); + this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; + this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; + if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; + this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; - /** - * Load a configurator - * @return {Object} - * @private - */ - Timeline.prototype._createConfigurator = function () { - return new Configurator(this, this.dom.container, configureOptions); + if (this.colValue !== undefined) { + var valueRange = this.getColumnRange(data, this.colValue); + this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; + this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; + if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + } + + // set the scale dependent on the ranges. + this._setScale(); }; /** - * Force a redraw. The size of all items will be recalculated. - * Can be useful to manually redraw when option autoResize=false and the window - * has been resized, or when the items CSS has been changed. + * Filter the data based on the current filter + * @param {Array} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen */ - Timeline.prototype.redraw = function () { - this.itemSet && this.itemSet.markDirty({ refreshItems: true }); - this._redraw(); - }; + Graph3d.prototype._getDataPoints = function (data) { + // TODO: store the created matrix dataPoints in the filters instead of reloading each time + var x, y, i, z, obj, point; - Timeline.prototype.setOptions = function (options) { - // validate options - var errorFound = Validator.validate(options, allOptions); - if (errorFound === true) { - console.log('%cErrors have been found in the supplied options object.', printStyle); - } + var dataPoints = []; - Core.prototype.setOptions.call(this, options); + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { + // copy all values from the google data table to a matrix + // the provided values are supposed to form a grid of (x,y) positions - if ('type' in options) { - if (options.type !== this.options.type) { - this.options.type = options.type; + // create two lists with all present x and y values + var dataX = []; + var dataY = []; + for (i = 0; i < this.getNumberOfRows(data); i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; - // force recreation of all items - var itemsData = this.itemsData; - if (itemsData) { - var selection = this.getSelection(); - this.setItems(null); // remove all - this.setItems(itemsData); // add all - this.setSelection(selection); // restore selection + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); } } - } - }; - /** - * Set items - * @param {vis.DataSet | Array | null} items - */ - Timeline.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; + var sortNumber = function sortNumber(a, b) { + return a - b; + }; + dataX.sort(sortNumber); + dataY.sort(sortNumber); - // 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' + // create a grid, a 2d matrix, with all values. + var dataMatrix = []; // temporary data matrix + for (i = 0; i < data.length; i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; + z = data[i][this.colZ] || 0; + + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); + + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; } - }); - } - // set items - this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; + + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); + + dataMatrix[xIndex][yIndex] = obj; + + dataPoints.push(obj); + } + + // fill in the pointers to the neighbors. + for (x = 0; x < dataMatrix.length; x++) { + for (y = 0; y < dataMatrix[x].length; y++) { + if (dataMatrix[x][y]) { + dataMatrix[x][y].pointRight = x < dataMatrix.length - 1 ? dataMatrix[x + 1][y] : undefined; + dataMatrix[x][y].pointTop = y < dataMatrix[x].length - 1 ? dataMatrix[x][y + 1] : undefined; + dataMatrix[x][y].pointCross = x < dataMatrix.length - 1 && y < dataMatrix[x].length - 1 ? dataMatrix[x + 1][y + 1] : undefined; + } + } + } + } else { + // 'dot', 'dot-line', etc. + // copy all values from the google data table to a list with Point3d objects + for (i = 0; i < data.length; i++) { + point = new Point3d(); + point.x = data[i][this.colX] || 0; + point.y = data[i][this.colY] || 0; + point.z = data[i][this.colZ] || 0; - if (initialLoad) { - if (this.options.start != undefined || this.options.end != undefined) { - if (this.options.start == undefined || this.options.end == undefined) { - var range = this.getItemRange(); + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; } - var start = this.options.start != undefined ? this.options.start : range.min; - var end = this.options.end != undefined ? this.options.end : range.max; + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; - this.setWindow(start, end, { animation: false }); - } else { - this.fit({ animation: false }); + dataPoints.push(obj); } } + + return dataPoints; }; /** - * Set groups - * @param {vis.DataSet | Array} groups + * Create the main frame for the Graph3d. + * This function is executed once when a Graph3d object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. */ - Timeline.prototype.setGroups = function (groups) { - // convert to type DataSet when needed - var newDataSet; - if (!groups) { - newDataSet = null; - } else if (groups instanceof DataSet || groups instanceof DataView) { - newDataSet = groups; - } else { - // turn an array into a dataset - newDataSet = new DataSet(groups); + Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); } - this.groupsData = newDataSet; - this.itemSet.setGroups(newDataSet); + this.frame = document.createElement('div'); + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; + + // create the graph canvas (HTML canvas element) + this.frame.canvas = document.createElement('canvas'); + this.frame.canvas.style.position = 'relative'; + this.frame.appendChild(this.frame.canvas); + //if (!this.frame.canvas.getContext) { + { + var noCanvas = document.createElement('DIV'); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold'; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.frame.canvas.appendChild(noCanvas); + } + + this.frame.filter = document.createElement('div'); + this.frame.filter.style.position = 'absolute'; + this.frame.filter.style.bottom = '0px'; + this.frame.filter.style.left = '0px'; + this.frame.filter.style.width = '100%'; + this.frame.appendChild(this.frame.filter); + + // add event listeners to handle moving and zooming the contents + var me = this; + var onmousedown = function onmousedown(event) { + me._onMouseDown(event); + }; + var ontouchstart = function ontouchstart(event) { + me._onTouchStart(event); + }; + var onmousewheel = function onmousewheel(event) { + me._onWheel(event); + }; + var ontooltip = function ontooltip(event) { + me._onTooltip(event); + }; + // TODO: these events are never cleaned up... can give a 'memory leakage' + + util.addEventListener(this.frame.canvas, 'keydown', onkeydown); + util.addEventListener(this.frame.canvas, 'mousedown', onmousedown); + util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart); + util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel); + util.addEventListener(this.frame.canvas, 'mousemove', ontooltip); + + // add the new graph to the container element + this.containerElement.appendChild(this.frame); }; /** - * Set both items and groups in one go - * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data + * Set a new size for the graph + * @param {string} width Width in pixels or percentage (for example '800px' + * or '50%') + * @param {string} height Height in pixels or percentage (for example '400px' + * or '30%') */ - Timeline.prototype.setData = function (data) { - if (data && data.groups) { - this.setGroups(data.groups); - } + Graph3d.prototype.setSize = function (width, height) { + this.frame.style.width = width; + this.frame.style.height = height; - if (data && data.items) { - this.setItems(data.items); - } + this._resizeCanvas(); }; /** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {string[] | string} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. - * @param {Object} [options] Available options: - * `focus: boolean` - * If true, focus will be set to the selected item(s) - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. - * Only applicable when option focus is true. + * Resize the canvas to the current size of the frame */ - Timeline.prototype.setSelection = function (ids, options) { - this.itemSet && this.itemSet.setSelection(ids); + Graph3d.prototype._resizeCanvas = function () { + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; - if (options && options.focus) { - this.focus(ids, options); - } + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; + + // adjust with for margin + this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px'; }; /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * Start animation */ - Timeline.prototype.getSelection = function () { - return this.itemSet && this.itemSet.getSelection() || []; + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available'; + + this.frame.filter.slider.play(); }; /** - * Adjust the visible window such that the selected item (or multiple items) - * are centered on screen. - * @param {String | String[]} id An item id or array with item ids - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. + * Stop animation */ - Timeline.prototype.focus = function (id, options) { - if (!this.itemsData || id == undefined) return; - - var ids = Array.isArray(id) ? id : [id]; + Graph3d.prototype.animationStop = function () { + if (!this.frame.filter || !this.frame.filter.slider) return; - // get the specified item(s) - var itemsData = this.itemsData.getDataSet().get(ids, { - type: { - start: 'Date', - end: 'Date' - } - }); + this.frame.filter.slider.stop(); + }; - // calculate minimum start and maximum end of specified items - var start = null; - var end = null; - itemsData.forEach(function (itemData) { - var s = itemData.start.valueOf(); - var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); + /** + * Resize the center position based on the current values in this.defaultXCenter + * and this.defaultYCenter (which are strings with a percentage or a value + * in pixels). The center positions are the variables this.xCenter + * and this.yCenter + */ + Graph3d.prototype._resizeCenter = function () { + // calculate the horizontal center position + if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === '%') { + this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; + } else { + this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px + } - if (start === null || s < start) { - start = s; - } + // calculate the vertical center position + if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === '%') { + this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); + } else { + this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px + } + }; - if (end === null || e > end) { - end = e; - } - }); + /** + * Set the rotation and distance of the camera + * @param {Object} pos An object with the camera position. The object + * contains three parameters: + * - horizontal {Number} + * The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * - vertical {Number} + * The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + * - distance {Number} + * The (normalized) distance of the camera to the + * center of the graph, a value between 0.71 and 5.0. + * Optional, can be left undefined. + */ + Graph3d.prototype.setCameraPosition = function (pos) { + if (pos === undefined) { + return; + } - if (start !== null && end !== null) { - // calculate the new middle and interval for the window - var middle = (start + end) / 2; - var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); + } - var animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(middle - interval / 2, middle + interval / 2, animation); + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); } + + this.redraw(); }; /** - * Set Timeline window such that it fits all items - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance */ - Timeline.prototype.fit = function (options) { - var animation = options && options.animation !== undefined ? options.animation : true; - var range = this.getItemRange(); - this.range.setRange(range.min, range.max, animation); + Graph3d.prototype.getCameraPosition = function () { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; }; /** - * Determine the range of the items, taking into account their actual width - * and a margin of 10 pixels on both sides. - * @return {{min: Date | null, max: Date | null}} + * Load data into the 3D Graph */ - Timeline.prototype.getItemRange = function () { - var _this = this; - - // get a rough approximation for the range based on the items start and end dates - var range = this.getDataRange(); - var min = range.min; - var max = range.max; - var minItem = null; - var maxItem = null; - - if (min != null && max != null) { - var interval; - var factor; - var lhs; - var rhs; - var delta; - - (function () { - var getStart = function (item) { - return util.convert(item.data.start, 'Date').valueOf(); - }; - - var getEnd = function (item) { - var end = item.data.end != undefined ? item.data.end : item.data.start; - return util.convert(end, 'Date').valueOf(); - }; - - interval = max - min; - // ms - if (interval <= 0) { - interval = 10; - } - factor = interval / _this.props.center.width; - - // calculate the date of the left side and right side of the items given - util.forEach(_this.itemSet.items, (function (item) { - item.show(); - - var start = getStart(item); - var end = getEnd(item); - - var left = new Date(start - (item.getWidthLeft() + 10) * factor); - var right = new Date(end + (item.getWidthRight() + 10) * factor); - - if (left < min) { - min = left; - minItem = item; - } - if (right > max) { - max = right; - maxItem = item; - } - }).bind(_this)); - - if (minItem && maxItem) { - lhs = minItem.getWidthLeft() + 10; - rhs = maxItem.getWidthRight() + 10; - delta = _this.props.center.width - lhs - rhs; - // px + Graph3d.prototype._readData = function (data) { + // read the data + this._dataInitialize(data, this.style); - if (delta > 0) { - min = getStart(minItem) - lhs * interval / delta; // ms - max = getEnd(maxItem) + rhs * interval / delta; // ms - } - } - })(); + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; + // draw the filter + this._redrawFilter(); }; /** - * Calculate the data range of the items start and end dates - * @returns {{min: Date | null, max: Date | null}} + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data */ - Timeline.prototype.getDataRange = function () { - var min = null; - var max = null; + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); - var dataset = this.itemsData && this.itemsData.getDataSet(); - if (dataset) { - dataset.forEach(function (item) { - var start = util.convert(item.start, 'Date').valueOf(); - var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf(); - if (min === null || start < min) { - min = start; - } - if (max === null || end > max) { - max = start; - } - }); + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); } - - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; }; /** - * Generate Timeline related information from an event - * @param {Event} event - * @return {Object} An object with related information, like on which area - * The event happened, whether clicked on an item, etc. + * Update the options. Options will be merged with current options + * @param {Object} options */ - Timeline.prototype.getEventProperties = function (event) { - var clientX = event.center ? event.center.x : event.clientX; - var clientY = event.center ? event.center.y : event.clientY; - var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); - var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - var item = this.itemSet.itemFromTarget(event); - var group = this.itemSet.groupFromTarget(event); - var customTime = CustomTime.customTimeFromTarget(event); + this.animationStop(); - var snap = this.itemSet.options.snap || null; - var scale = this.body.util.getScale(); - var step = this.body.util.getStep(); - var time = this._toTime(x); - var snappedTime = snap ? snap(time, scale, step) : time; + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; - var element = util.getTarget(event); - var what = null; - if (item != null) { - what = 'item'; - } else if (customTime != null) { - what = 'custom-time'; - } else if (util.hasParent(element, this.timeAxis.dom.foreground)) { - what = 'axis'; - } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { - what = 'axis'; - } else if (util.hasParent(element, this.itemSet.dom.labelSet)) { - what = 'group-label'; - } else if (util.hasParent(element, this.currentTime.bar)) { - what = 'current-time'; - } else if (util.hasParent(element, this.dom.center)) { - what = 'background'; - } + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - return { - event: event, - item: item ? item.id : null, - group: group ? group.groupId : null, - what: what, - pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, - pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, - x: x, - y: y, - time: time, - snappedTime: snappedTime - }; - }; + if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel; + if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel; + if (options.xLabel !== undefined) this.xLabel = options.xLabel; + if (options.yLabel !== undefined) this.yLabel = options.yLabel; + if (options.zLabel !== undefined) this.zLabel = options.zLabel; - module.exports = Timeline; + if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; + if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; + if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; -/***/ }, -/* 27 */ -/***/ function(module, exports, __webpack_require__) { + if (options.style !== undefined) { + var styleNumber = this._getStyleNumber(options.style); + if (styleNumber !== -1) { + this.style = styleNumber; + } + } + if (options.showGrid !== undefined) this.showGrid = options.showGrid; + if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective; + if (options.showShadow !== undefined) this.showShadow = options.showShadow; + if (options.tooltip !== undefined) this.showTooltip = options.tooltip; + if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls; + if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio; + if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio; - 'use strict'; + if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; - var util = __webpack_require__(9); - var Component = __webpack_require__(28); - var moment = __webpack_require__(10); - var locales = __webpack_require__(29); + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - /** - * A current time bar - * @param {{range: Range, dom: Object, domProps: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCurrentTime] - * @constructor CurrentTime - * @extends Component - */ - function CurrentTime(body, options) { - this.body = body; + if (options.xMin !== undefined) this.defaultXMin = options.xMin; + if (options.xStep !== undefined) this.defaultXStep = options.xStep; + if (options.xMax !== undefined) this.defaultXMax = options.xMax; + if (options.yMin !== undefined) this.defaultYMin = options.yMin; + if (options.yStep !== undefined) this.defaultYStep = options.yStep; + if (options.yMax !== undefined) this.defaultYMax = options.yMax; + if (options.zMin !== undefined) this.defaultZMin = options.zMin; + if (options.zStep !== undefined) this.defaultZStep = options.zStep; + if (options.zMax !== undefined) this.defaultZMax = options.zMax; + if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; + if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; - // default options - this.defaultOptions = { - showCurrentTime: true, + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - locales: locales, - locale: 'en' - }; - this.options = util.extend({}, this.defaultOptions); - this.offset = 0; + if (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); + } + + // colors + if (options.axisColor !== undefined) this.axisColor = options.axisColor; + if (options.gridColor !== undefined) this.gridColor = options.gridColor; + if (options.dataColor) { + if (typeof options.dataColor === 'string') { + this.dataColor.fill = options.dataColor; + this.dataColor.stroke = options.dataColor; + } else { + if (options.dataColor.fill) { + this.dataColor.fill = options.dataColor.fill; + } + if (options.dataColor.stroke) { + this.dataColor.stroke = options.dataColor.stroke; + } + if (options.dataColor.strokeWidth !== undefined) { + this.dataColor.strokeWidth = options.dataColor.strokeWidth; + } + } + } + this._setBackgroundColor(options.backgroundColor); + } - this._create(); + this.setSize(this.width, this.height); - this.setOptions(options); - } + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); + } - CurrentTime.prototype = new Component(); + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + }; /** - * Create the HTML DOM for the current time bar - * @private + * Redraw the Graph. */ - CurrentTime.prototype._create = function () { - var bar = document.createElement('div'); - bar.className = 'vis-current-time'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + Graph3d.prototype.redraw = function () { + if (this.dataPoints === undefined) { + throw 'Error: graph data not initialized'; + } - this.bar = bar; - }; + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); - /** - * Destroy the CurrentTime bar - */ - CurrentTime.prototype.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { + this._redrawDataGrid(); + } else if (this.style === Graph3d.STYLE.LINE) { + this._redrawDataLine(); + } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + this._redrawDataBar(); + } else { + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); + } - this.body = null; + this._redrawInfo(); + this._redrawLegend(); }; /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] + * Clear the canvas before redrawing */ - CurrentTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); - } + Graph3d.prototype._redrawClear = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + + ctx.clearRect(0, 0, canvas.width, canvas.height); }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Redraw the legend showing the colors */ - CurrentTime.prototype.redraw = function () { - if (this.options.showCurrentTime) { - var parent = this.body.dom.backgroundVertical; - if (this.bar.parentNode != parent) { - // attach to the dom - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - parent.appendChild(this.bar); + Graph3d.prototype._redrawLegend = function () { + var y; - this.start(); - } + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { - var now = new Date(new Date().valueOf() + this.offset); - var x = this.body.util.toScreen(now); + var dotSize = this.frame.clientWidth * 0.02; - var locale = this.options.locales[this.options.locale]; - if (!locale) { - if (!this.warned) { - console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); - this.warned = true; - } - locale = this.options.locales['en']; // fall back on english when not available + var widthMin, widthMax; + if (this.style === Graph3d.STYLE.DOTSIZE) { + widthMin = dotSize / 2; // px + widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function + } else { + widthMin = 20; // px + widthMax = 20; // px } - var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); - title = title.charAt(0).toUpperCase() + title.substring(1); - this.bar.style.left = x + 'px'; - this.bar.title = title; - } else { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - this.stop(); + var height = Math.max(this.frame.clientHeight * 0.25, 100); + var top = this.margin; + var right = this.frame.clientWidth - this.margin; + var left = right - widthMax; + var bottom = top + height; } - return false; - }; + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + ctx.lineWidth = 1; + ctx.font = '14px arial'; // TODO: put in options - /** - * Start auto refreshing the current time bar - */ - CurrentTime.prototype.start = function () { - var me = this; + if (this.style === Graph3d.STYLE.DOTCOLOR) { + // draw the color bar + var ymin = 0; + var ymax = height; // Todo: make height customizable + for (y = ymin; y < ymax; y++) { + var f = (y - ymin) / (ymax - ymin); - function update() { - me.stop(); + //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function + var hue = f * 240; + var color = this._hsv2rgb(hue, 1, 1); - // determine interval to refresh - var scale = me.body.range.conversion(me.body.domProps.center.width).scale; - var interval = 1 / scale / 10; - if (interval < 30) interval = 30; - if (interval > 1000) interval = 1000; + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } - me.redraw(); + ctx.strokeStyle = this.axisColor; + ctx.strokeRect(left, top, widthMax, height); + } - // start a renderTimer to adjust for the new time - me.currentTimeTimer = setTimeout(update, interval); + if (this.style === Graph3d.STYLE.DOTSIZE) { + // draw border around color bar + ctx.strokeStyle = this.axisColor; + ctx.fillStyle = this.dataColor.fill; + ctx.beginPath(); + ctx.moveTo(left, top); + ctx.lineTo(right, top); + ctx.lineTo(right - widthMax + widthMin, bottom); + ctx.lineTo(left, bottom); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); } - update(); - }; + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + // print values along the color bar + var gridLineLen = 5; // px + var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true); + step.start(); + if (step.getCurrent() < this.valueMin) { + step.next(); + } + while (!step.end()) { + y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; - /** - * Stop auto refreshing the current time bar - */ - CurrentTime.prototype.stop = function () { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); + + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + + step.next(); + } + + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); } }; /** - * Set a current time. This can be used for example to ensure that a client's - * time is synchronized with a shared server time. - * @param {Date | String | Number} time A Date, unix timestamp, or - * ISO date string. + * Redraw the filter */ - CurrentTime.prototype.setCurrentTime = function (time) { - var t = util.convert(time, 'Date').valueOf(); - var now = new Date().valueOf(); - this.offset = t - now; - this.redraw(); - }; + Graph3d.prototype._redrawFilter = function () { + this.frame.filter.innerHTML = ''; - /** - * Get the current time. - * @return {Date} Returns the current time. - */ - CurrentTime.prototype.getCurrentTime = function () { - return new Date(new Date().valueOf() + this.offset); - }; + if (this.dataFilter) { + var options = { + 'visible': this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; - module.exports = CurrentTime; + // TODO: css here is not nice here... + this.frame.filter.style.padding = '10px'; + //this.frame.filter.style.backgroundColor = '#EFEFEF'; -/***/ }, -/* 28 */ -/***/ function(module, exports, __webpack_require__) { + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); - /** - * Prototype for visual components - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] - * @param {Object} [options] - */ - "use strict"; + // create an event handler + var me = this; + var onchange = function onchange() { + var index = slider.getIndex(); - function Component(body, options) { - this.options = null; - this.props = null; - } + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); - /** - * Set options for the component. The new options will be merged into the - * current options. - * @param {Object} options - */ - Component.prototype.setOptions = function (options) { - if (options) { - util.extend(this.options, options); + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } else { + this.frame.filter.slider = undefined; } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Redraw the slider */ - Component.prototype.redraw = function () { - // should be implemented by the component - return false; + Graph3d.prototype._redrawSlider = function () { + if (this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); + } }; /** - * Destroy the component. Cleanup DOM and event listeners - */ - Component.prototype.destroy = function () {}; - - /** - * Test whether the component is resized since the last time _isResized() was - * called. - * @return {Boolean} Returns true if the component is resized - * @protected + * Redraw common information */ - Component.prototype._isResized = function () { - var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; - - return resized; - }; - - module.exports = Component; - - // should be implemented by the component - -/***/ }, -/* 29 */ -/***/ function(module, exports, __webpack_require__) { - - // English - 'use strict'; + Graph3d.prototype._redrawInfo = function () { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); - exports['en'] = { - current: 'current', - time: 'time' - }; - exports['en_EN'] = exports['en']; - exports['en_US'] = exports['en']; + ctx.font = '14px arial'; // TODO: put in options + ctx.lineStyle = 'gray'; + ctx.fillStyle = 'gray'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; - // Dutch - exports['nl'] = { - current: 'huidige', - time: 'tijd' + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); + } }; - exports['nl_NL'] = exports['nl']; - exports['nl_BE'] = exports['nl']; - -/***/ }, -/* 30 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var util = __webpack_require__(9); - var hammerUtil = __webpack_require__(31); - var moment = __webpack_require__(10); - var Component = __webpack_require__(28); - var DateUtil = __webpack_require__(32); /** - * @constructor Range - * A Range controls a numeric range with a start and end value. - * The Range adjusts the range based on mouse events or programmatic changes, - * and triggers events when the range is changing or has been changed. - * @param {{dom: Object, domProps: Object, emitter: Emitter}} body - * @param {Object} [options] See description at Range.setOptions + * Redraw the axis */ - function Range(body, options) { - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add(-3, 'days').valueOf(); // Number - this.end = now.clone().add(4, 'days').valueOf(); // Number - - this.body = body; - this.deltaDifference = 0; - this.scaleOffset = 0; - this.startToFront = false; - this.endToFront = true; + Graph3d.prototype._redrawAxis = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + from, + to, + step, + prettyStep, + text, + xText, + yText, + zText, + offset, + xOffset, + yOffset, + xMin2d, + xMax2d; - // default options - this.defaultOptions = { - start: null, - end: null, - direction: 'horizontal', // 'horizontal' or 'vertical' - moveable: true, - zoomable: true, - min: null, - max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds - }; - this.options = util.extend({}, this.defaultOptions); + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + 'px arial'; - this.props = { - touch: {} - }; - this.animationTimer = null; + // calculate the length for the short grid lines + var gridLenX = 0.025 / this.scale.x; + var gridLenY = 0.025 / this.scale.y; + var textMargin = 5 / this.camera.getArmLength(); // px + var armAngle = this.camera.getArmRotation().horizontal; - // drag listeners for dragging - this.body.emitter.on('panstart', this._onDragStart.bind(this)); - this.body.emitter.on('panmove', this._onDrag.bind(this)); - this.body.emitter.on('panend', this._onDragEnd.bind(this)); + // draw x-grid lines + ctx.lineWidth = 1; + prettyStep = this.defaultXStep === undefined; + step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); + step.start(); + if (step.getCurrent() < this.xMin) { + step.next(); + } + while (!step.end()) { + var x = step.getCurrent(); - // mouse wheel for zooming - this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); + if (this.showGrid) { + from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + ctx.strokeStyle = this.gridColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } else { + from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - // pinch to zoom - this.body.emitter.on('touch', this._onTouch.bind(this)); - this.body.emitter.on('pinch', this._onPinch.bind(this)); + from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - this.setOptions(options); - } + yText = Math.cos(armAngle) > 0 ? this.yMin : this.yMax; + text = this._convert3Dto2D(new Point3d(x, yText, this.zMin)); + if (Math.cos(armAngle * 2) > 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + text.y += textMargin; + } else if (Math.sin(armAngle * 2) < 0) { + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + ctx.fillStyle = this.axisColor; + ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y); - Range.prototype = new Component(); + step.next(); + } - /** - * Set options for the range controller - * @param {Object} options Available options: - * {Number | Date | String} start Start date for the range - * {Number | Date | String} end End date for the range - * {Number} min Minimum value for start - * {Number} max Maximum value for end - * {Number} zoomMin Set a minimum value for - * (end - start). - * {Number} zoomMax Set a maximum value for - * (end - start). - * {Boolean} moveable Enable moving of the range - * by dragging. True by default - * {Boolean} zoomable Enable zooming of the range - * by pinching/scrolling. True by default - */ - Range.prototype.setOptions = function (options) { - if (options) { - // copy the options that we know - var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates']; - util.selectiveExtend(fields, this.options, options); + // draw y-grid lines + ctx.lineWidth = 1; + prettyStep = this.defaultYStep === undefined; + step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); + step.start(); + if (step.getCurrent() < this.yMin) { + step.next(); + } + while (!step.end()) { + if (this.showGrid) { + from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.gridColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } else { + from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - if ('start' in options || 'end' in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); + from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); } - } - }; - /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' - */ - function validateDirection(direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + 'Choose "horizontal" or "vertical".'); + xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax; + text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + text.y += textMargin; + } else if (Math.sin(armAngle * 2) > 0) { + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + ctx.fillStyle = this.axisColor; + ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y); + + step.next(); } - } - /** - * Set a new start and end range - * @param {Date | Number | String} [start] - * @param {Date | Number | String} [end] - * @param {boolean | {duration: number, easingFunction: string}} [animation=false] - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. - * @param {Boolean} [byUser=false] - * - */ - Range.prototype.setRange = function (start, end, animation, byUser) { - if (byUser !== true) { - byUser = false; + // draw z-grid lines and axis + ctx.lineWidth = 1; + prettyStep = this.defaultZStep === undefined; + step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); + step.start(); + if (step.getCurrent() < this.zMin) { + step.next(); } - var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null; - var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null; - this._cancelAnimation(); + xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; + yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; + while (!step.end()) { + // TODO: make z-grid lines really 3d? + from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(from.x - textMargin, from.y); + ctx.stroke(); - if (animation) { - // true or an Object - var me = this; - var initStart = this.start; - var initEnd = this.end; - var duration = typeof animation === 'object' && 'duration' in animation ? animation.duration : 500; - var easingName = typeof animation === 'object' && 'easingFunction' in animation ? animation.easingFunction : 'easeInOutQuad'; - var easingFunction = util.easingFunctions[easingName]; - if (!easingFunction) { - throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' + 'Choose from: ' + Object.keys(util.easingFunctions).join(', ')); - } + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y); - var initTime = new Date().valueOf(); - var anyChanged = false; + step.next(); + } + ctx.lineWidth = 1; + from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - var next = function next() { - if (!me.props.touch.dragging) { - var now = new Date().valueOf(); - var time = now - initTime; - var ease = easingFunction(time / duration); - var done = time > duration; - var s = done || finalStart === null ? finalStart : initStart + (finalStart - initStart) * ease; - var e = done || finalEnd === null ? finalEnd : initEnd + (finalEnd - initEnd) * ease; + // draw x-axis + ctx.lineWidth = 1; + // line at yMin + xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); + xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); + // line at ymax + xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); + xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); - changed = me._applyRange(s, e); - DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); - anyChanged = anyChanged || changed; - if (changed) { - me.body.emitter.emit('rangechange', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); - } + // draw y-axis + ctx.lineWidth = 1; + // line at xMin + from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + // line at xMax + from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); + ctx.strokeStyle = this.axisColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - if (done) { - if (anyChanged) { - me.body.emitter.emit('rangechanged', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); - } - } else { - // animate with as high as possible frame rate, leave 20 ms in between - // each to prevent the browser from blocking - me.animationTimer = setTimeout(next, 20); - } - } - }; + // draw x-label + var xLabel = this.xLabel; + if (xLabel.length > 0) { + yOffset = 0.1 / this.scale.y; + xText = (this.xMin + this.xMax) / 2; + yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset; + text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + if (Math.cos(armAngle * 2) > 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + } else if (Math.sin(armAngle * 2) < 0) { + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + } + ctx.fillStyle = this.axisColor; + ctx.fillText(xLabel, text.x, text.y); + } - return next(); - } else { - var changed = this._applyRange(finalStart, finalEnd); - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - if (changed) { - var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; - this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); + // draw y-label + var yLabel = this.yLabel; + if (yLabel.length > 0) { + xOffset = 0.1 / this.scale.x; + xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset; + yText = (this.yMin + this.yMax) / 2; + text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + } else if (Math.sin(armAngle * 2) > 0) { + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + } else { + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; } + ctx.fillStyle = this.axisColor; + ctx.fillText(yLabel, text.x, text.y); } - }; - /** - * Stop an animation - * @private - */ - Range.prototype._cancelAnimation = function () { - if (this.animationTimer) { - clearTimeout(this.animationTimer); - this.animationTimer = null; + // draw z-label + var zLabel = this.zLabel; + if (zLabel.length > 0) { + offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? + xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; + yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; + zText = (this.zMin + this.zMax) / 2; + text = this._convert3Dto2D(new Point3d(xText, yText, zText)); + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.axisColor; + ctx.fillText(zLabel, text.x - offset, text.y); } }; /** - * Set a new start and end range. This method is the same as setRange, but - * does not trigger a range change and range changed event, and it returns - * true when the range is changed - * @param {Number} [start] - * @param {Number} [end] - * @return {Boolean} changed - * @private + * Calculate the color based on the given value. + * @param {Number} H Hue, a value be between 0 and 360 + * @param {Number} S Saturation, a value between 0 and 1 + * @param {Number} V Value, a value between 0 and 1 */ - Range.prototype._applyRange = function (start, end) { - var newStart = start != null ? util.convert(start, 'Date').valueOf() : this.start, - newEnd = end != null ? util.convert(end, 'Date').valueOf() : this.end, - max = this.options.max != null ? util.convert(this.options.max, 'Date').valueOf() : null, - min = this.options.min != null ? util.convert(this.options.min, 'Date').valueOf() : null, - diff; + Graph3d.prototype._hsv2rgb = function (H, S, V) { + var R, G, B, C, Hi, X; - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); - } + C = V * S; + Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(H / 60 % 2 - 1)); - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; + switch (Hi) { + case 0: + R = C;G = X;B = 0;break; + case 1: + R = X;G = C;B = 0;break; + case 2: + R = 0;G = C;B = X;break; + case 3: + R = 0;G = X;B = C;break; + case 4: + R = X;G = 0;B = C;break; + case 5: + R = C;G = 0;B = X;break; + + default: + R = 0;G = 0;B = 0;break; } - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = min - newStart; - newStart += diff; - newEnd += diff; + return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')'; + }; - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } - } + /** + * Draw all datapoints as a grid + * This function can be used when the style is 'grid' + */ + Graph3d.prototype._redrawDataGrid = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + point, + right, + top, + cross, + i, + topSideVisible, + fillStyle, + strokeStyle, + lineWidth, + h, + s, + v, + zAvg; - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = newEnd - max; - newStart -= diff; - newEnd -= diff; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } - } - } + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if (newEnd - newStart < zoomMin) { - if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } else { - // zoom to the minimum - diff = zoomMin - (newEnd - newStart); - newStart -= diff / 2; - newEnd += diff / 2; - } - } - } + // calculate the translations and screen position of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - if (newEnd - newStart > zoomMax) { - if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } else { - // zoom to the maximum - diff = newEnd - newStart - zoomMax; - newStart += diff / 2; - newEnd -= diff / 2; - } - } + // calculate the translation of the point at the bottom (needed for sorting) + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; } - var changed = this.start != newStart || this.end != newEnd; + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) - if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { - this.body.emitter.emit('checkRangedItems'); - } + if (this.style === Graph3d.STYLE.SURFACE) { + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + cross = this.dataPoints[i].pointCross; - this.start = newStart; - this.end = newEnd; - return changed; - }; + if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { - /** - * Retrieve the current range. - * @return {Object} An object with start and end properties - */ - Range.prototype.getRange = function () { - return { - start: this.start, - end: this.end - }; - }; + if (this.showGrayBottom || this.showShadow) { + // calculate the cross product of the two vectors from center + // to left and right, in order to know whether we are looking at the + // bottom or at the top side. We can also use the cross product + // for calculating light intensity + var aDiff = Point3d.subtract(cross.trans, point.trans); + var bDiff = Point3d.subtract(top.trans, right.trans); + var crossproduct = Point3d.crossProduct(aDiff, bDiff); + var len = crossproduct.length(); + // FIXME: there is a bug with determining the surface side (shadow or colored) - /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.prototype.conversion = function (width, totalHidden) { - return Range.conversion(this.start, this.end, width, totalHidden); - }; + topSideVisible = crossproduct.z > 0; + } else { + topSideVisible = true; + } - /** - * Static method to calculate the conversion offset and scale for a range, - * based on the provided start, end, and width - * @param {Number} start - * @param {Number} end - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.conversion = function (start, end, width, totalHidden) { - if (totalHidden === undefined) { - totalHidden = 0; - } - if (width != 0 && end - start != 0) { - return { - offset: start, - scale: width / (end - start - totalHidden) - }; + if (topSideVisible) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + s = 1; // saturation + + if (this.showShadow) { + v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = fillStyle; + } else { + v = 1; + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = this.axisColor; + } + } else { + fillStyle = 'gray'; + strokeStyle = this.axisColor; + } + + ctx.lineWidth = this._getStrokeWidth(point); + ctx.fillStyle = fillStyle; + ctx.strokeStyle = strokeStyle; + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(right.screen.x, right.screen.y); + ctx.lineTo(cross.screen.x, cross.screen.y); + ctx.lineTo(top.screen.x, top.screen.y); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + } } else { - return { - offset: 0, - scale: 1 - }; - } - }; + // grid style + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; - /** - * Start dragging horizontally or vertically - * @param {Event} event - * @private - */ - Range.prototype._onDragStart = function (event) { - this.deltaDifference = 0; - this.previousDelta = 0; - // only allow dragging when configured as movable - if (!this.options.moveable) return; + if (point !== undefined && right !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + right.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + + ctx.lineWidth = this._getStrokeWidth(point) * 2; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(right.screen.x, right.screen.y); + ctx.stroke(); + } - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + if (point !== undefined && top !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + top.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.dragging = true; + ctx.lineWidth = this._getStrokeWidth(point) * 2; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(top.screen.x, top.screen.y); + ctx.stroke(); + } + } + } + }; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'move'; + Graph3d.prototype._getStrokeWidth = function (point) { + if (point !== undefined) { + if (this.showPerspective) { + return 1 / -point.trans.z * this.dataColor.strokeWidth; + } else { + return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth; + } } + + return this.dataColor.strokeWidth; }; /** - * Perform dragging operation - * @param {Event} event - * @private + * Draw all datapoints as dots. + * This function can be used when the style is 'dot' or 'dot-line' */ - Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + Graph3d.prototype._redrawDataDot = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i; - // TODO: this may be redundant in hammerjs2 - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - var direction = this.options.direction; - validateDirection(direction); - var delta = direction == 'horizontal' ? event.deltaX : event.deltaY; - delta -= this.deltaDifference; - var interval = this.props.touch.end - this.props.touch.start; + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - // normalize dragging speed if cutout is in between. - var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - interval -= duration; + // calculate the distance from the point at the bottom to the camera + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + } - 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; - var newEnd = this.props.touch.end + diffRange; + // order the translated points by depth + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // snapping times away from hidden zones - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); - if (safeStart != newStart || safeEnd != newEnd) { - this.deltaDifference += delta; - this.props.touch.start = safeStart; - this.props.touch.end = safeEnd; - this._onDrag(event); - return; - } + // draw the datapoints as colored circles + var dotSize = this.frame.clientWidth * 0.02; // px + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; - this.previousDelta = delta; - this._applyRange(newStart, newEnd); + if (this.style === Graph3d.STYLE.DOTLINE) { + // draw a vertical line from the bottom to the graph value + //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); + var from = this._convert3Dto2D(point.bottom); + ctx.lineWidth = 1; + ctx.strokeStyle = this.gridColor; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(point.screen.x, point.screen.y); + ctx.stroke(); + } - // fire a rangechange event - this.body.emitter.emit('rangechange', { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); - }; + // calculate radius for the circle + var size; + if (this.style === Graph3d.STYLE.DOTSIZE) { + size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + } else { + size = dotSize; + } - /** - * Stop dragging operation - * @param {event} event - * @private - */ - Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; + } - // TODO: this may be redundant in hammerjs2 - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + var hue, color, borderColor; + if (this.style === Graph3d.STYLE.DOTCOLOR) { + // calculate the color based on the value + hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } else if (this.style === Graph3d.STYLE.DOTSIZE) { + color = this.dataColor.fill; + borderColor = this.dataColor.stroke; + } else { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } - this.props.touch.dragging = false; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'auto'; + // draw the circle + ctx.lineWidth = this._getStrokeWidth(point); + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI * 2, true); + ctx.fill(); + ctx.stroke(); } - - // fire a rangechanged event - this.body.emitter.emit('rangechanged', { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event - * @private + * Draw all datapoints as bars. + * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - Range.prototype._onMouseWheel = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + Graph3d.prototype._redrawDataBar = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i, j, surface, corners; - // retrieve delta - var delta = 0; - if (event.wheelDelta) { - /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { - /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + + // calculate the distance from the point at the bottom to the camera + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; } - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - // perform the zoom action. Delta is normally 1 or -1 + // order the translated points by depth + var sortDepth = function sortDepth(a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - delta / 5; + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + + // draw the datapoints as bars + var xWidth = this.xBarWidth / 2; + var yWidth = this.yBarWidth / 2; + for (i = 0; i < this.dataPoints.length; i++) { + var point = this.dataPoints[i]; + + // determine color + var hue, color, borderColor; + if (this.style === Graph3d.STYLE.BARCOLOR) { + // calculate the color based on the value + hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); + } else if (this.style === Graph3d.STYLE.BARSIZE) { + color = this.dataColor.fill; + borderColor = this.dataColor.stroke; } else { - scale = 1 / (1 + delta / 5); + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240; + color = this._hsv2rgb(hue, 1, 1); + borderColor = this._hsv2rgb(hue, 1, 0.8); } - // calculate center, the date to zoom around - var pointer = getPointer({ x: event.clientX, y: event.clientY }, this.body.dom.center); - var pointerDate = this._pointerToDate(pointer); + // calculate size for the bar + if (this.style === Graph3d.STYLE.BARSIZE) { + xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + } - this.zoom(scale, pointerDate, delta); - } + // calculate all corner points + var me = this; + var point3d = point.point; + var top = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z) }]; + var bottom = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin) }]; - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); - }; + // calculate screen location of the points + top.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); + bottom.forEach(function (obj) { + obj.screen = me._convert3Dto2D(obj.point); + }); - /** - * Start of a touch gesture - * @private - */ - Range.prototype._onTouch = function (event) { - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.allowDragging = true; - this.props.touch.center = null; - this.scaleOffset = 0; - this.deltaDifference = 0; + // create five sides, calculate both corner points and center points + var surfaces = [{ corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point) }, { corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point) }, { corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point) }, { corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point) }, { corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point) }]; + point.surfaces = surfaces; + + // calculate the distance of each of the surface centers to the camera + for (j = 0; j < surfaces.length; j++) { + surface = surfaces[j]; + var transCenter = this._convertPointToTranslation(surface.center); + surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; + // TODO: this dept calculation doesn't work 100% of the cases due to perspective, + // but the current solution is fast/simple and works in 99.9% of all cases + // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + } + + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; + + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = this._getStrokeWidth(point); + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside + for (j = 2; j < surfaces.length; j++) { + surface = surfaces[j]; + corners = surface.corners; + ctx.beginPath(); + ctx.moveTo(corners[3].screen.x, corners[3].screen.y); + ctx.lineTo(corners[0].screen.x, corners[0].screen.y); + ctx.lineTo(corners[1].screen.x, corners[1].screen.y); + ctx.lineTo(corners[2].screen.x, corners[2].screen.y); + ctx.lineTo(corners[3].screen.x, corners[3].screen.y); + ctx.fill(); + ctx.stroke(); + } + } }; /** - * Handle pinch event - * @param {Event} event - * @private + * Draw a line through all datapoints. + * This function can be used when the style is 'line' */ - Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + Graph3d.prototype._redrawDataLine = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext('2d'), + point, + i; - this.props.touch.allowDragging = false; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.center, this.body.dom.center); - } + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); - var scale = 1 / (event.scale + this.scaleOffset); - var centerDate = this._pointerToDate(this.props.touch.center); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + } - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; - // calculate new start and end - var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; - var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; + ctx.lineWidth = this._getStrokeWidth(point); + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + ctx.strokeStyle = this.dataColor.stroke; + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); - // snapping times away from hidden zones - this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times - this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times + // draw the datapoints as colored circles + for (i = 1; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + ctx.lineTo(point.screen.x, point.screen.y); + } - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); - if (safeStart != newStart || safeEnd != newEnd) { - this.props.touch.start = safeStart; - this.props.touch.end = safeEnd; - this.scaleOffset = 1 - event.scale; - newStart = safeStart; - newEnd = safeEnd; + // finish the line + ctx.stroke(); } - - this.setRange(newStart, newEnd, false, true); - - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default }; /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date - * @private + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) */ - Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; - - validateDirection(direction); + Graph3d.prototype._onMouseDown = function (event) { + event = event || window.event; - if (direction == 'horizontal') { - return this.body.util.toTime(pointer.x).valueOf(); - } else { - var height = this.body.domProps.center.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); } - }; - /** - * Get the pointer location relative to the location of the dom element - * @param {{x: Number, y: Number}} touch - * @param {Element} element HTML DOM element - * @return {{x: Number, y: Number}} pointer - * @private - */ - function getPointer(touch, element) { - return { - x: touch.x - util.getAbsoluteLeft(element), - y: touch.y - util.getAbsoluteTop(element) + // only react on left mouse button down + this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; + if (!this.leftButtonDown && !this.touchDown) return; + + // get mouse position (different code for IE and all other browsers) + this.startMouseX = getMouseX(event); + this.startMouseY = getMouseY(event); + + this.startStart = new Date(this.start); + this.startEnd = new Date(this.end); + this.startArmRotation = this.camera.getArmRotation(); + + this.frame.style.cursor = 'move'; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) { + me._onMouseMove(event); }; - } + this.onmouseup = function (event) { + me._onMouseUp(event); + }; + util.addEventListener(document, 'mousemove', me.onmousemove); + util.addEventListener(document, 'mouseup', me.onmouseup); + util.preventDefault(event); + }; /** - * Zoom the range the given scale in or out. Start and end date will - * be adjusted, and the timeline will be redrawn. You can optionally give a - * date around which to zoom. - * For example, try scale = 0.9 or 1.1 - * @param {Number} scale Scaling factor. Values above 1 will zoom out, - * values below 1 will zoom in. - * @param {Number} [center] Value representing a date around which will - * be zoomed. + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the event */ - Range.prototype.zoom = function (scale, center, delta) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + // calculate change in mouse position + var diffX = parseFloat(getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - // calculate new start and end - var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; - var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; - // snapping times away from hidden zones - this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times - this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); - if (safeStart != newStart || safeEnd != newEnd) { - newStart = safeStart; - newEnd = safeEnd; + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + + // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... + // the -0.001 is to take care that the vertical axis is always drawn at the left front corner + if (Math.abs(Math.sin(horizontalNew)) < snapValue) { + horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001; + } + if (Math.abs(Math.cos(horizontalNew)) < snapValue) { + horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; + } + + // snap vertically to nice angles + if (Math.abs(Math.sin(verticalNew)) < snapValue) { + verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; + } + if (Math.abs(Math.cos(verticalNew)) < snapValue) { + verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; } - this.setRange(newStart, newEnd, false, true); + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); + + util.preventDefault(event); }; /** - * Move the range with a given delta to the left or right. Start and end - * value will be adjusted. For example, try delta = 0.1 or -0.1 - * @param {Number} delta Moving amount. Positive value will move right, - * negative value will move left + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event */ - Range.prototype.move = function (delta) { - // zoom start Date and end Date relative to the centerDate - var diff = this.end - this.start; - - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; - - // TODO: reckon with min and max range + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + this.leftButtonDown = false; - this.start = newStart; - this.end = newEnd; + // remove event listeners here + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point + * @param {Event} event A mouse move event */ - Range.prototype.moveTo = function (moveTo) { - var center = (this.start + this.end) / 2; - - var diff = center - moveTo; - - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; - - this.setRange(newStart, newEnd); - }; - - module.exports = Range; - -/***/ }, -/* 31 */ -/***/ function(module, exports, __webpack_require__) { + Graph3d.prototype._onTooltip = function (event) { + var delay = 300; // ms + var boundingRect = this.frame.getBoundingClientRect(); + var mouseX = getMouseX(event) - boundingRect.left; + var mouseY = getMouseY(event) - boundingRect.top; - 'use strict'; + if (!this.showTooltip) { + return; + } - var Hammer = __webpack_require__(5); + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); + } - /** - * Register a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) - */ - exports.onTouch = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFirst && !isTouching) { - callback(event); + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; + } - isTouching = true; - setTimeout(function () { - isTouching = false; - }, 0); + if (this.tooltip && this.tooltip.dataPoint) { + // tooltip is currently visible + var dataPoint = this._dataPointFromXY(mouseX, mouseY); + if (dataPoint !== this.tooltip.dataPoint) { + // datapoint changed + if (dataPoint) { + this._showTooltip(dataPoint); + } else { + this._hideTooltip(); + } } - }; + } else { + // tooltip is currently not visible + var me = this; + this.tooltipTimeout = setTimeout(function () { + me.tooltipTimeout = null; - hammer.on('hammer.input', callback.inputHandler); + // show a tooltip if we have a data point + var dataPoint = me._dataPointFromXY(mouseX, mouseY); + if (dataPoint) { + me._showTooltip(dataPoint); + } + }, delay); + } }; - // isTouching is true while a touch action is being emitted - // this is a hack to prevent `touch` from being fired twice - var isTouching = false; - /** - * Register a release event, taking place after a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Event handler for touchstart event on mobile devices */ - exports.onRelease = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFinal && !isReleasing) { - callback(event); + Graph3d.prototype._onTouchStart = function (event) { + this.touchDown = true; - isReleasing = true; - setTimeout(function () { - isReleasing = false; - }, 0); - } + var me = this; + this.ontouchmove = function (event) { + me._onTouchMove(event); }; + this.ontouchend = function (event) { + me._onTouchEnd(event); + }; + util.addEventListener(document, 'touchmove', me.ontouchmove); + util.addEventListener(document, 'touchend', me.ontouchend); - return hammer.on('hammer.input', callback.inputHandler); + this._onMouseDown(event); }; - // isReleasing is true while a release action is being emitted - // this is a hack to prevent `release` from being fired twice - var isReleasing = false; - /** - * Unregister a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Event handler for touchmove event on mobile devices */ - exports.offTouch = function (hammer, callback) { - hammer.off('hammer.input', callback.inputHandler); + Graph3d.prototype._onTouchMove = function (event) { + this._onMouseMove(event); }; /** - * Unregister a release event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Event handler for touchend event on mobile devices */ - exports.offRelease = exports.offTouch; - -/***/ }, -/* 32 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; + Graph3d.prototype._onTouchEnd = function (event) { + this.touchDown = false; - var moment = __webpack_require__(10); + util.removeEventListener(document, 'touchmove', this.ontouchmove); + util.removeEventListener(document, 'touchend', this.ontouchend); - /** - * used in Core to convert the options into a volatile variable - * - * @param Core - */ - exports.convertHiddenOptions = function (body, hiddenDates) { - body.hiddenDates = []; - if (hiddenDates) { - if (Array.isArray(hiddenDates) == true) { - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat === undefined) { - var dateItem = {}; - dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); - dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); - body.hiddenDates.push(dateItem); - } - } - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time - } - } + this._onMouseUp(event); }; /** - * create new entrees for the repeating hidden dates - * @param body - * @param hiddenDates + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event */ - exports.updateHiddenDates = function (body, hiddenDates) { - if (hiddenDates && body.domProps.centerContainer.width !== undefined) { - exports.convertHiddenOptions(body, hiddenDates); - - var start = moment(body.range.start); - var end = moment(body.range.end); - - var totalRange = body.range.end - body.range.start; - var pixelTime = totalRange / body.domProps.centerContainer.width; - - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat !== undefined) { - var startDate = moment(hiddenDates[i].start); - var endDate = moment(hiddenDates[i].end); - - if (startDate._d == "Invalid Date") { - throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); - } - if (endDate._d == "Invalid Date") { - throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); - } - - var duration = endDate - startDate; - if (duration >= 4 * pixelTime) { - - var offset = 0; - var runUntil = end.clone(); - switch (hiddenDates[i].repeat) { - case "daily": - // case of time - if (startDate.day() != endDate.day()) { - offset = 1; - } - startDate.dayOfYear(start.dayOfYear()); - startDate.year(start.year()); - startDate.subtract(7, "days"); - - endDate.dayOfYear(start.dayOfYear()); - endDate.year(start.year()); - endDate.subtract(7 - offset, "days"); - - runUntil.add(1, "weeks"); - break; - case "weekly": - var dayOffset = endDate.diff(startDate, "days"); - var day = startDate.day(); - - // set the start date to the range.start - startDate.date(start.date()); - startDate.month(start.month()); - startDate.year(start.year()); - endDate = startDate.clone(); + Graph3d.prototype._onWheel = function (event) { + if (!event) /* For IE. */ + event = window.event; - // force - startDate.day(day); - endDate.day(day); - endDate.add(dayOffset, "days"); + // retrieve delta + var delta = 0; + if (event.wheelDelta) { + /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { + /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; + } - startDate.subtract(1, "weeks"); - endDate.subtract(1, "weeks"); + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + var oldLength = this.camera.getArmLength(); + var newLength = oldLength * (1 - delta / 10); - runUntil.add(1, "weeks"); - break; - case "monthly": - if (startDate.month() != endDate.month()) { - offset = 1; - } - startDate.month(start.month()); - startDate.year(start.year()); - startDate.subtract(1, "months"); + this.camera.setArmLength(newLength); + this.redraw(); - endDate.month(start.month()); - endDate.year(start.year()); - endDate.subtract(1, "months"); - endDate.add(offset, "months"); + this._hideTooltip(); + } - runUntil.add(1, "months"); - break; - case "yearly": - if (startDate.year() != endDate.year()) { - offset = 1; - } - startDate.year(start.year()); - startDate.subtract(1, "years"); - endDate.year(start.year()); - endDate.subtract(1, "years"); - endDate.add(offset, "years"); + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); - runUntil.add(1, "years"); - break; - default: - console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); - return; - } - while (startDate < runUntil) { - body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); - switch (hiddenDates[i].repeat) { - case "daily": - startDate.add(1, "days"); - endDate.add(1, "days"); - break; - case "weekly": - startDate.add(1, "weeks"); - endDate.add(1, "weeks"); - break; - case "monthly": - startDate.add(1, "months"); - endDate.add(1, "months"); - break; - case "yearly": - startDate.add(1, "y"); - endDate.add(1, "y"); - break; - default: - console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); - return; - } - } - body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); - } - } - } - // remove duplicates, merge where possible - exports.removeDuplicates(body); - // ensure the new positions are not on hidden dates - var startHidden = exports.isHidden(body.range.start, body.hiddenDates); - var endHidden = exports.isHidden(body.range.end, body.hiddenDates); - var rangeStart = body.range.start; - var rangeEnd = body.range.end; - if (startHidden.hidden == true) { - rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; - } - if (endHidden.hidden == true) { - rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; - } - if (startHidden.hidden == true || endHidden.hidden == true) { - body.range._applyRange(rangeStart, rangeEnd); - } - } + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here.. + util.preventDefault(event); }; /** - * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. - * Scales with N^2 - * @param body + * Test whether a point lies inside given 2D triangle + * @param {Point2d} point + * @param {Point2d[]} triangle + * @return {boolean} Returns true if given point lies inside or on the edge of the triangle + * @private */ - exports.removeDuplicates = function (body) { - var hiddenDates = body.hiddenDates; - var safeDates = []; - for (var i = 0; i < hiddenDates.length; i++) { - for (var j = 0; j < hiddenDates.length; j++) { - if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { - // j inside i - if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[j].remove = true; - } - // j start inside i - else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { - hiddenDates[i].end = hiddenDates[j].end; - hiddenDates[j].remove = true; - } - // j end inside i - else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[i].start = hiddenDates[j].start; - hiddenDates[j].remove = true; - } - } - } - } + Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].remove !== true) { - safeDates.push(hiddenDates[i]); - } + function sign(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; } - body.hiddenDates = safeDates; - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time - }; + var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x)); + var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x)); + var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)); - exports.printDates = function (dates) { - for (var i = 0; i < dates.length; i++) { - console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); - } + // each of the three signs must be either equal to each other or zero + return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs); }; /** - * Used in TimeStep to avoid the hidden times. - * @param timeStep - * @param previousTime + * Find a data point close to given screen position (x, y) + * @param {Number} x + * @param {Number} y + * @return {Object | null} The closest data point or null if not close to any data point + * @private */ - exports.stepOverHiddenDates = function (timeStep, previousTime) { - var stepInHidden = false; - var currentValue = timeStep.current.valueOf(); - for (var i = 0; i < timeStep.hiddenDates.length; i++) { - var startDate = timeStep.hiddenDates[i].start; - var endDate = timeStep.hiddenDates[i].end; - if (currentValue >= startDate && currentValue < endDate) { - stepInHidden = true; - break; - } - } - - if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { - var prevValue = moment(previousTime); - var newValue = moment(endDate); - //check if the next step should be major - if (prevValue.year() != newValue.year()) { - timeStep.switchedYear = true; - } else if (prevValue.month() != newValue.month()) { - timeStep.switchedMonth = true; - } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { - timeStep.switchedDay = true; - } - - timeStep.current = newValue.toDate(); - } - }; - - ///** - // * Used in TimeStep to avoid the hidden times. - // * @param timeStep - // * @param previousTime - // */ - //exports.checkFirstStep = function(timeStep) { - // var stepInHidden = false; - // var currentValue = timeStep.current.valueOf(); - // for (var i = 0; i < timeStep.hiddenDates.length; i++) { - // var startDate = timeStep.hiddenDates[i].start; - // var endDate = timeStep.hiddenDates[i].end; - // if (currentValue >= startDate && currentValue < endDate) { - // stepInHidden = true; - // break; - // } - // } - // - // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) { - // var newValue = moment(endDate); - // timeStep.current = newValue.toDate(); - // } - //}; + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, + // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); - /** - * replaces the Core toScreen methods - * @param Core - * @param time - * @param width - * @returns {number} - */ - exports.toScreen = function (Core, time, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return (time.valueOf() - conversion.offset) * conversion.scale; - } else { - var hidden = exports.isHidden(time, Core.body.hiddenDates); - if (hidden.hidden == true) { - time = hidden.startDate; + if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + // the data points are ordered from far away to closest + for (i = this.dataPoints.length - 1; i >= 0; i--) { + dataPoint = this.dataPoints[i]; + var surfaces = dataPoint.surfaces; + if (surfaces) { + for (var s = surfaces.length - 1; s >= 0; s--) { + // split each surface in two triangles, and see if the center point is inside one of these + var surface = surfaces[s]; + var corners = surface.corners; + var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen]; + var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen]; + if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) { + // return immediately at the first hit + return dataPoint; + } + } + } } + } else { + // find the closest data point, using distance to the center of the point on 2d screen + for (i = 0; i < this.dataPoints.length; i++) { + dataPoint = this.dataPoints[i]; + var point = dataPoint.screen; + if (point) { + var distX = Math.abs(x - point.x); + var distY = Math.abs(y - point.y); + var dist = Math.sqrt(distX * distX + distY * distY); - var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); - - var conversion = Core.range.conversion(width, duration); - return (time.valueOf() - conversion.offset) * conversion.scale; + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } + } + } } + + return closestDataPoint; }; /** - * Replaces the core toTime methods - * @param body - * @param range - * @param x - * @param width - * @returns {Date} + * Display a tooltip for given data point + * @param {Object} dataPoint + * @private */ - exports.toTime = function (Core, x, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return new Date(x / conversion.scale + conversion.offset); - } else { - var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - var totalDuration = Core.range.end - Core.range.start - hiddenDuration; - var partialDuration = totalDuration * x / width; - var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); - return newTime; + if (!this.tooltip) { + content = document.createElement('div'); + content.style.position = 'absolute'; + content.style.padding = '10px'; + content.style.border = '1px solid #4d4d4d'; + content.style.color = '#1a1a1a'; + content.style.background = 'rgba(255,255,255,0.7)'; + content.style.borderRadius = '2px'; + content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)'; + + line = document.createElement('div'); + line.style.position = 'absolute'; + line.style.height = '40px'; + line.style.width = '0'; + line.style.borderLeft = '1px solid #4d4d4d'; + + dot = document.createElement('div'); + dot.style.position = 'absolute'; + dot.style.height = '0'; + dot.style.width = '0'; + dot.style.border = '5px solid #4d4d4d'; + dot.style.borderRadius = '5px'; + + this.tooltip = { + dataPoint: null, + dom: { + content: content, + line: line, + dot: dot + } + }; + } else { + content = this.tooltip.dom.content; + line = this.tooltip.dom.line; + dot = this.tooltip.dom.dot; } - }; - /** - * Support function - * - * @param hiddenDates - * @param range - * @returns {number} - */ - exports.getHiddenDurationBetween = function (hiddenDates, start, end) { - var duration = 0; - 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 >= start && endDate < end) { - duration += endDate - startDate; - } + this._hideTooltip(); + + this.tooltip.dataPoint = dataPoint; + if (typeof this.showTooltip === 'function') { + content.innerHTML = this.showTooltip(dataPoint.point); + } else { + content.innerHTML = '' + '' + '' + '' + '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; } - return duration; + + content.style.left = '0'; + content.style.top = '0'; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); + + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; + + var left = dataPoint.screen.x - contentWidth / 2; + left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + + line.style.left = dataPoint.screen.x + 'px'; + line.style.top = dataPoint.screen.y - lineHeight + 'px'; + content.style.left = left + 'px'; + content.style.top = dataPoint.screen.y - lineHeight - contentHeight + 'px'; + dot.style.left = dataPoint.screen.x - dotWidth / 2 + 'px'; + dot.style.top = dataPoint.screen.y - dotHeight / 2 + 'px'; }; /** - * Support function - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} + * Hide the tooltip when displayed + * @private */ - exports.correctTimeForHidden = function (hiddenDates, range, time) { - time = moment(time).toDate().valueOf(); - time -= exports.getHiddenDurationBefore(hiddenDates, range, time); - return time; - }; - - exports.getHiddenDurationBefore = function (hiddenDates, range, time) { - var timeOffset = 0; - time = moment(time).toDate().valueOf(); + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; - 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) { - if (time >= endDate) { - timeOffset += endDate - startDate; + for (var prop in this.tooltip.dom) { + if (this.tooltip.dom.hasOwnProperty(prop)) { + var elem = this.tooltip.dom[prop]; + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } } } } - return timeOffset; }; + /**--------------------------------------------------------------------------**/ + /** - * sum the duration from start to finish, including the hidden duration, - * until the required amount has been reached, return the accumulated hidden duration - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x */ - 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; - }; + function getMouseX(event) { + if ('clientX' in event) return event.clientX; + return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + } /** - * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true - * @param hiddenDates - * @param time - * @param direction - * @param correctionEnabled - * @returns {*} + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse y */ - exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { - var isHidden = exports.isHidden(time, hiddenDates); - if (isHidden.hidden == true) { - if (direction < 0) { - if (correctionEnabled == true) { - return isHidden.startDate - (isHidden.endDate - time) - 1; - } else { - return isHidden.startDate - 1; - } - } else { - if (correctionEnabled == true) { - return isHidden.endDate + (time - isHidden.startDate) + 1; - } else { - return isHidden.endDate + 1; - } - } - } else { - return time; - } - }; + function getMouseY(event) { + if ('clientY' in event) return event.clientY; + return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + } + + module.exports = Graph3d; + + // use use defaults + +/***/ }, +/* 24 */ +/***/ function(module, exports, __webpack_require__) { /** - * Check if a time is hidden - * - * @param time - * @param hiddenDates - * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} + * @prototype Point2d + * @param {Number} [x] + * @param {Number} [y] */ - exports.isHidden = function (time, hiddenDates) { - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; + "use strict"; - if (time >= startDate && time < endDate) { - // if the start is entering a hidden zone - return { hidden: true, startDate: startDate, endDate: endDate }; - break; - } - } - return { hidden: false, startDate: startDate, endDate: endDate }; - }; + function Point2d(x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + } + + module.exports = Point2d; /***/ }, -/* 33 */ +/* 25 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + + /** + * Expose `Emitter`. + */ - var Emitter = __webpack_require__(20); - var Hammer = __webpack_require__(5); - var hammerUtil = __webpack_require__(31); - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - var Range = __webpack_require__(30); - var ItemSet = __webpack_require__(34); - var TimeAxis = __webpack_require__(42); - var Activator = __webpack_require__(43); - var DateUtil = __webpack_require__(32); - var CustomTime = __webpack_require__(45); + module.exports = Emitter; /** - * Create a timeline visualization - * @constructor + * Initialize a new `Emitter`. + * + * @api public */ - function Core() {} - // turn Core into an event emitter - Emitter(Core.prototype); + function Emitter(obj) { + if (obj) return mixin(obj); + }; /** - * Create the main DOM for the Core: a root panel containing left, right, - * top, bottom, content, and background panel. - * @param {Element} container The container element where the Core will - * be attached. - * @protected + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private */ - Core.prototype._create = function (container) { - this.dom = {}; - this.dom.container = container; + function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; + } - this.dom.root = document.createElement('div'); - this.dom.background = document.createElement('div'); - this.dom.backgroundVertical = document.createElement('div'); - this.dom.backgroundHorizontal = document.createElement('div'); - this.dom.centerContainer = document.createElement('div'); - this.dom.leftContainer = document.createElement('div'); - this.dom.rightContainer = document.createElement('div'); - this.dom.center = document.createElement('div'); - this.dom.left = document.createElement('div'); - this.dom.right = document.createElement('div'); - this.dom.top = document.createElement('div'); - this.dom.bottom = document.createElement('div'); - this.dom.shadowTop = document.createElement('div'); - this.dom.shadowBottom = document.createElement('div'); - this.dom.shadowTopLeft = document.createElement('div'); - this.dom.shadowBottomLeft = document.createElement('div'); - this.dom.shadowTopRight = document.createElement('div'); - this.dom.shadowBottomRight = document.createElement('div'); + /** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - this.dom.root.className = 'vis-timeline'; - this.dom.background.className = 'vis-panel vis-background'; - this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical'; - this.dom.backgroundHorizontal.className = 'vis-panel vis-background vis-horizontal'; - this.dom.centerContainer.className = 'vis-panel vis-center'; - this.dom.leftContainer.className = 'vis-panel vis-left'; - this.dom.rightContainer.className = 'vis-panel vis-right'; - this.dom.top.className = 'vis-panel vis-top'; - this.dom.bottom.className = 'vis-panel vis-bottom'; - this.dom.left.className = 'vis-content'; - this.dom.center.className = 'vis-content'; - this.dom.right.className = 'vis-content'; - this.dom.shadowTop.className = 'vis-shadow vis-top'; - this.dom.shadowBottom.className = 'vis-shadow vis-bottom'; - this.dom.shadowTopLeft.className = 'vis-shadow vis-top'; - this.dom.shadowBottomLeft.className = 'vis-shadow vis-bottom'; - this.dom.shadowTopRight.className = 'vis-shadow vis-top'; - this.dom.shadowBottomRight.className = 'vis-shadow vis-bottom'; + Emitter.prototype.on = + Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; + }; - this.dom.root.appendChild(this.dom.background); - this.dom.root.appendChild(this.dom.backgroundVertical); - this.dom.root.appendChild(this.dom.backgroundHorizontal); - this.dom.root.appendChild(this.dom.centerContainer); - this.dom.root.appendChild(this.dom.leftContainer); - this.dom.root.appendChild(this.dom.rightContainer); - this.dom.root.appendChild(this.dom.top); - this.dom.root.appendChild(this.dom.bottom); + /** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); + Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; - this.dom.centerContainer.appendChild(this.dom.shadowTop); - this.dom.centerContainer.appendChild(this.dom.shadowBottom); - this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); - this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); - this.dom.rightContainer.appendChild(this.dom.shadowTopRight); - this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); + function on() { + self.off(event, on); + fn.apply(this, arguments); + } - this.on('rangechange', this.redraw.bind(this)); - this.on('touch', this._onTouch.bind(this)); - this.on('pan', this._onDrag.bind(this)); + on.fn = fn; + this.on(event, on); + return this; + }; - var me = this; - this.on('change', function (properties) { - if (properties && properties.queue == true) { - // redraw once on next tick - if (!me._redrawTimer) { - me._redrawTimer = setTimeout(function () { - me._redrawTimer = null; - me._redraw(); - }, 0); - } - } else { - // redraw immediately - me._redraw(); - } - }); + /** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - // create event listeners for all interesting events, these events will be - // emitted via emitter - this.hammer = new Hammer(this.dom.root); - this.hammer.get('pinch').set({ enable: true }); - this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. - this.listeners = {}; + Emitter.prototype.off = + Emitter.prototype.removeListener = + Emitter.prototype.removeAllListeners = + Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; - var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend' - // TODO: cleanup - //'touch', 'pinch', - //'tap', 'doubletap', 'hold', - //'dragstart', 'drag', 'dragend', - //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (type) { - var listener = function listener(event) { - if (me.isActive()) { - me.emit(type, event); - } - }; - me.hammer.on(type, listener); - me.listeners[type] = listener; - }); + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } - // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) - hammerUtil.onTouch(this.hammer, (function (event) { - me.emit('touch', event); - }).bind(this)); + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; - // emulate a release event (emitted after a pan, pinch, tap, or press) - hammerUtil.onRelease(this.hammer, (function (event) { - me.emit('release', event); - }).bind(this)); + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } - function onMouseWheel(event) { - if (me.isActive()) { - me.emit('mousewheel', event); + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; } } - this.dom.root.addEventListener('mousewheel', onMouseWheel); - this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); - - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; + return this; + }; - this.customTimes = []; + /** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ - // store state information needed for touch events - this.touch = {}; + Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; - this.redrawCount = 0; + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } - // attach the root panel to the provided container - if (!container) throw new Error('No container provided'); - container.appendChild(this.dom.root); + return this; }; /** - * Set options. Options will be passed to all components loaded in the Timeline. - * @param {Object} [options] - * {String} orientation - * Vertical orientation for the Timeline, - * can be 'bottom' (default) or 'top'. - * {String | Number} width - * Width for the timeline, a number in pixels or - * a css string like '1000px' or '75%'. '100%' by default. - * {String | Number} height - * Fixed height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. If undefined, - * The Timeline will automatically size such that - * its contents fit. - * {String | Number} minHeight - * Minimum height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. - * {String | Number} maxHeight - * Maximum height for the Timeline, a number in pixels or - * a css string like '400px' or '75%'. - * {Number | Date | String} start - * Start date for the visible window - * {Number | Date | String} end - * End date for the visible window + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public */ - Core.prototype.setOptions = function (options) { - if (options) { - // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates']; - util.selectiveExtend(fields, this.options, options); - if ('orientation' in options) { - if (typeof options.orientation === 'string') { - this.options.orientation = { - item: options.orientation, - axis: options.orientation - }; - } else if (typeof options.orientation === 'object') { - if ('item' in options.orientation) { - this.options.orientation.item = options.orientation.item; - } - if ('axis' in options.orientation) { - this.options.orientation.axis = options.orientation.axis; - } - } - } + Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; + }; - if (this.options.orientation.axis === 'both') { - if (!this.timeAxis2) { - var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body); - timeAxis2.setOptions = function (options) { - var _options = options ? util.extend({}, options) : {}; - _options.orientation = 'top'; // override the orientation option, always top - TimeAxis.prototype.setOptions.call(timeAxis2, _options); - }; - this.components.push(timeAxis2); - } - } else { - if (this.timeAxis2) { - var index = this.components.indexOf(this.timeAxis2); - if (index !== -1) { - this.components.splice(index, 1); - } - this.timeAxis2.destroy(); - this.timeAxis2 = null; - } - } + /** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ - // if the graph2d's drawPoints is a function delegate the callback to the onRender property - if (typeof options.drawPoints == 'function') { - options.drawPoints = { - onRender: options.drawPoints - }; - } + Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; + }; - if ('hiddenDates' in this.options) { - DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); - } - if ('clickToUse' in options) { - if (options.clickToUse) { - if (!this.activator) { - this.activator = new Activator(this.dom.root); - } - } else { - if (this.activator) { - this.activator.destroy(); - delete this.activator; - } - } - } +/***/ }, +/* 26 */ +/***/ function(module, exports, __webpack_require__) { - if ('showCustomTime' in options) { - throw new Error('Option `showCustomTime` is deprecated. Create a custom time bar via timeline.addCustomTime(time [, id])'); - } + /** + * @prototype Point3d + * @param {Number} [x] + * @param {Number} [y] + * @param {Number} [z] + */ + "use strict"; - // enable/disable autoResize - this._initAutoResize(); - } + function Point3d(x, y, z) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + this.z = z !== undefined ? z : 0; + }; - // propagate options to all components - this.components.forEach(function (component) { - return component.setOptions(options); - }); + /** + * Subtract the two provided points, returns a-b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a-b + */ + Point3d.subtract = function (a, b) { + var sub = new Point3d(); + sub.x = a.x - b.x; + sub.y = a.y - b.y; + sub.z = a.z - b.z; + return sub; + }; - // enable/disable configure - if ('configure' in options) { - if (!this.configurator) { - this.configurator = this._createConfigurator(); - } + /** + * Add the two provided points, returns a+b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a+b + */ + Point3d.add = function (a, b) { + var sum = new Point3d(); + sum.x = a.x + b.x; + sum.y = a.y + b.y; + sum.z = a.z + b.z; + return sum; + }; - this.configurator.setOptions(options.configure); + /** + * Calculate the average of two 3d points + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} The average, (a+b)/2 + */ + Point3d.avg = function (a, b) { + return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); + }; - // collect the settings of all components, and pass them to the configuration system - var appliedOptions = util.deepExtend({}, this.options); - this.components.forEach(function (component) { - util.deepExtend(appliedOptions, component.options); - }); - this.configurator.setModuleOptions({ global: appliedOptions }); - } + /** + * Calculate the cross product of the two provided points, returns axb + * Documentation: http://en.wikipedia.org/wiki/Cross_product + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} cross product axb + */ + Point3d.crossProduct = function (a, b) { + var crossproduct = new Point3d(); - // redraw everything - this._redraw(); + crossproduct.x = a.y * b.z - a.z * b.y; + crossproduct.y = a.z * b.x - a.x * b.z; + crossproduct.z = a.x * b.y - a.y * b.x; + + return crossproduct; }; /** - * Returns true when the Timeline is active. - * @returns {boolean} + * Rtrieve the length of the vector (or the distance from this point to the origin + * @return {Number} length */ - Core.prototype.isActive = function () { - return !this.activator || this.activator.active; + Point3d.prototype.length = function () { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); }; + module.exports = Point3d; + +/***/ }, +/* 27 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var Point3d = __webpack_require__(26); + /** - * Destroy the Core, clean up all DOM elements and event listeners. + * @class Camera + * The camera is mounted on a (virtual) camera arm. The camera arm can rotate + * The camera is always looking in the direction of the origin of the arm. + * This way, the camera always rotates around one fixed point, the location + * of the camera arm. + * + * Documentation: + * http://en.wikipedia.org/wiki/3D_projection */ - Core.prototype.destroy = function () { - // unbind datasets - this.setItems(null); - this.setGroups(null); + function Camera() { + this.armLocation = new Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; - // remove all event listeners - this.off(); + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - // stop checking for changed size - this._stopAutoResize(); + this.calculateCameraOrientation(); + } - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); + /** + * Set the location (origin) of the arm + * @param {Number} x Normalized value of x + * @param {Number} y Normalized value of y + * @param {Number} z Normalized value of z + */ + Camera.prototype.setArmLocation = function (x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; + + this.calculateCameraOrientation(); + }; + + /** + * Set the rotation of the camera arm + * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + */ + Camera.prototype.setArmRotation = function (horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; } - this.dom = null; - // remove Activator - if (this.activator) { - this.activator.destroy(); - delete this.activator; + if (vertical !== undefined) { + this.armRotation.vertical = vertical; + if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; + if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI; } - // cleanup hammer touch events - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - delete this.listeners[event]; - } + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); } - this.listeners = null; - this.hammer = null; + }; - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - return component.destroy(); - }); + /** + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical + */ + Camera.prototype.getArmRotation = function () { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; - this.body = null; + return rot; }; /** - * Set a custom time bar - * @param {Date} time - * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted. + * Set the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 */ - Core.prototype.setCustomTime = function (time, id) { - var customTimes = this.customTimes.filter(function (component) { - return id === component.options.id; - }); + Camera.prototype.setArmLength = function (length) { + if (length === undefined) return; - if (customTimes.length === 0) { - throw new Error('No custom time bar found with id ' + JSON.stringify(id)); - } + this.armLength = length; - if (customTimes.length > 0) { - customTimes[0].setCustomTime(time); - } + // Radius must be larger than the corner of the graph, + // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the + // graph + if (this.armLength < 0.71) this.armLength = 0.71; + if (this.armLength > 5) this.armLength = 5; + + this.calculateCameraOrientation(); }; /** - * Retrieve the current custom time. - * @param {number} [id=undefined] Id of the custom time bar. - * @return {Date | undefined} customTime + * Retrieve the arm length + * @return {Number} length */ - Core.prototype.getCustomTime = function (id) { - var customTimes = this.customTimes.filter(function (component) { - return component.options.id === id; - }); + Camera.prototype.getArmLength = function () { + return this.armLength; + }; - if (customTimes.length === 0) { - throw new Error('No custom time bar found with id ' + JSON.stringify(id)); - } - return customTimes[0].getCustomTime(); + /** + * Retrieve the camera location + * @return {Point3d} cameraLocation + */ + Camera.prototype.getCameraLocation = function () { + return this.cameraLocation; }; /** - * Add custom vertical bar - * @param {Date | String | Number} [time] A Date, unix timestamp, or - * ISO date string. Time point where - * the new bar should be placed. - * If not provided, `new Date()` will - * be used. - * @param {Number | String} [id=undefined] Id of the new bar. Optional - * @return {Number | String} Returns the id of the new bar + * Retrieve the camera rotation + * @return {Point3d} cameraRotation */ - Core.prototype.addCustomTime = function (time, id) { - var timestamp = time !== undefined ? util.convert(time, 'Date').valueOf() : new Date(); + Camera.prototype.getCameraRotation = function () { + return this.cameraRotation; + }; - var exists = this.customTimes.some(function (customTime) { - return customTime.options.id === id; - }); - if (exists) { - throw new Error('A custom time with id ' + JSON.stringify(id) + ' already exists'); - } + /** + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm + */ + Camera.prototype.calculateCameraOrientation = function () { + // calculate location of the camera + this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); - var customTime = new CustomTime(this.body, { - time: timestamp, - id: id - }); + // calculate rotation of the camera + this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; + this.cameraRotation.y = 0; + this.cameraRotation.z = -this.armRotation.horizontal; + }; - this.customTimes.push(customTime); - this.components.push(customTime); - this.redraw(); + module.exports = Camera; - return id; - }; +/***/ }, +/* 28 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var DataView = __webpack_require__(22); /** - * Remove previously added custom bar - * @param {int} id ID of the custom bar to be removed - * @return {boolean} True if the bar exists and is removed, false otherwise + * @class Filter + * + * @param {DataSet} data The google data table + * @param {Number} column The index of the column to be filtered + * @param {Graph} graph The graph */ - Core.prototype.removeCustomTime = function (id) { - var customTimes = this.customTimes.filter(function (bar) { - return bar.options.id === id; + function Filter(data, column, graph) { + this.data = data; + this.column = column; + this.graph = graph; // the parent graph + + this.index = undefined; + this.value = undefined; + + // read all distinct values and select the first one + this.values = graph.getDistinctValues(data.get(), this.column); + + // sort both numeric and string values correctly + this.values.sort(function (a, b) { + return a > b ? 1 : a < b ? -1 : 0; }); - if (customTimes.length === 0) { - throw new Error('No custom time bar found with id ' + JSON.stringify(id)); + if (this.values.length > 0) { + this.selectValue(0); } - customTimes.forEach((function (customTime) { - this.customTimes.splice(this.customTimes.indexOf(customTime), 1); - this.components.splice(this.components.indexOf(customTime), 1); - customTime.destroy(); - }).bind(this)); + // create an array with the filtered datapoints. this will be loaded afterwards + this.dataPoints = []; + + this.loaded = false; + this.onLoadCallback = undefined; + + if (graph.animationPreload) { + this.loaded = false; + this.loadInBackground(); + } else { + this.loaded = true; + } }; /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * Return the label + * @return {string} label */ - Core.prototype.getVisibleItems = function () { - return this.itemSet && this.itemSet.getVisibleItems() || []; + Filter.prototype.isLoaded = function () { + return this.loaded; }; /** - * Set Core window such that it fits all items - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. + * Return the loaded progress + * @return {Number} percentage between 0 and 100 */ - Core.prototype.fit = function (options) { - var range = this.getDataRange(); + Filter.prototype.getLoadedProgress = function () { + var len = this.values.length; - // skip range set if there is no min and max date - if (range.min === null && range.max === null) { - return; + var i = 0; + while (this.dataPoints[i]) { + i++; } - // apply a margin of 1% left and right of the data - var interval = range.max - range.min; - var min = new Date(range.min.valueOf() - interval * 0.01); - var max = new Date(range.max.valueOf() + interval * 0.01); + return Math.round(i / len * 100); + }; - var animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(min, max, animation); + /** + * Return the label + * @return {string} label + */ + Filter.prototype.getLabel = function () { + return this.graph.filterLabel; + }; + + /** + * Return the columnIndex of the filter + * @return {Number} columnIndex + */ + Filter.prototype.getColumn = function () { + return this.column; + }; + + /** + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value + */ + Filter.prototype.getSelectedValue = function () { + if (this.index === undefined) return undefined; + + return this.values[this.index]; }; /** - * Calculate the data range of the items start and end dates - * @returns {{min: Date | null, max: Date | null}} - * @protected + * Retrieve all values of the filter + * @return {Array} values */ - Core.prototype.getDataRange = function () { - // must be implemented by Timeline and Graph2d - throw new Error('Cannot invoke abstract method getDataRange'); + Filter.prototype.getValues = function () { + return this.values; }; /** - * Set the visible window. Both parameters are optional, you can change only - * start or only end. Syntax: - * - * TimeLine.setWindow(start, end) - * TimeLine.setWindow(start, end, options) - * TimeLine.setWindow(range) - * - * Where start and end can be a Date, number, or string, and range is an - * object with properties start and end. - * - * @param {Date | Number | String | Object} [start] Start date of visible window - * @param {Date | Number | String} [end] End date of visible window - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. + * Retrieve one value of the filter + * @param {Number} index + * @return {*} value */ - Core.prototype.setWindow = function (start, end, options) { - var animation; - if (arguments.length == 1) { - var range = arguments[0]; - animation = range.animation !== undefined ? range.animation : true; - this.range.setRange(range.start, range.end, animation); - } else { - animation = options && options.animation !== undefined ? options.animation : true; - this.range.setRange(start, end, animation); - } + Filter.prototype.getValue = function (index) { + if (index >= this.values.length) throw 'Error: index out of range'; + + return this.values[index]; }; /** - * Move the window such that given time is centered on screen. - * @param {Date | Number | String} time - * @param {Object} [options] Available options: - * `animation: boolean | {duration: number, easingFunction: string}` - * If true (default), the range is animated - * smoothly to the new window. An object can be - * provided to specify duration and easing function. - * Default duration is 500 ms, and default easing - * function is 'easeInOutQuad'. + * Retrieve the (filtered) dataPoints for the currently selected filter index + * @param {Number} [index] (optional) + * @return {Array} dataPoints */ - Core.prototype.moveTo = function (time, options) { - var interval = this.range.end - this.range.start; - var t = util.convert(time, 'Date').valueOf(); + Filter.prototype._getDataPoints = function (index) { + if (index === undefined) index = this.index; - var start = t - interval / 2; - var end = t + interval / 2; - var animation = options && options.animation !== undefined ? options.animation : true; + if (index === undefined) return []; - this.range.setRange(start, end, animation); + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; + } else { + var f = {}; + f.column = this.column; + f.value = this.values[index]; + + var dataView = new DataView(this.data, { filter: function filter(item) { + return item[f.column] == f.value; + } }).get(); + dataPoints = this.graph._getDataPoints(dataView); + + this.dataPoints[index] = dataPoints; + } + + return dataPoints; }; /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range + * Set a callback function when the filter is fully loaded. */ - Core.prototype.getWindow = function () { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) - }; + Filter.prototype.setOnLoadCallback = function (callback) { + this.onLoadCallback = callback; }; /** - * Force a redraw. Can be overridden by implementations of Core + * Add a value to the list with available values for this filter + * No double entries will be created. + * @param {Number} index */ - Core.prototype.redraw = function () { - this._redraw(); + Filter.prototype.selectValue = function (index) { + if (index >= this.values.length) throw 'Error: index out of range'; + + this.index = index; + this.value = this.values[index]; }; /** - * Redraw for internal use. Redraws all components. See also the public - * method redraw. - * @protected + * Load all filtered rows in the background one by one + * Start this method without providing an index! */ - Core.prototype._redraw = function () { - var resized = false; - var options = this.options; - var props = this.props; - var dom = this.dom; + Filter.prototype.loadInBackground = function (index) { + if (index === undefined) index = 0; - if (!dom) return; // when destroyed + var frame = this.graph.frame; - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + if (index < this.values.length) { + var dataPointsTemp = this._getDataPoints(index); + //this.graph.redrawInfo(); // TODO: not neat - // update class names - if (options.orientation == 'top') { - util.addClassName(dom.root, 'vis-top'); - util.removeClassName(dom.root, 'vis-bottom'); - } else { - util.removeClassName(dom.root, 'vis-top'); - util.addClassName(dom.root, 'vis-bottom'); - } + // create a progress box + if (frame.progress === undefined) { + frame.progress = document.createElement('DIV'); + frame.progress.style.position = 'absolute'; + frame.progress.style.color = 'gray'; + frame.appendChild(frame.progress); + } + var progress = this.getLoadedProgress(); + frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; + // TODO: this is no nice solution... + frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider + frame.progress.style.left = 10 + 'px'; - // update root width and height options - dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); - dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); - dom.root.style.width = util.option.asSize(options.width, ''); + var me = this; + setTimeout(function () { + me.loadInBackground(index + 1); + }, 10); + this.loaded = false; + } else { + this.loaded = true; - // calculate border widths - props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; - props.border.right = props.border.left; - props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; - props.border.bottom = props.border.top; - var borderRootHeight = dom.root.offsetHeight - dom.root.clientHeight; - var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; + // remove the progress box + if (frame.progress !== undefined) { + frame.removeChild(frame.progress); + frame.progress = undefined; + } - // workaround for a bug in IE: the clientWidth of an element with - // a height:0px and overflow:hidden is not calculated and always has value 0 - if (dom.centerContainer.clientHeight === 0) { - props.border.left = props.border.top; - props.border.right = props.border.left; - } - if (dom.root.clientHeight === 0) { - borderRootWidth = borderRootHeight; + if (this.onLoadCallback) this.onLoadCallback(); } + }; - // calculate the heights. If any of the side panels is empty, we set the height to - // minus the border width, such that the border will be invisible - props.center.height = dom.center.offsetHeight; - props.left.height = dom.left.offsetHeight; - props.right.height = dom.right.offsetHeight; - props.top.height = dom.top.clientHeight || -props.border.top; - props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; + module.exports = Filter; - // TODO: compensate borders when any of the panels is empty. +/***/ }, +/* 29 */ +/***/ function(module, exports, __webpack_require__) { - // apply auto height - // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) - var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); - var autoHeight = props.top.height + contentHeight + props.bottom.height + borderRootHeight + props.border.top + props.border.bottom; - dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); + 'use strict'; - // calculate heights of the content panels - props.root.height = dom.root.offsetHeight; - props.background.height = props.root.height - borderRootHeight; - var containerHeight = props.root.height - props.top.height - props.bottom.height - borderRootHeight; - props.centerContainer.height = containerHeight; - props.leftContainer.height = containerHeight; - props.rightContainer.height = props.leftContainer.height; + var util = __webpack_require__(14); - // calculate the widths of the panels - props.root.width = dom.root.offsetWidth; - props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; - props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; - props.rightContainer.width = props.right.width; - var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; - props.center.width = centerWidth; - props.centerContainer.width = centerWidth; - props.top.width = centerWidth; - props.bottom.width = centerWidth; + /** + * @constructor Slider + * + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + * @param {Object} options Available options: + * {boolean} visible If true (default) the + * slider is visible. + */ + function Slider(container, options) { + if (container === undefined) { + throw 'Error: No container element defined'; + } + this.container = container; + this.visible = options && options.visible != undefined ? options.visible : true; - // resize the panels - dom.background.style.height = props.background.height + 'px'; - dom.backgroundVertical.style.height = props.background.height + 'px'; - dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; - dom.centerContainer.style.height = props.centerContainer.height + 'px'; - dom.leftContainer.style.height = props.leftContainer.height + 'px'; - dom.rightContainer.style.height = props.rightContainer.height + 'px'; + if (this.visible) { + this.frame = document.createElement('DIV'); + //this.frame.style.backgroundColor = '#E5E5E5'; + this.frame.style.width = '100%'; + this.frame.style.position = 'relative'; + this.container.appendChild(this.frame); - dom.background.style.width = props.background.width + 'px'; - dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; - dom.backgroundHorizontal.style.width = props.background.width + 'px'; - dom.centerContainer.style.width = props.center.width + 'px'; - dom.top.style.width = props.top.width + 'px'; - dom.bottom.style.width = props.bottom.width + 'px'; + this.frame.prev = document.createElement('INPUT'); + this.frame.prev.type = 'BUTTON'; + this.frame.prev.value = 'Prev'; + this.frame.appendChild(this.frame.prev); - // reposition the panels - dom.background.style.left = '0'; - dom.background.style.top = '0'; - dom.backgroundVertical.style.left = props.left.width + props.border.left + 'px'; - dom.backgroundVertical.style.top = '0'; - dom.backgroundHorizontal.style.left = '0'; - dom.backgroundHorizontal.style.top = props.top.height + 'px'; - dom.centerContainer.style.left = props.left.width + 'px'; - dom.centerContainer.style.top = props.top.height + 'px'; - dom.leftContainer.style.left = '0'; - dom.leftContainer.style.top = props.top.height + 'px'; - dom.rightContainer.style.left = props.left.width + props.center.width + 'px'; - dom.rightContainer.style.top = props.top.height + 'px'; - dom.top.style.left = props.left.width + 'px'; - dom.top.style.top = '0'; - dom.bottom.style.left = props.left.width + 'px'; - dom.bottom.style.top = props.top.height + props.centerContainer.height + 'px'; + this.frame.play = document.createElement('INPUT'); + this.frame.play.type = 'BUTTON'; + this.frame.play.value = 'Play'; + this.frame.appendChild(this.frame.play); - // update the scrollTop, feasible range for the offset can be changed - // when the height of the Core or of the contents of the center changed - this._updateScrollTop(); + this.frame.next = document.createElement('INPUT'); + this.frame.next.type = 'BUTTON'; + this.frame.next.value = 'Next'; + this.frame.appendChild(this.frame.next); - // reposition the scrollable contents - var offset = this.props.scrollTop; - if (options.orientation.item != 'top') { - offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); - } - dom.center.style.left = '0'; - dom.center.style.top = offset + 'px'; - dom.left.style.left = '0'; - dom.left.style.top = offset + 'px'; - dom.right.style.left = '0'; - dom.right.style.top = offset + 'px'; + this.frame.bar = document.createElement('INPUT'); + this.frame.bar.type = 'BUTTON'; + this.frame.bar.style.position = 'absolute'; + this.frame.bar.style.border = '1px solid red'; + this.frame.bar.style.width = '100px'; + this.frame.bar.style.height = '6px'; + this.frame.bar.style.borderRadius = '2px'; + this.frame.bar.style.MozBorderRadius = '2px'; + this.frame.bar.style.border = '1px solid #7F7F7F'; + this.frame.bar.style.backgroundColor = '#E5E5E5'; + this.frame.appendChild(this.frame.bar); - // show shadows when vertical scrolling is available - var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; - var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; - dom.shadowTop.style.visibility = visibilityTop; - dom.shadowBottom.style.visibility = visibilityBottom; - dom.shadowTopLeft.style.visibility = visibilityTop; - dom.shadowBottomLeft.style.visibility = visibilityBottom; - dom.shadowTopRight.style.visibility = visibilityTop; - dom.shadowBottomRight.style.visibility = visibilityBottom; + this.frame.slide = document.createElement('INPUT'); + this.frame.slide.type = 'BUTTON'; + this.frame.slide.style.margin = '0px'; + this.frame.slide.value = ' '; + this.frame.slide.style.position = 'relative'; + this.frame.slide.style.left = '-100px'; + this.frame.appendChild(this.frame.slide); - // redraw all components - this.components.forEach(function (component) { - resized = component.redraw() || resized; - }); - if (resized) { - // keep repainting until all sizes are settled - var MAX_REDRAWS = 3; // maximum number of consecutive redraws - if (this.redrawCount < MAX_REDRAWS) { - this.redrawCount++; - this._redraw(); - } else { - console.log('WARNING: infinite loop in redraw?'); - } - this.redrawCount = 0; + // create events + var me = this; + this.frame.slide.onmousedown = function (event) { + me._onMouseDown(event); + }; + this.frame.prev.onclick = function (event) { + me.prev(event); + }; + this.frame.play.onclick = function (event) { + me.togglePlay(event); + }; + this.frame.next.onclick = function (event) { + me.next(event); + }; } - }; - // TODO: deprecated since version 1.1.0, remove some day - Core.prototype.repaint = function () { - throw new Error('Function repaint is deprecated. Use redraw instead.'); - }; + this.onChangeCallback = undefined; - /** - * Set a current time. This can be used for example to ensure that a client's - * time is synchronized with a shared server time. - * Only applicable when option `showCurrentTime` is true. - * @param {Date | String | Number} time A Date, unix timestamp, or - * ISO date string. - */ - Core.prototype.setCurrentTime = function (time) { - if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); - } + this.values = []; + this.index = undefined; - this.currentTime.setCurrentTime(time); - }; + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } /** - * Get the current time. - * Only applicable when option `showCurrentTime` is true. - * @return {Date} Returns the current time. + * Select the previous index */ - Core.prototype.getCurrentTime = function () { - if (!this.currentTime) { - throw new Error('Option showCurrentTime must be true'); + Slider.prototype.prev = function () { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); } - - return this.currentTime.getCurrentTime(); }; /** - * Convert a position on screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x - * @protected + * Select the next index */ - // TODO: move this function to Range - Core.prototype._toTime = function (x) { - return DateUtil.toTime(this, x, this.props.center.width); + Slider.prototype.next = function () { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } }; /** - * Convert a position on the global screen (pixels) to a datetime - * @param {int} x Position on the screen in pixels - * @return {Date} time The datetime the corresponds with given position x - * @protected + * Select the next index */ - // TODO: move this function to Range - Core.prototype._toGlobalTime = function (x) { - return DateUtil.toTime(this, x, this.props.root.width); - //var conversion = this.range.conversion(this.props.root.width); - //return new Date(x / conversion.scale + conversion.offset); - }; + Slider.prototype.playNext = function () { + var start = new Date(); - /** - * Convert a datetime (Date object) into a position on the screen - * @param {Date} time A date - * @return {int} x The position on the screen in pixels which corresponds - * with the given date. - * @protected - */ - // TODO: move this function to Range - Core.prototype._toScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.center.width); - }; + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } else if (this.playLoop) { + // jump to the start + index = 0; + this.setIndex(index); + } - /** - * Convert a datetime (Date object) into a position on the root - * This is used to get the pixel density estimate for the screen, not the center panel - * @param {Date} time A date - * @return {int} x The position on root in pixels which corresponds - * with the given date. - * @protected - */ - // TODO: move this function to Range - Core.prototype._toGlobalScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.root.width); - //var conversion = this.range.conversion(this.props.root.width); - //return (time.valueOf() - conversion.offset) * conversion.scale; + var end = new Date(); + var diff = end - start; + + // calculate how much time it to to set the index and to execute the callback + // function. + var interval = Math.max(this.playInterval - diff, 0); + // document.title = diff // TODO: cleanup + + var me = this; + this.playTimeout = setTimeout(function () { + me.playNext(); + }, interval); }; /** - * Initialize watching when option autoResize is true - * @private + * Toggle start or stop playing */ - Core.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); + Slider.prototype.togglePlay = function () { + if (this.playTimeout === undefined) { + this.play(); } else { - this._stopAutoResize(); + this.stop(); } }; /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. - * @private + * Start playing */ - Core.prototype._startAutoResize = function () { - var me = this; - - this._stopAutoResize(); - - this._onResize = function () { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); - return; - } + Slider.prototype.play = function () { + // Test whether already playing + if (this.playTimeout) return; - if (me.dom.root) { - // check whether the frame is resized - // Note: we compare offsetWidth here, not clientWidth. For some reason, - // IE does not restore the clientWidth from 0 to the actual width after - // changing the timeline's container display style from none to visible - if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { - me.props.lastWidth = me.dom.root.offsetWidth; - me.props.lastHeight = me.dom.root.offsetHeight; + this.playNext(); - me.emit('change'); - } - } - }; + if (this.frame) { + this.frame.play.value = 'Stop'; + } + }; - // add event listener to window resize - util.addEventListener(window, 'resize', this._onResize); + /** + * Stop playing + */ + Slider.prototype.stop = function () { + clearInterval(this.playTimeout); + this.playTimeout = undefined; - this.watchTimer = setInterval(this._onResize, 1000); + if (this.frame) { + this.frame.play.value = 'Play'; + } }; /** - * Stop watching for a resize of the frame. - * @private + * Set a callback function which will be triggered when the value of the + * slider bar has changed. */ - Core.prototype._stopAutoResize = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; - } - - // remove event listener on window.resize - util.removeEventListener(window, 'resize', this._onResize); - this._onResize = null; + Slider.prototype.setOnChangeCallback = function (callback) { + this.onChangeCallback = callback; }; /** - * Start moving the timeline vertically - * @param {Event} event - * @private + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds */ - Core.prototype._onTouch = function (event) { - this.touch.allowDragging = true; - this.touch.initialScrollTop = this.props.scrollTop; + Slider.prototype.setPlayInterval = function (interval) { + this.playInterval = interval; }; /** - * Start moving the timeline vertically - * @param {Event} event - * @private + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds */ - Core.prototype._onPinch = function (event) { - this.touch.allowDragging = false; + Slider.prototype.getPlayInterval = function (interval) { + return this.playInterval; }; /** - * Move the timeline vertically - * @param {Event} event - * @private + * Set looping on or off + * @pararm {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. */ - Core.prototype._onDrag = function (event) { - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.touch.allowDragging) return; - - var delta = event.deltaY; - - var oldScrollTop = this._getScrollTop(); - var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - - if (newScrollTop != oldScrollTop) { - this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already - this.emit('verticalDrag'); - } + Slider.prototype.setPlayLoop = function (doLoop) { + this.playLoop = doLoop; }; /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop - * @private + * Execute the onchange callback function */ - Core.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; + Slider.prototype.onChange = function () { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } }; /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop - * @private + * redraw the slider on the correct place */ - Core.prototype._updateScrollTop = function () { - // recalculate the scrollTopMin - var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero - if (scrollTopMin != this.props.scrollTopMin) { - // in case of bottom orientation, change the scrollTop such that the contents - // do not move relative to the time axis at the bottom - if (this.options.orientation.item != 'top') { - this.props.scrollTop += scrollTopMin - this.props.scrollTopMin; - } - this.props.scrollTopMin = scrollTopMin; + Slider.prototype.redraw = function () { + if (this.frame) { + // resize the bar + this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + 'px'; + this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + 'px'; + + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = left + 'px'; } + }; - // limit the scrollTop to the feasible scroll range - if (this.props.scrollTop > 0) this.props.scrollTop = 0; - if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; + /** + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) + */ + Slider.prototype.setValues = function (values) { + this.values = values; - return this.props.scrollTop; + if (this.values.length > 0) this.setIndex(0);else this.index = undefined; }; /** - * Get the current scrollTop - * @returns {number} scrollTop - * @private + * Select a value by its index + * @param {Number} index */ - Core.prototype._getScrollTop = function () { - return this.props.scrollTop; + Slider.prototype.setIndex = function (index) { + if (index < this.values.length) { + this.index = index; + + this.redraw(); + this.onChange(); + } else { + throw 'Error: index out of range'; + } }; /** - * Load a configurator - * @return {Object} - * @private + * retrieve the index of the currently selected vaue + * @return {Number} index */ - Core.prototype._createConfigurator = function () { - throw new Error('Cannot invoke abstract method _createConfigurator'); + Slider.prototype.getIndex = function () { + return this.index; }; - module.exports = Core; - -/***/ }, -/* 34 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - var Hammer = __webpack_require__(5); - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - var TimeStep = __webpack_require__(38); - var Component = __webpack_require__(28); - var Group = __webpack_require__(35); - var BackgroundGroup = __webpack_require__(39); - var BoxItem = __webpack_require__(40); - var PointItem = __webpack_require__(2); - var RangeItem = __webpack_require__(37); - var BackgroundItem = __webpack_require__(41); - - var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items - var BACKGROUND = '__background__'; // reserved group id for background items without group - /** - * An ItemSet holds a set of items and ranges which can be displayed in a - * range. The width is determined by the parent of the ItemSet, and the height - * is determined by the size of the items. - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body - * @param {Object} [options] See ItemSet.setOptions for the available options. - * @constructor ItemSet - * @extends Component + * retrieve the currently selected value + * @return {*} value */ - function ItemSet(body, options) { - this.body = body; - - this.defaultOptions = { - type: null, // 'box', 'point', 'range', 'background' - orientation: { - item: 'bottom' // item orientation: 'top' or 'bottom' - }, - align: 'auto', // alignment of box items - stack: true, - groupOrder: null, - - selectable: true, - multiselect: false, - - editable: { - updateTime: false, - updateGroup: false, - add: false, - remove: false - }, - - snap: TimeStep.snap, - - onAdd: function onAdd(item, callback) { - callback(item); - }, - onUpdate: function onUpdate(item, callback) { - callback(item); - }, - onMove: function onMove(item, callback) { - callback(item); - }, - onRemove: function onRemove(item, callback) { - callback(item); - }, - onMoving: function onMoving(item, callback) { - callback(item); - }, - - margin: { - item: { - horizontal: 10, - vertical: 10 - }, - axis: 20 - } - }; + Slider.prototype.get = function () { + return this.values[this.index]; + }; - // options is shared by this ItemSet and all its items - this.options = util.extend({}, this.defaultOptions); + Slider.prototype._onMouseDown = function (event) { + // only react on left mouse button down + var leftButtonDown = event.which ? event.which === 1 : event.button === 1; + if (!leftButtonDown) return; - // options for getting items from the DataSet with the correct type - this.itemOptions = { - type: { start: 'Date', end: 'Date' } - }; + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); - this.conversion = { - toScreen: body.util.toScreen, - toTime: body.util.toTime - }; - this.dom = {}; - this.props = {}; - this.hammer = null; + this.frame.style.cursor = 'move'; + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - - // listeners for the DataSet of the items - this.itemListeners = { - 'add': function add(event, params, senderId) { - me._onAdd(params.items); - }, - 'update': function update(event, params, senderId) { - me._onUpdate(params.items); - }, - 'remove': function remove(event, params, senderId) { - me._onRemove(params.items); - } + this.onmousemove = function (event) { + me._onMouseMove(event); }; - - // listeners for the DataSet of the groups - this.groupListeners = { - 'add': function add(event, params, senderId) { - me._onAddGroups(params.items); - }, - 'update': function update(event, params, senderId) { - me._onUpdateGroups(params.items); - }, - 'remove': function remove(event, params, senderId) { - me._onRemoveGroups(params.items); - } + this.onmouseup = function (event) { + me._onMouseUp(event); }; + util.addEventListener(document, 'mousemove', this.onmousemove); + util.addEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); + }; - this.items = {}; // object with an Item for every data item - this.groups = {}; // Group object for every group - this.groupIds = []; - - this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw - - this.touchParams = {}; // stores properties while dragging - // create the HTML DOM - - this._create(); - - this.setOptions(options); - } + Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + var x = left - 3; - ItemSet.prototype = new Component(); + var index = Math.round(x / width * (this.values.length - 1)); + if (index < 0) index = 0; + if (index > this.values.length - 1) index = this.values.length - 1; - // available item types will be registered here - ItemSet.types = { - background: BackgroundItem, - box: BoxItem, - range: RangeItem, - point: PointItem + return index; }; - /** - * Create the HTML DOM for the ItemSet - */ - ItemSet.prototype._create = function () { - var frame = document.createElement('div'); - frame.className = 'vis-itemset'; - frame['timeline-itemset'] = this; - this.dom.frame = frame; - - // create background panel - var background = document.createElement('div'); - background.className = 'vis-background'; - frame.appendChild(background); - this.dom.background = background; + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - // create foreground panel - var foreground = document.createElement('div'); - foreground.className = 'vis-foreground'; - frame.appendChild(foreground); - this.dom.foreground = foreground; + var x = index / (this.values.length - 1) * width; + var left = x + 3; - // create axis panel - var axis = document.createElement('div'); - axis.className = 'vis-axis'; - this.dom.axis = axis; + return left; + }; - // create labelset - var labelSet = document.createElement('div'); - labelSet.className = 'vis-labelset'; - this.dom.labelSet = labelSet; + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; - // create ungrouped Group - this._updateUngrouped(); + var index = this.leftToIndex(x); - // create background Group - var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); - backgroundGroup.show(); - this.groups[BACKGROUND] = backgroundGroup; + this.setIndex(index); - // attach event listeners - // Note: we bind to the centerContainer for the case where the height - // of the center container is larger than of the ItemSet, so we - // can click in the empty area to create a new item or deselect an item. - this.hammer = new Hammer(this.body.dom.centerContainer); + util.preventDefault(); + }; - // drag items when selected - this.hammer.on('hammer.input', (function (event) { - if (event.isFirst) { - this._onTouch(event); - } - }).bind(this)); - this.hammer.on('panstart', this._onDragStart.bind(this)); - this.hammer.on('panmove', this._onDrag.bind(this)); - this.hammer.on('panend', this._onDragEnd.bind(this)); - this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. + Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; - // single select (or unselect) when tapping an item - this.hammer.on('tap', this._onSelectItem.bind(this)); + // remove event listeners + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); - // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on('press', this._onMultiSelectItem.bind(this)); + util.preventDefault(); + }; - // add item on doubletap - this.hammer.on('doubletap', this._onAddItem.bind(this)); + module.exports = Slider; - // attach to the DOM - this.show(); - }; +/***/ }, +/* 30 */ +/***/ function(module, exports, __webpack_require__) { /** - * Set options for the ItemSet. Existing options will be extended/overwritten. - * @param {Object} [options] The following options are available: - * {String} type - * Default type for the items. Choose from 'box' - * (default), 'point', 'range', or 'background'. - * The default style can be overwritten by - * individual items. - * {String} align - * Alignment for the items, only applicable for - * BoxItem. Choose 'center' (default), 'left', or - * 'right'. - * {String} orientation.item - * Orientation of the item set. Choose 'top' or - * 'bottom' (default). - * {Function} groupOrder - * A sorting function for ordering groups - * {Boolean} stack - * If true (default), items will be stacked on - * top of each other. - * {Number} margin.axis - * Margin between the axis and the items in pixels. - * Default is 20. - * {Number} margin.item.horizontal - * Horizontal margin between items in pixels. - * Default is 10. - * {Number} margin.item.vertical - * Vertical Margin between items in pixels. - * Default is 10. - * {Number} margin.item - * Margin between items in pixels in both horizontal - * and vertical direction. Default is 10. - * {Number} margin - * Set margin for both axis and items in pixels. - * {Boolean} selectable - * If true (default), items can be selected. - * {Boolean} multiselect - * If true, multiple items can be selected. - * False by default. - * {Boolean} editable - * Set all editable options to true or false - * {Boolean} editable.updateTime - * Allow dragging an item to an other moment in time - * {Boolean} editable.updateGroup - * Allow dragging an item to an other group - * {Boolean} editable.add - * Allow creating new items on double tap - * {Boolean} editable.remove - * Allow removing items by clicking the delete button - * top right of a selected item. - * {Function(item: Item, callback: Function)} onAdd - * Callback function triggered when an item is about to be added: - * when the user double taps an empty space in the Timeline. - * {Function(item: Item, callback: Function)} onUpdate - * Callback function fired when an item is about to be updated. - * This function typically has to show a dialog where the user - * change the item. If not implemented, nothing happens. - * {Function(item: Item, callback: Function)} onMove - * Fired when an item has been moved. If not implemented, - * the move action will be accepted. - * {Function(item: Item, callback: Function)} onRemove - * Fired when an item is about to be deleted. - * If not implemented, the item will be always removed. + * @prototype StepNumber + * The class StepNumber is an iterator for Numbers. You provide a start and end + * value, and a best step size. StepNumber itself rounds to fixed values and + * a finds the step that best fits the provided step. + * + * If prettyStep is true, the step size is chosen as close as possible to the + * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... + * + * Example usage: + * var step = new StepNumber(0, 10, 2.5, true); + * step.start(); + * while (!step.end()) { + * alert(step.getCurrent()); + * step.next(); + * } + * + * Version: 1.0 + * + * @param {Number} start The start value + * @param {Number} end The end value + * @param {Number} step Optional. Step size. Must be a positive value. + * @param {boolean} prettyStep Optional. If true, the step size is rounded + * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ - ItemSet.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - var fields = ['type', 'align', 'order', 'stack', 'selectable', 'multiselect', 'groupOrder', 'dataAttributes', 'template', 'hide', 'snap']; - util.selectiveExtend(fields, this.options, options); - - if ('orientation' in options) { - if (typeof options.orientation === 'string') { - this.options.orientation.item = options.orientation === 'top' ? 'top' : 'bottom'; - } else if (typeof options.orientation === 'object' && 'item' in options.orientation) { - this.options.orientation.item = options.orientation.item; - } - } - - if ('margin' in options) { - if (typeof options.margin === 'number') { - this.options.margin.axis = options.margin; - this.options.margin.item.horizontal = options.margin; - this.options.margin.item.vertical = options.margin; - } else if (typeof options.margin === 'object') { - util.selectiveExtend(['axis'], this.options.margin, options.margin); - if ('item' in options.margin) { - if (typeof options.margin.item === 'number') { - this.options.margin.item.horizontal = options.margin.item; - this.options.margin.item.vertical = options.margin.item; - } else if (typeof options.margin.item === 'object') { - util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item); - } - } - } - } - - if ('editable' in options) { - if (typeof options.editable === 'boolean') { - this.options.editable.updateTime = options.editable; - this.options.editable.updateGroup = options.editable; - this.options.editable.add = options.editable; - this.options.editable.remove = options.editable; - } else if (typeof options.editable === 'object') { - util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable); - } - } + "use strict"; - // callback functions - var addCallback = (function (name) { - var fn = options[name]; - if (fn) { - if (!(fn instanceof Function)) { - throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)'); - } - this.options[name] = fn; - } - }).bind(this); - ['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving'].forEach(addCallback); + function StepNumber(start, end, step, prettyStep) { + // set default values + this._start = 0; + this._end = 0; + this._step = 1; + this.prettyStep = true; + this.precision = 5; - // force the itemSet to refresh: options like orientation and margins may be changed - this.markDirty(); - } + this._current = 0; + this.setRange(start, end, step, prettyStep); }; /** - * Mark the ItemSet dirty so it will refresh everything with next redraw. - * Optionally, all items can be marked as dirty and be refreshed. - * @param {{refreshItems: boolean}} [options] + * Set a new range: start, end and step. + * + * @param {Number} start The start value + * @param {Number} end The end value + * @param {Number} step Optional. Step size. Must be a positive value. + * @param {boolean} prettyStep Optional. If true, the step size is rounded + * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ - ItemSet.prototype.markDirty = function (options) { - this.groupIds = []; - this.stackDirty = true; + StepNumber.prototype.setRange = function (start, end, step, prettyStep) { + this._start = start ? start : 0; + this._end = end ? end : 0; - if (options && options.refreshItems) { - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); - } + this.setStep(step, prettyStep); }; /** - * Destroy the ItemSet + * Set a new step size + * @param {Number} step New step size. Must be a positive value + * @param {boolean} prettyStep Optional. If true, the provided step is rounded + * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) */ - ItemSet.prototype.destroy = function () { - this.hide(); - this.setItems(null); - this.setGroups(null); + StepNumber.prototype.setStep = function (step, prettyStep) { + if (step === undefined || step <= 0) return; - this.hammer = null; + if (prettyStep !== undefined) this.prettyStep = prettyStep; - this.body = null; - this.conversion = null; + if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; }; /** - * Hide the component from the DOM + * Calculate a nice step size, closest to the desired step size. + * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an + * integer Number. For example 1, 2, 5, 10, 20, 50, etc... + * @param {Number} step Desired step size + * @return {Number} Nice step size */ - ItemSet.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + StepNumber.calculatePrettyStep = function (step) { + var log10 = function log10(x) { + return Math.log(x) / Math.LN10; + }; - // remove the axis with dots - if (this.dom.axis.parentNode) { - this.dom.axis.parentNode.removeChild(this.dom.axis); - } + // try three steps (multiple of 1, 2, or 5 + var step1 = Math.pow(10, Math.round(log10(step))), + step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), + step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); - // remove the labelset containing all group labels - if (this.dom.labelSet.parentNode) { - this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + // choose the best step (closest to minimum step) + var prettyStep = step1; + if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; + if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; + + // for safety + if (prettyStep <= 0) { + prettyStep = 1; } + + return prettyStep; }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * returns the current value of the step + * @return {Number} current value */ - ItemSet.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } - - // show axis with dots - if (!this.dom.axis.parentNode) { - this.body.dom.backgroundVertical.appendChild(this.dom.axis); - } - - // show labelset containing labels - if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); - } + StepNumber.prototype.getCurrent = function () { + return parseFloat(this._current.toPrecision(this.precision)); }; /** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {string[] | string} [ids] An array with zero or more id's of the items to be - * selected, or a single item id. If ids is undefined - * or an empty array, all items will be unselected. + * returns the current step size + * @return {Number} current step size */ - ItemSet.prototype.setSelection = function (ids) { - var i, ii, id, item; - - if (ids == undefined) ids = []; - if (!Array.isArray(ids)) ids = [ids]; + StepNumber.prototype.getStep = function () { + return this._step; + }; - // unselect currently selected items - for (i = 0, ii = this.selection.length; i < ii; i++) { - id = this.selection[i]; - item = this.items[id]; - if (item) item.unselect(); - } + /** + * Set the current value to the largest value smaller than start, which + * is a multiple of the step size + */ + StepNumber.prototype.start = function () { + this._current = this._start - this._start % this._step; + }; - // select items - this.selection = []; - for (i = 0, ii = ids.length; i < ii; i++) { - id = ids[i]; - item = this.items[id]; - if (item) { - this.selection.push(id); - item.select(); - } - } + /** + * Do a step, add the step size to the current value + */ + StepNumber.prototype.next = function () { + this._current += this._step; }; /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * Returns true whether the end is reached + * @return {boolean} True if the current value has passed the end value. */ - ItemSet.prototype.getSelection = function () { - return this.selection.concat([]); + StepNumber.prototype.end = function () { + return this._current > this._end; }; + module.exports = StepNumber; + +/***/ }, +/* 31 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var Emitter = __webpack_require__(25); + var Hammer = __webpack_require__(10); + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + var Range = __webpack_require__(35); + var Core = __webpack_require__(38); + var TimeAxis = __webpack_require__(47); + var CurrentTime = __webpack_require__(32); + var CustomTime = __webpack_require__(50); + var ItemSet = __webpack_require__(39); + + var Configurator = __webpack_require__(51); + var Validator = __webpack_require__(53)['default']; + var printStyle = __webpack_require__(53).printStyle; + var allOptions = __webpack_require__(54).allOptions; + var configureOptions = __webpack_require__(54).configureOptions; + /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | vis.DataView | Array} [items] + * @param {vis.DataSet | vis.DataView | Array} [groups] + * @param {Object} [options] See Timeline.setOptions for the available options. + * @constructor + * @extends Core */ - ItemSet.prototype.getVisibleItems = function () { - var range = this.body.range.getRange(); - var left = this.body.util.toScreen(range.start); - var right = this.body.util.toScreen(range.end); + function Timeline(container, items, groups, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } - var ids = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - var group = this.groups[groupId]; - var rawVisibleItems = group.visibleItems; + // if the third element is options, the forth is groups (optionally); + if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { + var forthArgument = options; + options = groups; + groups = forthArgument; + } - // filter the "raw" set with visibleItems into a set which is really - // visible by pixels - for (var i = 0; i < rawVisibleItems.length; i++) { - var item = rawVisibleItems[i]; - // TODO: also check whether visible vertically - if (item.left < right && item.left + item.width > left) { - ids.push(item.id); - } - } + var me = this; + this.defaultOptions = { + start: null, + end: null, + + autoResize: true, + + orientation: { + axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' + item: 'bottom' // not relevant + }, + + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); + + // Create the DOM, props, and emitter + this._create(container); + + // all components listed here will be repainted automatically + this.components = []; + + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + hiddenDates: [], + util: { + getScale: function getScale() { + return me.timeAxis.step.scale; + }, + getStep: function getStep() { + return me.timeAxis.step.step; + }, + + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime: me._toGlobalTime.bind(me) } + }; + + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; + + // time axis + this.timeAxis = new TimeAxis(this.body); + this.timeAxis2 = null; // used in case of orientation option 'both' + this.components.push(this.timeAxis); + + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); + + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + this.on('tap', function (event) { + me.emit('click', me.getEventProperties(event)); + }); + this.on('doubletap', function (event) { + me.emit('doubleClick', me.getEventProperties(event)); + }); + this.dom.root.oncontextmenu = function (event) { + me.emit('contextmenu', me.getEventProperties(event)); + }; + + // apply options + if (options) { + this.setOptions(options); } - return ids; - }; + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } + + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } + } + + // Extend the functionality from Core + Timeline.prototype = new Core(); /** - * Deselect a selected item - * @param {String | Number} id + * Load a configurator + * @return {Object} * @private */ - ItemSet.prototype._deselect = function (id) { - var selection = this.selection; - for (var i = 0, ii = selection.length; i < ii; i++) { - if (selection[i] == id) { - // non-strict comparison! - selection.splice(i, 1); - break; - } - } + Timeline.prototype._createConfigurator = function () { + return new Configurator(this, this.dom.container, configureOptions); }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Force a redraw. The size of all items will be recalculated. + * Can be useful to manually redraw when option autoResize=false and the window + * has been resized, or when the items CSS has been changed. */ - ItemSet.prototype.redraw = function () { - var margin = this.options.margin, - range = this.body.range, - asSize = util.option.asSize, - options = this.options, - orientation = options.orientation.item, - resized = false, - frame = this.dom.frame; + Timeline.prototype.redraw = function () { + this.itemSet && this.itemSet.markDirty({ refreshItems: true }); + this._redraw(); + }; - // recalculate absolute position (before redrawing groups) - this.props.top = this.body.domProps.top.height + this.body.domProps.border.top; - this.props.left = this.body.domProps.left.width + this.body.domProps.border.left; + Timeline.prototype.setOptions = function (options) { + // validate options + var errorFound = Validator.validate(options, allOptions); + if (errorFound === true) { + console.log('%cErrors have been found in the supplied options object.', printStyle); + } - // update class name - frame.className = 'vis-itemset'; + Core.prototype.setOptions.call(this, options); - // reorder the groups (if needed) - resized = this._orderGroups() || resized; + if ('type' in options) { + if (options.type !== this.options.type) { + this.options.type = options.type; - // check whether zoomed (in that case we need to re-stack everything) - // TODO: would be nicer to get this as a trigger from Range - var visibleInterval = range.end - range.start; - var zoomed = visibleInterval != this.lastVisibleInterval || this.props.width != this.props.lastWidth; - if (zoomed) this.stackDirty = true; - this.lastVisibleInterval = visibleInterval; - this.props.lastWidth = this.props.width; + // force recreation of all items + var itemsData = this.itemsData; + if (itemsData) { + var selection = this.getSelection(); + this.setItems(null); // remove all + this.setItems(itemsData); // add all + this.setSelection(selection); // restore selection + } + } + } + }; - var restack = this.stackDirty; - var firstGroup = this._firstGroup(); - var firstMargin = { - item: margin.item, - axis: margin.axis - }; - var nonFirstMargin = { - item: margin.item, - axis: margin.item.vertical / 2 - }; - var height = 0; - var minHeight = margin.axis + margin.item.vertical; + /** + * Set items + * @param {vis.DataSet | Array | null} items + */ + Timeline.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - // redraw the background group - this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); + // 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' + } + }); + } - // redraw all regular groups - util.forEach(this.groups, function (group) { - var groupMargin = group == firstGroup ? firstMargin : nonFirstMargin; - var groupResized = group.redraw(range, groupMargin, restack); - resized = groupResized || resized; - height += group.height; - }); - height = Math.max(height, minHeight); - this.stackDirty = false; + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - // update frame height - frame.style.height = asSize(height); + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + if (this.options.start == undefined || this.options.end == undefined) { + var range = this.getItemRange(); + } - // calculate actual size - this.props.width = frame.offsetWidth; - this.props.height = height; + var start = this.options.start != undefined ? this.options.start : range.min; + var end = this.options.end != undefined ? this.options.end : range.max; - // reposition axis - this.dom.axis.style.top = asSize(orientation == 'top' ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); - this.dom.axis.style.left = '0'; + this.setWindow(start, end, { animation: false }); + } else { + this.fit({ animation: false }); + } + } + }; - // check if this component is resized - resized = this._isResized() || resized; + /** + * Set groups + * @param {vis.DataSet | Array} groups + */ + Timeline.prototype.setGroups = function (groups) { + // convert to type DataSet when needed + var newDataSet; + if (!groups) { + newDataSet = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + newDataSet = groups; + } else { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } - return resized; + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); }; /** - * Get the first group, aligned with the axis - * @return {Group | null} firstGroup - * @private + * Set both items and groups in one go + * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data */ - ItemSet.prototype._firstGroup = function () { - var firstGroupIndex = this.options.orientation.item == 'top' ? 0 : this.groupIds.length - 1; - var firstGroupId = this.groupIds[firstGroupIndex]; - var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; + Timeline.prototype.setData = function (data) { + if (data && data.groups) { + this.setGroups(data.groups); + } - return firstGroup || null; + if (data && data.items) { + this.setItems(data.items); + } }; /** - * Create or delete the group holding all ungrouped items. This group is used when - * there are no groups specified. - * @protected + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {string[] | string} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + * @param {Object} [options] Available options: + * `focus: boolean` + * If true, focus will be set to the selected item(s) + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + * Only applicable when option focus is true. + */ + Timeline.prototype.setSelection = function (ids, options) { + this.itemSet && this.itemSet.setSelection(ids); + + if (options && options.focus) { + this.focus(ids, options); + } + }; + + /** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ + Timeline.prototype.getSelection = function () { + return this.itemSet && this.itemSet.getSelection() || []; + }; + + /** + * Adjust the visible window such that the selected item (or multiple items) + * are centered on screen. + * @param {String | String[]} id An item id or array with item ids + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. */ - ItemSet.prototype._updateUngrouped = function () { - var ungrouped = this.groups[UNGROUPED]; - var background = this.groups[BACKGROUND]; - var item, itemId; + Timeline.prototype.focus = function (id, options) { + if (!this.itemsData || id == undefined) return; - if (this.groupsData) { - // remove the group holding all ungrouped items - if (ungrouped) { - ungrouped.hide(); - delete this.groups[UNGROUPED]; + var ids = Array.isArray(id) ? id : [id]; - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - item.parent && item.parent.remove(item); - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - group && group.add(item) || item.hide(); - } - } + // get the specified item(s) + var itemsData = this.itemsData.getDataSet().get(ids, { + type: { + start: 'Date', + end: 'Date' } - } else { - // create a group holding all (unfiltered) items - if (!ungrouped) { - var id = null; - var data = null; - ungrouped = new Group(id, data, this); - this.groups[UNGROUPED] = ungrouped; + }); - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - ungrouped.add(item); - } - } + // calculate minimum start and maximum end of specified items + var start = null; + var end = null; + itemsData.forEach(function (itemData) { + var s = itemData.start.valueOf(); + var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); - ungrouped.show(); + if (start === null || s < start) { + start = s; + } + + if (end === null || e > end) { + end = e; } + }); + + if (start !== null && end !== null) { + // calculate the new middle and interval for the window + var middle = (start + end) / 2; + var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); + + var animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(middle - interval / 2, middle + interval / 2, animation); } }; /** - * Get the element for the labelset - * @return {HTMLElement} labelSet + * Set Timeline window such that it fits all items + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. */ - ItemSet.prototype.getLabelSet = function () { - return this.dom.labelSet; + Timeline.prototype.fit = function (options) { + var animation = options && options.animation !== undefined ? options.animation : true; + var range = this.getItemRange(); + this.range.setRange(range.min, range.max, animation); }; /** - * Set items - * @param {vis.DataSet | null} items + * Determine the range of the items, taking into account their actual width + * and a margin of 10 pixels on both sides. + * @return {{min: Date | null, max: Date | null}} */ - ItemSet.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; + Timeline.prototype.getItemRange = function () { + var _this = this; - // replace the dataset - if (!items) { - this.itemsData = null; - } else if (items instanceof DataSet || items instanceof DataView) { - this.itemsData = items; - } else { - throw new TypeError('Data must be an instance of DataSet or DataView'); - } + // get a rough approximation for the range based on the items start and end dates + var range = this.getDataRange(); + var min = range.min; + var max = range.max; + var minItem = null; + var maxItem = null; - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); - }); + if (min != null && max != null) { + var interval; + var factor; + var lhs; + var rhs; + var delta; - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + (function () { + var getStart = function (item) { + return util.convert(item.data.start, 'Date').valueOf(); + }; - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); - }); + var getEnd = function (item) { + var end = item.data.end != undefined ? item.data.end : item.data.start; + return util.convert(end, 'Date').valueOf(); + }; - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + interval = max - min; + // ms + if (interval <= 0) { + interval = 10; + } + factor = interval / _this.props.center.width; - // update the group holding all ungrouped items - this._updateUngrouped(); + // calculate the date of the left side and right side of the items given + util.forEach(_this.itemSet.items, (function (item) { + item.show(); + + var start = getStart(item); + var end = getEnd(item); + + var left = new Date(start - (item.getWidthLeft() + 10) * factor); + var right = new Date(end + (item.getWidthRight() + 10) * factor); + + if (left < min) { + min = left; + minItem = item; + } + if (right > max) { + max = right; + maxItem = item; + } + }).bind(_this)); + + if (minItem && maxItem) { + lhs = minItem.getWidthLeft() + 10; + rhs = maxItem.getWidthRight() + 10; + delta = _this.props.center.width - lhs - rhs; + // px + + if (delta > 0) { + min = getStart(minItem) - lhs * interval / delta; // ms + max = getEnd(maxItem) + rhs * interval / delta; // ms + } + } + })(); } + + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; }; /** - * Get the current items - * @returns {vis.DataSet | null} + * Calculate the data range of the items start and end dates + * @returns {{min: Date | null, max: Date | null}} */ - ItemSet.prototype.getItems = function () { - return this.itemsData; + Timeline.prototype.getDataRange = function () { + var min = null; + var max = null; + + var dataset = this.itemsData && this.itemsData.getDataSet(); + if (dataset) { + dataset.forEach(function (item) { + var start = util.convert(item.start, 'Date').valueOf(); + var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf(); + if (min === null || start < min) { + min = start; + } + if (max === null || end > max) { + max = start; + } + }); + } + + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; }; /** - * Set groups - * @param {vis.DataSet} groups + * Generate Timeline related information from an event + * @param {Event} event + * @return {Object} An object with related information, like on which area + * The event happened, whether clicked on an item, etc. */ - ItemSet.prototype.setGroups = function (groups) { - var me = this, - ids; + Timeline.prototype.getEventProperties = function (event) { + var clientX = event.center ? event.center.x : event.clientX; + var clientY = event.center ? event.center.y : event.clientY; + var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); + var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.off(event, callback); - }); + var item = this.itemSet.itemFromTarget(event); + var group = this.itemSet.groupFromTarget(event); + var customTime = CustomTime.customTimeFromTarget(event); - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + var snap = this.itemSet.options.snap || null; + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var time = this._toTime(x); + var snappedTime = snap ? snap(time, scale, step) : time; - // replace the dataset - if (!groups) { - this.groupsData = null; - } else if (groups instanceof DataSet || groups instanceof DataView) { - this.groupsData = groups; - } else { - throw new TypeError('Data must be an instance of DataSet or DataView'); + var element = util.getTarget(event); + var what = null; + if (item != null) { + what = 'item'; + } else if (customTime != null) { + what = 'custom-time'; + } else if (util.hasParent(element, this.timeAxis.dom.foreground)) { + what = 'axis'; + } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { + what = 'axis'; + } else if (util.hasParent(element, this.itemSet.dom.labelSet)) { + what = 'group-label'; + } else if (util.hasParent(element, this.currentTime.bar)) { + what = 'current-time'; + } else if (util.hasParent(element, this.dom.center)) { + what = 'background'; } - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + return { + event: event, + item: item ? item.id : null, + group: group ? group.groupId : null, + what: what, + pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, + pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, + x: x, + y: y, + time: time, + snappedTime: snappedTime + }; + }; - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); - } + module.exports = Timeline; - // update the group holding all ungrouped items - this._updateUngrouped(); +/***/ }, +/* 32 */ +/***/ function(module, exports, __webpack_require__) { - // update the order of all items in each group - this._order(); + 'use strict'; - this.body.emitter.emit('change', { queue: true }); - }; + var util = __webpack_require__(14); + var Component = __webpack_require__(33); + var moment = __webpack_require__(15); + var locales = __webpack_require__(34); /** - * Get the current groups - * @returns {vis.DataSet | null} groups + * A current time bar + * @param {{range: Range, dom: Object, domProps: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCurrentTime] + * @constructor CurrentTime + * @extends Component */ - ItemSet.prototype.getGroups = function () { - return this.groupsData; - }; + function CurrentTime(body, options) { + this.body = body; + + // default options + this.defaultOptions = { + showCurrentTime: true, + + locales: locales, + locale: 'en' + }; + this.options = util.extend({}, this.defaultOptions); + this.offset = 0; + + this._create(); + + this.setOptions(options); + } + + CurrentTime.prototype = new Component(); /** - * Remove an item by its id - * @param {String | Number} id + * Create the HTML DOM for the current time bar + * @private */ - ItemSet.prototype.removeItem = function (id) { - var item = this.itemsData.get(id), - dataset = this.itemsData.getDataSet(); + CurrentTime.prototype._create = function () { + var bar = document.createElement('div'); + bar.className = 'vis-current-time'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; - if (item) { - // confirm deletion - this.options.onRemove(item, function (item) { - if (item) { - // remove by id here, it is possible that an item has no id defined - // itself, so better not delete by the item itself - dataset.remove(id); - } - }); - } + this.bar = bar; }; /** - * Get the time of an item based on it's data and options.type - * @param {Object} itemData - * @returns {string} Returns the type - * @private + * Destroy the CurrentTime bar */ - ItemSet.prototype._getType = function (itemData) { - return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); + CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing + + this.body = null; }; /** - * Get the group id for an item - * @param {Object} itemData - * @returns {string} Returns the groupId - * @private + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] */ - ItemSet.prototype._getGroupId = function (itemData) { - var type = this._getType(itemData); - if (type == 'background' && itemData.group == undefined) { - return BACKGROUND; - } else { - return this.groupsData ? itemData.group : UNGROUPED; + CurrentTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); } }; /** - * Handle updated items - * @param {Number[]} ids - * @protected + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - ItemSet.prototype._onUpdate = function (ids) { - var me = this; + CurrentTime.prototype.redraw = function () { + if (this.options.showCurrentTime) { + var parent = this.body.dom.backgroundVertical; + if (this.bar.parentNode != parent) { + // attach to the dom + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + parent.appendChild(this.bar); - ids.forEach((function (id) { - var itemData = me.itemsData.get(id, me.itemOptions); - var item = me.items[id]; - var type = me._getType(itemData); + this.start(); + } - var constructor = ItemSet.types[type]; - var selected; + var now = new Date(new Date().valueOf() + this.offset); + var x = this.body.util.toScreen(now); - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, delete the item and recreate it - selected = item.selected; // preserve selection of this item - me._removeItem(item); - item = null; - } else { - me._updateItem(item, itemData); + var locale = this.options.locales[this.options.locale]; + if (!locale) { + if (!this.warned) { + console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); + this.warned = true; } + locale = this.options.locales['en']; // fall back on english when not available } + var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); + title = title.charAt(0).toUpperCase() + title.substring(1); - if (!item) { - // create item - if (constructor) { - item = new constructor(itemData, me.conversion, me.options); - item.id = id; // TODO: not so nice setting id afterwards - me._addItem(item); - if (selected) { - this.selection.push(id); - item.select(); - } - } else if (type == 'rangeoverflow') { - // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day - throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' + '.vis-item.vis-range .vis-item-content {overflow: visible;}'); - } else { - throw new TypeError('Unknown item type "' + type + '"'); - } + this.bar.style.left = x + 'px'; + this.bar.title = title; + } else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); } - }).bind(this)); + this.stop(); + } - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change', { queue: true }); + return false; }; /** - * Handle added items - * @param {Number[]} ids - * @protected - */ - ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; - - /** - * Handle removed items - * @param {Number[]} ids - * @protected + * Start auto refreshing the current time bar */ - ItemSet.prototype._onRemove = function (ids) { - var count = 0; + CurrentTime.prototype.start = function () { var me = this; - ids.forEach(function (id) { - var item = me.items[id]; - if (item) { - count++; - me._removeItem(item); - } - }); - if (count) { - // update order - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change', { queue: true }); + function update() { + me.stop(); + + // determine interval to refresh + var scale = me.body.range.conversion(me.body.domProps.center.width).scale; + var interval = 1 / scale / 10; + if (interval < 30) interval = 30; + if (interval > 1000) interval = 1000; + + me.redraw(); + + // start a renderTimer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); } - }; - /** - * Update the order of item in all groups - * @private - */ - ItemSet.prototype._order = function () { - // reorder the items in all groups - // TODO: optimization: only reorder groups affected by the changed items - util.forEach(this.groups, function (group) { - group.order(); - }); + update(); }; /** - * Handle updated groups - * @param {Number[]} ids - * @private + * Stop auto refreshing the current time bar */ - ItemSet.prototype._onUpdateGroups = function (ids) { - this._onAddGroups(ids); + CurrentTime.prototype.stop = function () { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } }; /** - * Handle changed groups (added or updated) - * @param {Number[]} ids - * @private + * Set a current time. This can be used for example to ensure that a client's + * time is synchronized with a shared server time. + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. */ - ItemSet.prototype._onAddGroups = function (ids) { - var me = this; + CurrentTime.prototype.setCurrentTime = function (time) { + var t = util.convert(time, 'Date').valueOf(); + var now = new Date().valueOf(); + this.offset = t - now; + this.redraw(); + }; - ids.forEach(function (id) { - var groupData = me.groupsData.get(id); - var group = me.groups[id]; + /** + * Get the current time. + * @return {Date} Returns the current time. + */ + CurrentTime.prototype.getCurrentTime = function () { + return new Date(new Date().valueOf() + this.offset); + }; - if (!group) { - // check for reserved ids - if (id == UNGROUPED || id == BACKGROUND) { - throw new Error('Illegal group id. ' + id + ' is a reserved id.'); - } + module.exports = CurrentTime; - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null - }); +/***/ }, +/* 33 */ +/***/ function(module, exports, __webpack_require__) { - group = new Group(id, groupData, me); - me.groups[id] = group; + /** + * Prototype for visual components + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] + * @param {Object} [options] + */ + "use strict"; - // add items with this groupId to the new group - for (var itemId in me.items) { - if (me.items.hasOwnProperty(itemId)) { - var item = me.items[itemId]; - if (item.data.group == id) { - group.add(item); - } - } - } + function Component(body, options) { + this.options = null; + this.props = null; + } - group.order(); - group.show(); - } else { - // update group - group.setData(groupData); - } - }); + /** + * Set options for the component. The new options will be merged into the + * current options. + * @param {Object} options + */ + Component.prototype.setOptions = function (options) { + if (options) { + util.extend(this.options, options); + } + }; - this.body.emitter.emit('change', { queue: true }); + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + Component.prototype.redraw = function () { + // should be implemented by the component + return false; }; /** - * Handle removed groups - * @param {Number[]} ids - * @private + * Destroy the component. Cleanup DOM and event listeners */ - ItemSet.prototype._onRemoveGroups = function (ids) { - var groups = this.groups; - ids.forEach(function (id) { - var group = groups[id]; + Component.prototype.destroy = function () {}; - if (group) { - group.hide(); - delete groups[id]; - } - }); + /** + * Test whether the component is resized since the last time _isResized() was + * called. + * @return {Boolean} Returns true if the component is resized + * @protected + */ + Component.prototype._isResized = function () { + var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - this.markDirty(); + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - this.body.emitter.emit('change', { queue: true }); + return resized; }; - /** - * Reorder the groups if needed - * @return {boolean} changed - * @private - */ - ItemSet.prototype._orderGroups = function () { - if (this.groupsData) { - // reorder the groups - var groupIds = this.groupsData.getIds({ - order: this.options.groupOrder - }); + module.exports = Component; - var changed = !util.equalArray(groupIds, this.groupIds); - if (changed) { - // hide all groups, removes them from the DOM - var groups = this.groups; - groupIds.forEach(function (groupId) { - groups[groupId].hide(); - }); + // should be implemented by the component - // show the groups again, attach them to the DOM in correct order - groupIds.forEach(function (groupId) { - groups[groupId].show(); - }); +/***/ }, +/* 34 */ +/***/ function(module, exports, __webpack_require__) { - this.groupIds = groupIds; - } + // English + 'use strict'; - return changed; - } else { - return false; - } + exports['en'] = { + current: 'current', + time: 'time' }; + exports['en_EN'] = exports['en']; + exports['en_US'] = exports['en']; - /** - * Add a new item - * @param {Item} item - * @private - */ - ItemSet.prototype._addItem = function (item) { - this.items[item.id] = item; - - // add to group - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); + // Dutch + exports['nl'] = { + current: 'huidige', + time: 'tijd' }; + exports['nl_NL'] = exports['nl']; + exports['nl_BE'] = exports['nl']; + +/***/ }, +/* 35 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var util = __webpack_require__(14); + var hammerUtil = __webpack_require__(36); + var moment = __webpack_require__(15); + var Component = __webpack_require__(33); + var DateUtil = __webpack_require__(37); /** - * Update an existing item - * @param {Item} item - * @param {Object} itemData - * @private + * @constructor Range + * A Range controls a numeric range with a start and end value. + * The Range adjusts the range based on mouse events or programmatic changes, + * and triggers events when the range is changing or has been changed. + * @param {{dom: Object, domProps: Object, emitter: Emitter}} body + * @param {Object} [options] See description at Range.setOptions */ - ItemSet.prototype._updateItem = function (item, itemData) { - var oldGroupId = item.data.group; - var oldSubGroupId = item.data.subgroup; + function Range(body, options) { + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.start = now.clone().add(-3, 'days').valueOf(); // Number + this.end = now.clone().add(4, 'days').valueOf(); // Number - // update the items data (will redraw the item when displayed) - item.setData(itemData); + this.body = body; + this.deltaDifference = 0; + this.scaleOffset = 0; + this.startToFront = false; + this.endToFront = true; - // update group - if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); + // default options + this.defaultOptions = { + start: null, + end: null, + direction: 'horizontal', // 'horizontal' or 'vertical' + moveable: true, + zoomable: true, + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds + }; + this.options = util.extend({}, this.defaultOptions); - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); - } - }; + this.props = { + touch: {} + }; + this.animationTimer = null; - /** - * Delete an item from the ItemSet: remove it from the DOM, from the map - * with items, and from the map with visible items, and from the selection - * @param {Item} item - * @private - */ - ItemSet.prototype._removeItem = function (item) { - // remove from DOM - item.hide(); + // drag listeners for dragging + this.body.emitter.on('panstart', this._onDragStart.bind(this)); + this.body.emitter.on('panmove', this._onDrag.bind(this)); + this.body.emitter.on('panend', this._onDragEnd.bind(this)); - // remove from items - delete this.items[item.id]; + // mouse wheel for zooming + this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); - // remove from selection - var index = this.selection.indexOf(item.id); - if (index != -1) this.selection.splice(index, 1); + // pinch to zoom + this.body.emitter.on('touch', this._onTouch.bind(this)); + this.body.emitter.on('pinch', this._onPinch.bind(this)); - // remove from group - item.parent && item.parent.remove(item); - }; + this.setOptions(options); + } + + Range.prototype = new Component(); /** - * Create an array containing all items being a range (having an end date) - * @param array - * @returns {Array} - * @private + * Set options for the range controller + * @param {Object} options Available options: + * {Number | Date | String} start Start date for the range + * {Number | Date | String} end End date for the range + * {Number} min Minimum value for start + * {Number} max Maximum value for end + * {Number} zoomMin Set a minimum value for + * (end - start). + * {Number} zoomMax Set a maximum value for + * (end - start). + * {Boolean} moveable Enable moving of the range + * by dragging. True by default + * {Boolean} zoomable Enable zooming of the range + * by pinching/scrolling. True by default */ - ItemSet.prototype._constructByEndArray = function (array) { - var endArray = []; + Range.prototype.setOptions = function (options) { + if (options) { + // copy the options that we know + var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable', 'activate', 'hiddenDates']; + util.selectiveExtend(fields, this.options, options); - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof RangeItem) { - endArray.push(array[i]); + if ('start' in options || 'end' in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); } } - return endArray; }; /** - * Register the clicked item on touch, before dragStart is initiated. - * - * dragStart is initiated from a mousemove event, AFTER the mouse/touch is - * already moving. Therefore, the mouse/touch can sometimes be above an other - * DOM element than the item itself. - * - * @param {Event} event - * @private + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - ItemSet.prototype._onTouch = function (event) { - // store the touched item, used in _onDragStart - this.touchParams.item = this.itemFromTarget(event); - this.touchParams.dragLeftItem = event.target.dragLeftItem || false; - this.touchParams.dragRightItem = event.target.dragRightItem || false; - this.touchParams.itemProps = null; - }; + function validateDirection(direction) { + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + 'Choose "horizontal" or "vertical".'); + } + } /** - * Start dragging the selected events - * @param {Event} event - * @private + * Set a new start and end range + * @param {Date | Number | String} [start] + * @param {Date | Number | String} [end] + * @param {boolean | {duration: number, easingFunction: string}} [animation=false] + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + * @param {Boolean} [byUser=false] + * */ - ItemSet.prototype._onDragStart = function (event) { - var item = this.touchParams.item || null; - var me = this; - var props; - - if (item && item.selected) { - - if (!this.options.editable.updateTime && !this.options.editable.updateGroup && !item.editable) { - return; - } + Range.prototype.setRange = function (start, end, animation, byUser) { + if (byUser !== true) { + byUser = false; + } + var finalStart = start != undefined ? util.convert(start, 'Date').valueOf() : null; + var finalEnd = end != undefined ? util.convert(end, 'Date').valueOf() : null; + this._cancelAnimation(); - // override options.editable - if (item.editable === false) { - return; + if (animation) { + // true or an Object + var me = this; + var initStart = this.start; + var initEnd = this.end; + var duration = typeof animation === 'object' && 'duration' in animation ? animation.duration : 500; + var easingName = typeof animation === 'object' && 'easingFunction' in animation ? animation.easingFunction : 'easeInOutQuad'; + var easingFunction = util.easingFunctions[easingName]; + if (!easingFunction) { + throw new Error('Unknown easing function ' + JSON.stringify(easingName) + '. ' + 'Choose from: ' + Object.keys(util.easingFunctions).join(', ')); } - var dragLeftItem = this.touchParams.dragLeftItem; - var dragRightItem = this.touchParams.dragRightItem; + var initTime = new Date().valueOf(); + var anyChanged = false; - if (dragLeftItem) { - props = { - item: dragLeftItem, - initialX: event.center.x, - dragLeft: true, - data: util.extend({}, item.data) // clone the items data - }; + var next = function next() { + if (!me.props.touch.dragging) { + var now = new Date().valueOf(); + var time = now - initTime; + var ease = easingFunction(time / duration); + var done = time > duration; + var s = done || finalStart === null ? finalStart : initStart + (finalStart - initStart) * ease; + var e = done || finalEnd === null ? finalEnd : initEnd + (finalEnd - initEnd) * ease; - this.touchParams.itemProps = [props]; - } else if (dragRightItem) { - props = { - item: dragRightItem, - initialX: event.center.x, - dragRight: true, - data: util.extend({}, item.data) // clone the items data - }; + changed = me._applyRange(s, e); + DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); + anyChanged = anyChanged || changed; + if (changed) { + me.body.emitter.emit('rangechange', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + } - this.touchParams.itemProps = [props]; - } else { - this.touchParams.itemProps = this.getSelection().map(function (id) { - var item = me.items[id]; - var props = { - item: item, - initialX: event.center.x, - data: util.extend({}, item.data) // clone the items data - }; + if (done) { + if (anyChanged) { + me.body.emitter.emit('rangechanged', { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + } + } else { + // animate with as high as possible frame rate, leave 20 ms in between + // each to prevent the browser from blocking + me.animationTimer = setTimeout(next, 20); + } + } + }; - return props; - }); + return next(); + } else { + var changed = this._applyRange(finalStart, finalEnd); + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + if (changed) { + var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; + this.body.emitter.emit('rangechange', params); + this.body.emitter.emit('rangechanged', params); } - - event.stopPropagation(); - } else if (this.options.editable.add && (event.srcEvent.ctrlKey || event.srcEvent.metaKey)) { - // create a new range item when dragging with ctrl key down - this._onDragStartAddItem(event); } }; /** - * Start creating a new range item by dragging. - * @param {Event} event + * Stop an animation * @private */ - ItemSet.prototype._onDragStartAddItem = function (event) { - var snap = this.options.snap || null; - var xAbs = util.getAbsoluteLeft(this.dom.frame); - var x = event.center.x - xAbs - 10; // minus 10 to compensate for the drag starting as soon as you've moved 10px - var time = this.body.util.toTime(x); - var scale = this.body.util.getScale(); - var step = this.body.util.getStep(); - var start = snap ? snap(time, scale, step) : start; - var end = start; - - var itemData = { - type: 'range', - start: start, - end: end, - content: 'new item' - }; - - var id = util.randomUUID(); - itemData[this.itemsData._fieldId] = id; - - var group = this.groupFromTarget(event); - if (group) { - itemData.group = group.groupId; + Range.prototype._cancelAnimation = function () { + if (this.animationTimer) { + clearTimeout(this.animationTimer); + this.animationTimer = null; } - - var newItem = new RangeItem(itemData, this.conversion, this.options); - newItem.id = id; // TODO: not so nice setting id afterwards - newItem.data = itemData; - this._addItem(newItem); - - var props = { - item: newItem, - dragRight: true, - initialX: event.center.x, - data: util.extend({}, itemData) - }; - this.touchParams.itemProps = [props]; - - event.stopPropagation(); }; /** - * Drag selected items - * @param {Event} event + * Set a new start and end range. This method is the same as setRange, but + * does not trigger a range change and range changed event, and it returns + * true when the range is changed + * @param {Number} [start] + * @param {Number} [end] + * @return {Boolean} changed * @private */ - ItemSet.prototype._onDrag = function (event) { - if (this.touchParams.itemProps) { - event.stopPropagation(); - - var me = this; - var snap = this.options.snap || null; - var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; - var scale = this.body.util.getScale(); - var step = this.body.util.getStep(); - - // move - this.touchParams.itemProps.forEach(function (props) { - var newProps = {}; - var current = me.body.util.toTime(event.center.x - xOffset); - var initial = me.body.util.toTime(props.initialX - xOffset); - var offset = current - initial; - - var itemData = util.extend({}, props.item.data); // clone the data - - if (props.item.editable === false) { - return; - } + Range.prototype._applyRange = function (start, end) { + var newStart = start != null ? util.convert(start, 'Date').valueOf() : this.start, + newEnd = end != null ? util.convert(end, 'Date').valueOf() : this.end, + max = this.options.max != null ? util.convert(this.options.max, 'Date').valueOf() : null, + min = this.options.min != null ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; - var updateTimeAllowed = me.options.editable.updateTime || props.item.editable === true; + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error('Invalid start "' + start + '"'); + } + if (isNaN(newEnd) || newEnd === null) { + throw new Error('Invalid end "' + end + '"'); + } - if (updateTimeAllowed) { - if (props.dragLeft) { - // drag left side of a range item - if (itemData.start != undefined) { - var initialStart = util.convert(props.data.start, 'Date'); - var start = new Date(initialStart.valueOf() + offset); - itemData.start = snap ? snap(start, scale, step) : start; - } - } else if (props.dragRight) { - // drag right side of a range item - if (itemData.end != undefined) { - var initialEnd = util.convert(props.data.end, 'Date'); - var end = new Date(initialEnd.valueOf() + offset); - itemData.end = snap ? snap(end, scale, step) : end; - } - } else { - // drag both start and end - if (itemData.start != undefined) { - var initialStart = util.convert(props.data.start, 'Date').valueOf(); - var start = new Date(initialStart + offset); + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } - if (itemData.end != undefined) { - var initialEnd = util.convert(props.data.end, 'Date'); - var duration = initialEnd.valueOf() - initialStart.valueOf(); + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = min - newStart; + newStart += diff; + newEnd += diff; - itemData.start = snap ? snap(start, scale, step) : start; - itemData.end = new Date(itemData.start.valueOf() + duration); - } else { - itemData.start = snap ? snap(start, scale, step) : start; - } - } + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; } } + } + } - var updateGroupAllowed = me.options.editable.updateGroup || props.item.editable === true; + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = newEnd - max; + newStart -= diff; + newEnd -= diff; - if (updateGroupAllowed && (!props.dragLeft && !props.dragRight)) { - if (itemData.group != undefined) { - // drag from one group to another - var group = me.groupFromTarget(event); - if (group) { - itemData.group = group.groupId; - } + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; } } + } + } - // confirm moving the item - me.options.onMoving(itemData, function (itemData) { - if (itemData) { - props.item.setData(itemData); - } - }); - }); + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; + } + if (newEnd - newStart < zoomMin) { + if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } else { + // zoom to the minimum + diff = zoomMin - (newEnd - newStart); + newStart -= diff / 2; + newEnd += diff / 2; + } + } + } - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } + + if (newEnd - newStart > zoomMax) { + if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } else { + // zoom to the maximum + diff = newEnd - newStart - zoomMax; + newStart += diff / 2; + newEnd -= diff / 2; + } + } + } + + var changed = this.start != newStart || this.end != newEnd; + + // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) + if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { + this.body.emitter.emit('checkRangedItems'); } + + this.start = newStart; + this.end = newEnd; + return changed; }; /** - * Move an item to another group - * @param {Item} item - * @param {String | Number} groupId - * @private + * Retrieve the current range. + * @return {Object} An object with start and end properties */ - ItemSet.prototype._moveToGroup = function (item, groupId) { - var group = this.groups[groupId]; - if (group && group.groupId != item.data.group) { - var oldGroup = item.parent; - oldGroup.remove(item); - oldGroup.order(); - group.add(item); - group.order(); + Range.prototype.getRange = function () { + return { + start: this.start, + end: this.end + }; + }; - item.data.group = group.groupId; + /** + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.prototype.conversion = function (width, totalHidden) { + return Range.conversion(this.start, this.end, width, totalHidden); + }; + + /** + * Static method to calculate the conversion offset and scale for a range, + * based on the provided start, end, and width + * @param {Number} start + * @param {Number} end + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.conversion = function (start, end, width, totalHidden) { + if (totalHidden === undefined) { + totalHidden = 0; + } + if (width != 0 && end - start != 0) { + return { + offset: start, + scale: width / (end - start - totalHidden) + }; + } else { + return { + offset: 0, + scale: 1 + }; } }; /** - * End of dragging selected items + * Start dragging horizontally or vertically * @param {Event} event * @private */ - ItemSet.prototype._onDragEnd = function (event) { - if (this.touchParams.itemProps) { - event.stopPropagation(); - - // prepare a change set for the changed items - var changes = []; - var me = this; - var dataset = this.itemsData.getDataSet(); - - var itemProps = this.touchParams.itemProps; - this.touchParams.itemProps = null; - itemProps.forEach(function (props) { - var id = props.item.id; - var exists = me.itemsData.get(id, me.itemOptions) != null; - - if (!exists) { - // add a new item - me.options.onAdd(props.item.data, function (itemData) { - me._removeItem(props.item); // remove temporary item - if (itemData) { - me.itemsData.getDataSet().add(itemData); - } + Range.prototype._onDragStart = function (event) { + this.deltaDifference = 0; + this.previousDelta = 0; + // only allow dragging when configured as movable + if (!this.options.moveable) return; - // force re-stacking of all items next redraw - me.stackDirty = true; - me.body.emitter.emit('change'); - }); - } else { - // update existing item - var itemData = util.extend({}, props.item.data); // clone the data - me.options.onMove(itemData, function (itemData) { - if (itemData) { - // apply changes - itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) - changes.push(itemData); - } else { - // restore original values - props.item.setData(props.data); + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - me.stackDirty = true; // force re-stacking of all items next redraw - me.body.emitter.emit('change'); - } - }); - } - }); + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.dragging = true; - // apply the changes to the data (if there are changes) - if (changes.length) { - dataset.update(changes); - } + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'move'; } }; /** - * Handle selecting/deselecting an item when tapping it + * Perform dragging operation * @param {Event} event * @private */ - ItemSet.prototype._onSelectItem = function (event) { - if (!this.options.selectable) return; + Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); - var shiftKey = event.srcEvent && event.srcEvent.shiftKey; - if (ctrlKey || shiftKey) { - this._onMultiSelectItem(event); - return; - } + // TODO: this may be redundant in hammerjs2 + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - var oldSelection = this.getSelection(); + var direction = this.options.direction; + validateDirection(direction); + var delta = direction == 'horizontal' ? event.deltaX : event.deltaY; + delta -= this.deltaDifference; + var interval = this.props.touch.end - this.props.touch.start; - var item = this.itemFromTarget(event); - var selection = item ? [item.id] : []; - this.setSelection(selection); + // normalize dragging speed if cutout is in between. + var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + interval -= duration; - var newSelection = this.getSelection(); + 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; + var newEnd = this.props.touch.end + diffRange; - // emit a select event, - // except when old selection is empty and new selection is still empty - if (newSelection.length > 0 || oldSelection.length > 0) { - this.body.emitter.emit('select', { - items: newSelection, - event: event - }); + // snapping times away from hidden zones + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); + if (safeStart != newStart || safeEnd != newEnd) { + this.deltaDifference += delta; + this.props.touch.start = safeStart; + this.props.touch.end = safeEnd; + this._onDrag(event); + return; } + + this.previousDelta = delta; + this._applyRange(newStart, newEnd); + + // fire a rangechange event + this.body.emitter.emit('rangechange', { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); }; /** - * Handle creation and updates of an item on double tap - * @param event + * Stop dragging operation + * @param {event} event * @private */ - ItemSet.prototype._onAddItem = function (event) { - if (!this.options.selectable) return; - if (!this.options.editable.add) return; - - var me = this; - var snap = this.options.snap || null; - var item = this.itemFromTarget(event); + Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - event.stopPropagation(); + // TODO: this may be redundant in hammerjs2 + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - if (item) { - // update item + this.props.touch.dragging = false; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'auto'; + } - // execute async handler to update the item (or cancel it) - var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset - this.options.onUpdate(itemData, function (itemData) { - if (itemData) { - me.itemsData.getDataSet().update(itemData); - } - }); - } else { - // add item - var xAbs = util.getAbsoluteLeft(this.dom.frame); - var x = event.center.x - xAbs; - var start = this.body.util.toTime(x); - var scale = this.body.util.getScale(); - var step = this.body.util.getStep(); + // fire a rangechanged event + this.body.emitter.emit('rangechanged', { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); + }; - var newItem = { - start: snap ? snap(start, scale, step) : start, - content: 'new item' - }; + /** + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event + * @private + */ + Range.prototype._onMouseWheel = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - // when default type is a range, add a default end date to the new item - if (this.options.type === 'range') { - var end = this.body.util.toTime(x + this.props.width / 5); - newItem.end = snap ? snap(end, scale, step) : end; - } + // retrieve delta + var delta = 0; + if (event.wheelDelta) { + /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { + /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; + } - newItem[this.itemsData._fieldId] = util.randomUUID(); + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // perform the zoom action. Delta is normally 1 or -1 - var group = this.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - delta / 5; + } else { + scale = 1 / (1 + delta / 5); } - // execute async handler to customize (or cancel) adding an item - this.options.onAdd(newItem, function (item) { - if (item) { - me.itemsData.getDataSet().add(item); - // TODO: need to trigger a redraw? - } - }); + // calculate center, the date to zoom around + var pointer = getPointer({ x: event.clientX, y: event.clientY }, this.body.dom.center); + var pointerDate = this._pointerToDate(pointer); + + this.zoom(scale, pointerDate, delta); } + + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); }; /** - * Handle selecting/deselecting multiple items when holding an item + * Start of a touch gesture + * @private + */ + Range.prototype._onTouch = function (event) { + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.allowDragging = true; + this.props.touch.center = null; + this.scaleOffset = 0; + this.deltaDifference = 0; + }; + + /** + * Handle pinch event * @param {Event} event * @private */ - ItemSet.prototype._onMultiSelectItem = function (event) { - if (!this.options.selectable) return; + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - var item = this.itemFromTarget(event); + this.props.touch.allowDragging = false; - if (item) { - // multi select items (if allowed) + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.center, this.body.dom.center); + } - var selection = this.options.multiselect ? this.getSelection() // take current selection - : []; // deselect current selection + var scale = 1 / (event.scale + this.scaleOffset); + var centerDate = this._pointerToDate(this.props.touch.center); - var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - if (shiftKey && this.options.multiselect) { - // select all items between the old selection and the tapped item + // calculate new start and end + var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; + var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; - // determine the selection range - selection.push(item.id); - var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + // snapping times away from hidden zones + this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times + this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times - // select all items within the selection range - selection = []; - for (var id in this.items) { - if (this.items.hasOwnProperty(id)) { - var _item = this.items[id]; - var start = _item.data.start; - var end = _item.data.end !== undefined ? _item.data.end : start; + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); + if (safeStart != newStart || safeEnd != newEnd) { + this.props.touch.start = safeStart; + this.props.touch.end = safeEnd; + this.scaleOffset = 1 - event.scale; + newStart = safeStart; + newEnd = safeEnd; + } - if (start >= range.min && end <= range.max && !(_item instanceof BackgroundItem)) { - selection.push(_item.id); // do not use id but item.id, id itself is stringified - } - } - } - } else { - // add/remove this item from the current selection - var index = selection.indexOf(item.id); - if (index == -1) { - // item is not yet selected -> select it - selection.push(item.id); - } else { - // item is already selected -> deselect it - selection.splice(index, 1); - } - } + this.setRange(newStart, newEnd, false, true); - this.setSelection(selection); + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default + }; - this.body.emitter.emit('select', { - items: this.getSelection(), - event: event - }); + /** + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date + * @private + */ + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; + + validateDirection(direction); + + if (direction == 'horizontal') { + return this.body.util.toTime(pointer.x).valueOf(); + } else { + var height = this.body.domProps.center.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; } }; /** - * Calculate the time range of a list of items - * @param {Array.} itemsData - * @return {{min: Date, max: Date}} Returns the range of the provided items + * Get the pointer location relative to the location of the dom element + * @param {{x: Number, y: Number}} touch + * @param {Element} element HTML DOM element + * @return {{x: Number, y: Number}} pointer * @private */ - ItemSet._getItemRange = function (itemsData) { - var max = null; - var min = null; - - itemsData.forEach(function (data) { - if (min == null || data.start < min) { - min = data.start; - } - - if (data.end != undefined) { - if (max == null || data.end > max) { - max = data.end; - } - } else { - if (max == null || data.start > max) { - max = data.start; - } - } - }); - + function getPointer(touch, element) { return { - min: min, - max: max + x: touch.x - util.getAbsoluteLeft(element), + y: touch.y - util.getAbsoluteTop(element) }; - }; + } /** - * Find an item from an event target: - * searches for the attribute 'timeline-item' in the event target's element tree - * @param {Event} event - * @return {Item | null} item + * Zoom the range the given scale in or out. Start and end date will + * be adjusted, and the timeline will be redrawn. You can optionally give a + * date around which to zoom. + * For example, try scale = 0.9 or 1.1 + * @param {Number} scale Scaling factor. Values above 1 will zoom out, + * values below 1 will zoom in. + * @param {Number} [center] Value representing a date around which will + * be zoomed. */ - ItemSet.prototype.itemFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; - } - target = target.parentNode; + Range.prototype.zoom = function (scale, center, delta) { + // if centerDate is not provided, take it half between start Date and end Date + if (center == null) { + center = (this.start + this.end) / 2; } - return null; + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + + // calculate new start and end + var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; + var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; + + // snapping times away from hidden zones + this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); + if (safeStart != newStart || safeEnd != newEnd) { + newStart = safeStart; + newEnd = safeEnd; + } + + this.setRange(newStart, newEnd, false, true); + + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default }; /** - * Find the Group from an event target: - * searches for the attribute 'timeline-group' in the event target's element tree - * @param {Event} event - * @return {Group | null} group + * Move the range with a given delta to the left or right. Start and end + * value will be adjusted. For example, try delta = 0.1 or -0.1 + * @param {Number} delta Moving amount. Positive value will move right, + * negative value will move left */ - ItemSet.prototype.groupFromTarget = function (event) { - var clientY = event.center ? event.center.y : event.clientY; - for (var i = 0; i < this.groupIds.length; i++) { - var groupId = this.groupIds[i]; - var group = this.groups[groupId]; - var foreground = group.dom.foreground; - var top = util.getAbsoluteTop(foreground); - if (clientY > top && clientY < top + foreground.offsetHeight) { - return group; - } + Range.prototype.move = function (delta) { + // zoom start Date and end Date relative to the centerDate + var diff = this.end - this.start; - if (this.options.orientation.item === 'top') { - if (i === this.groupIds.length - 1 && clientY > top) { - return group; - } - } else { - if (i === 0 && clientY < top + foreground.offset) { - return group; - } - } - } + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; - return null; + // TODO: reckon with min and max range + + this.start = newStart; + this.end = newEnd; }; /** - * Find the ItemSet from an event target: - * searches for the attribute 'timeline-itemset' in the event target's element tree - * @param {Event} event - * @return {ItemSet | null} item + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ - ItemSet.itemSetFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-itemset')) { - return target['timeline-itemset']; - } - target = target.parentNode; - } + Range.prototype.moveTo = function (moveTo) { + var center = (this.start + this.end) / 2; - return null; + var diff = center - moveTo; + + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); }; - module.exports = ItemSet; + module.exports = Range; /***/ }, -/* 35 */ +/* 36 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(9); - var stack = __webpack_require__(36); - var RangeItem = __webpack_require__(37); + var Hammer = __webpack_require__(10); /** - * @constructor Group - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet + * Register a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - function Group(groupId, data, itemSet) { - this.groupId = groupId; - this.subgroups = {}; - this.subgroupIndex = 0; - this.subgroupOrderer = data && data.subgroupOrder; - this.itemSet = itemSet; + exports.onTouch = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFirst && !isTouching) { + callback(event); - this.dom = {}; - this.props = { - label: { - width: 0, - height: 0 + isTouching = true; + setTimeout(function () { + isTouching = false; + }, 0); } }; - this.className = null; - - this.items = {}; // items filtered by groupId of this group - this.visibleItems = []; // items currently visible in window - this.orderedItems = { - byStart: [], - byEnd: [] - }; - this.checkRangedItems = false; // needed to refresh the ranged items if the window is programatically changed with NO overlap. - var me = this; - this.itemSet.body.emitter.on('checkRangedItems', function () { - me.checkRangedItems = true; - }); - this._create(); + hammer.on('hammer.input', callback.inputHandler); + }; - this.setData(data); - } + // isTouching is true while a touch action is being emitted + // this is a hack to prevent `touch` from being fired twice + var isTouching = false; /** - * Create DOM elements for the group - * @private + * Register a release event, taking place after a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - Group.prototype._create = function () { - var label = document.createElement('div'); - label.className = 'vis-label'; - this.dom.label = label; - - var inner = document.createElement('div'); - inner.className = 'vis-inner'; - label.appendChild(inner); - this.dom.inner = inner; + exports.onRelease = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFinal && !isReleasing) { + callback(event); - var foreground = document.createElement('div'); - foreground.className = 'vis-group'; - foreground['timeline-group'] = this; - this.dom.foreground = foreground; + isReleasing = true; + setTimeout(function () { + isReleasing = false; + }, 0); + } + }; - this.dom.background = document.createElement('div'); - this.dom.background.className = 'vis-group'; + return hammer.on('hammer.input', callback.inputHandler); + }; - this.dom.axis = document.createElement('div'); - this.dom.axis.className = 'vis-group'; + // isReleasing is true while a release action is being emitted + // this is a hack to prevent `release` from being fired twice + var isReleasing = false; - // create a hidden marker to detect when the Timelines container is attached - // to the DOM, or the style of a parent of the Timeline is changed from - // display:none is changed to visible. - this.dom.marker = document.createElement('div'); - this.dom.marker.style.visibility = 'hidden'; - this.dom.marker.innerHTML = '?'; - this.dom.background.appendChild(this.dom.marker); + /** + * Unregister a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.offTouch = function (hammer, callback) { + hammer.off('hammer.input', callback.inputHandler); }; /** - * Set the group data for this group - * @param {Object} data Group data, can contain properties content and className + * Unregister a release event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - 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 - } - - // update title - this.dom.label.title = data && data.title || ''; + exports.offRelease = exports.offTouch; - if (!this.dom.inner.firstChild) { - util.addClassName(this.dom.inner, 'vis-hidden'); - } else { - util.removeClassName(this.dom.inner, 'vis-hidden'); - } +/***/ }, +/* 37 */ +/***/ function(module, exports, __webpack_require__) { - // 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; - } + "use strict"; - // update style - if (this.style) { - util.removeCssText(this.dom.label, 'vis-' + this.style); - this.style = null; - } - if (data && data.style) { - util.addCssText(this.dom.label, 'vis-' + data.style); - this.style = data.style; - } - }; + var moment = __webpack_require__(15); /** - * Get the width of the group label - * @return {number} width + * used in Core to convert the options into a volatile variable + * + * @param Core */ - Group.prototype.getLabelWidth = function () { - return this.props.label.width; + exports.convertHiddenOptions = function (body, hiddenDates) { + body.hiddenDates = []; + if (hiddenDates) { + if (Array.isArray(hiddenDates) == true) { + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].repeat === undefined) { + var dateItem = {}; + dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); + dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); + body.hiddenDates.push(dateItem); + } + } + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time + } + } }; /** - * 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 + * create new entrees for the repeating hidden dates + * @param body + * @param hiddenDates */ - Group.prototype.redraw = function (range, margin, restack) { - var resized = false; - - // 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(); - }); + exports.updateHiddenDates = function (body, hiddenDates) { + if (hiddenDates && body.domProps.centerContainer.width !== undefined) { + exports.convertHiddenOptions(body, hiddenDates); - restack = true; - } + var start = moment(body.range.start); + var end = moment(body.range.end); - // reposition visible items vertically - if (typeof this.itemSet.options.order === 'function') { - // a custom order function + var totalRange = body.range.end - body.range.start; + var pixelTime = totalRange / body.domProps.centerContainer.width; - if (restack) { - // brute force restack of all items + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].repeat !== undefined) { + var startDate = moment(hiddenDates[i].start); + var endDate = moment(hiddenDates[i].end); - // show all items - var me = this; - var limitSize = false; - util.forEach(this.items, function (item) { - if (!item.displayed) { - item.redraw(); - me.visibleItems.push(item); + if (startDate._d == "Invalid Date") { + throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); + } + if (endDate._d == "Invalid Date") { + throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); } - item.repositionX(limitSize); - }); - - // order all items and force a restacking - var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) { - return me.itemSet.options.order(a.data, b.data); - }); - stack.stack(customOrderedItems, margin, true /* restack=true */); - } - - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - } else { - // no custom order function, lazy stacking - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - - 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 = this._calculateHeight(margin); - // 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; + var duration = endDate - startDate; + if (duration >= 4 * pixelTime) { - // 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; + var offset = 0; + var runUntil = end.clone(); + switch (hiddenDates[i].repeat) { + case "daily": + // case of time + if (startDate.day() != endDate.day()) { + offset = 1; + } + startDate.dayOfYear(start.dayOfYear()); + startDate.year(start.year()); + startDate.subtract(7, "days"); - // apply new height - this.dom.background.style.height = height + 'px'; - this.dom.foreground.style.height = height + 'px'; - this.dom.label.style.height = height + 'px'; + endDate.dayOfYear(start.dayOfYear()); + endDate.year(start.year()); + endDate.subtract(7 - offset, "days"); - // 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); - } + runUntil.add(1, "weeks"); + break; + case "weekly": + var dayOffset = endDate.diff(startDate, "days"); + var day = startDate.day(); - return resized; - }; + // set the start date to the range.start + startDate.date(start.date()); + startDate.month(start.month()); + startDate.year(start.year()); + endDate = startDate.clone(); - /** - * recalculate the height of the group - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @returns {number} Returns the height - * @private - */ - Group.prototype._calculateHeight = function (margin) { - // 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 > 0) { - 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 (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 = 0; - } - height = Math.max(height, this.props.label.height); + // force + startDate.day(day); + endDate.day(day); + endDate.add(dayOffset, "days"); - return height; - }; + startDate.subtract(1, "weeks"); + endDate.subtract(1, "weeks"); - /** - * Show this group: attach to the DOM - */ - Group.prototype.show = function () { - if (!this.dom.label.parentNode) { - this.itemSet.dom.labelSet.appendChild(this.dom.label); - } + runUntil.add(1, "weeks"); + break; + case "monthly": + if (startDate.month() != endDate.month()) { + offset = 1; + } + startDate.month(start.month()); + startDate.year(start.year()); + startDate.subtract(1, "months"); - if (!this.dom.foreground.parentNode) { - this.itemSet.dom.foreground.appendChild(this.dom.foreground); - } + endDate.month(start.month()); + endDate.year(start.year()); + endDate.subtract(1, "months"); + endDate.add(offset, "months"); - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } + runUntil.add(1, "months"); + break; + case "yearly": + if (startDate.year() != endDate.year()) { + offset = 1; + } + startDate.year(start.year()); + startDate.subtract(1, "years"); + endDate.year(start.year()); + endDate.subtract(1, "years"); + endDate.add(offset, "years"); - if (!this.dom.axis.parentNode) { - this.itemSet.dom.axis.appendChild(this.dom.axis); + runUntil.add(1, "years"); + break; + default: + console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); + return; + } + while (startDate < runUntil) { + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); + switch (hiddenDates[i].repeat) { + case "daily": + startDate.add(1, "days"); + endDate.add(1, "days"); + break; + case "weekly": + startDate.add(1, "weeks"); + endDate.add(1, "weeks"); + break; + case "monthly": + startDate.add(1, "months"); + endDate.add(1, "months"); + break; + case "yearly": + startDate.add(1, "y"); + endDate.add(1, "y"); + break; + default: + console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); + return; + } + } + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); + } + } + } + // remove duplicates, merge where possible + exports.removeDuplicates(body); + // ensure the new positions are not on hidden dates + var startHidden = exports.isHidden(body.range.start, body.hiddenDates); + var endHidden = exports.isHidden(body.range.end, body.hiddenDates); + var rangeStart = body.range.start; + var rangeEnd = body.range.end; + if (startHidden.hidden == true) { + rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; + } + if (endHidden.hidden == true) { + rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; + } + if (startHidden.hidden == true || endHidden.hidden == true) { + body.range._applyRange(rangeStart, rangeEnd); + } } }; /** - * Hide this group: remove from the DOM + * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. + * Scales with N^2 + * @param body */ - Group.prototype.hide = function () { - var label = this.dom.label; - if (label.parentNode) { - label.parentNode.removeChild(label); + exports.removeDuplicates = function (body) { + var hiddenDates = body.hiddenDates; + var safeDates = []; + for (var i = 0; i < hiddenDates.length; i++) { + for (var j = 0; j < hiddenDates.length; j++) { + if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { + // j inside i + if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[j].remove = true; + } + // j start inside i + else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { + hiddenDates[i].end = hiddenDates[j].end; + hiddenDates[j].remove = true; + } + // j end inside i + else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[i].start = hiddenDates[j].start; + hiddenDates[j].remove = true; + } + } + } } - var foreground = this.dom.foreground; - if (foreground.parentNode) { - foreground.parentNode.removeChild(foreground); + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].remove !== true) { + safeDates.push(hiddenDates[i]); + } } - var background = this.dom.background; - if (background.parentNode) { - background.parentNode.removeChild(background); - } + body.hiddenDates = safeDates; + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time + }; - var axis = this.dom.axis; - if (axis.parentNode) { - axis.parentNode.removeChild(axis); + exports.printDates = function (dates) { + for (var i = 0; i < dates.length; i++) { + console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); } }; /** - * Add an item to the group - * @param {Item} item + * Used in TimeStep to avoid the hidden times. + * @param timeStep + * @param previousTime */ - Group.prototype.add = function (item) { - this.items[item.id] = item; - item.setParent(this); - - // add to - if (item.data.subgroup !== undefined) { - if (this.subgroups[item.data.subgroup] === undefined) { - this.subgroups[item.data.subgroup] = { height: 0, visible: false, index: this.subgroupIndex, items: [] }; - this.subgroupIndex++; + exports.stepOverHiddenDates = function (timeStep, previousTime) { + var stepInHidden = false; + var currentValue = timeStep.current.valueOf(); + for (var i = 0; i < timeStep.hiddenDates.length; i++) { + var startDate = timeStep.hiddenDates[i].start; + var endDate = timeStep.hiddenDates[i].end; + if (currentValue >= startDate && currentValue < endDate) { + stepInHidden = true; + break; } - this.subgroups[item.data.subgroup].items.push(item); } - this.orderSubgroups(); - - 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); - } - }; - Group.prototype.orderSubgroups = function () { - if (this.subgroupOrderer !== undefined) { - var sortArray = []; - if (typeof this.subgroupOrderer == 'string') { - for (var subgroup in this.subgroups) { - sortArray.push({ subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer] }); - } - sortArray.sort(function (a, b) { - return a.sortField - b.sortField; - }); - } else if (typeof this.subgroupOrderer == 'function') { - for (var subgroup in this.subgroups) { - sortArray.push(this.subgroups[subgroup].items[0].data); - } - sortArray.sort(this.subgroupOrderer); + if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { + var prevValue = moment(previousTime); + var newValue = moment(endDate); + //check if the next step should be major + if (prevValue.year() != newValue.year()) { + timeStep.switchedYear = true; + } else if (prevValue.month() != newValue.month()) { + timeStep.switchedMonth = true; + } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { + timeStep.switchedDay = true; } - if (sortArray.length > 0) { - for (var i = 0; i < sortArray.length; i++) { - this.subgroups[sortArray[i].subgroup].index = i; - } - } + timeStep.current = newValue.toDate(); } }; - Group.prototype.resetSubgroups = function () { - for (var subgroup in this.subgroups) { - if (this.subgroups.hasOwnProperty(subgroup)) { - this.subgroups[subgroup].visible = false; - } - } - }; + ///** + // * Used in TimeStep to avoid the hidden times. + // * @param timeStep + // * @param previousTime + // */ + //exports.checkFirstStep = function(timeStep) { + // var stepInHidden = false; + // var currentValue = timeStep.current.valueOf(); + // for (var i = 0; i < timeStep.hiddenDates.length; i++) { + // var startDate = timeStep.hiddenDates[i].start; + // var endDate = timeStep.hiddenDates[i].end; + // if (currentValue >= startDate && currentValue < endDate) { + // stepInHidden = true; + // break; + // } + // } + // + // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) { + // var newValue = moment(endDate); + // timeStep.current = newValue.toDate(); + // } + //}; /** - * Remove an item from the group - * @param {Item} item + * replaces the Core toScreen methods + * @param Core + * @param time + * @param width + * @returns {number} */ - Group.prototype.remove = function (item) { - delete this.items[item.id]; - item.setParent(null); + exports.toScreen = function (Core, time, width) { + if (Core.body.hiddenDates.length == 0) { + var conversion = Core.range.conversion(width); + return (time.valueOf() - conversion.offset) * conversion.scale; + } else { + var hidden = exports.isHidden(time, Core.body.hiddenDates); + if (hidden.hidden == true) { + time = hidden.startDate; + } - // remove from visible items - var index = this.visibleItems.indexOf(item); - if (index != -1) this.visibleItems.splice(index, 1); + var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); - // TODO: also remove from ordered items? + var conversion = Core.range.conversion(width, duration); + return (time.valueOf() - conversion.offset) * conversion.scale; + } }; /** - * Remove an item from the corresponding DataSet - * @param {Item} item + * Replaces the core toTime methods + * @param body + * @param range + * @param x + * @param width + * @returns {Date} */ - Group.prototype.removeFromDataSet = function (item) { - this.itemSet.removeItem(item.id); + exports.toTime = function (Core, x, width) { + if (Core.body.hiddenDates.length == 0) { + var conversion = Core.range.conversion(width); + return new Date(x / conversion.scale + conversion.offset); + } else { + var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + var totalDuration = Core.range.end - Core.range.start - hiddenDuration; + var partialDuration = totalDuration * x / width; + var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); + + var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); + return newTime; + } }; /** - * Reorder the items + * Support function + * + * @param hiddenDates + * @param range + * @returns {number} */ - Group.prototype.order = function () { - var array = util.toArray(this.items); - var startArray = []; - var endArray = []; - - for (var i = 0; i < array.length; i++) { - if (array[i].data.end !== undefined) { - endArray.push(array[i]); + exports.getHiddenDurationBetween = function (hiddenDates, start, end) { + var duration = 0; + 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 >= start && endDate < end) { + duration += endDate - startDate; } - startArray.push(array[i]); } - this.orderedItems = { - byStart: startArray, - byEnd: endArray - }; - - stack.orderByStart(this.orderedItems.byStart); - stack.orderByEnd(this.orderedItems.byEnd); + return duration; }; /** - * 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 + * Support function + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} */ - Group.prototype._updateVisibleItems = function (orderedItems, oldVisibleItems, range) { - var visibleItems = []; - var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems - var interval = (range.end - range.start) / 4; - var lowerBound = range.start - interval; - var upperBound = range.end + interval; - var item, i; - - // this function is used to do the binary search. - var searchFunction = function searchFunction(value) { - if (value < lowerBound) { - return -1; - } else if (value <= upperBound) { - return 0; - } else { - return 1; - } - }; - - // first check if the items that were in view previously are still in view. - // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! - // also cleans up invisible items. - if (oldVisibleItems.length > 0) { - for (i = 0; i < oldVisibleItems.length; i++) { - this._checkIfVisibleWithReference(oldVisibleItems[i], visibleItems, visibleItemsLookup, range); - } - } - - // we do a binary search for the items that have only start values. - var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, 'data', 'start'); - - // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. - this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { - return item.data.start < lowerBound || item.data.start > upperBound; - }); - - // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. - // We therefore have to brute force check all items in the byEnd list - if (this.checkRangedItems == true) { - this.checkRangedItems = false; - for (i = 0; i < orderedItems.byEnd.length; i++) { - this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); - } - } else { - // we do a binary search for the items that have defined end times. - var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, 'data', 'end'); - - // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. - this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { - return item.data.end < lowerBound || item.data.end > upperBound; - }); - } - - // finally, we reposition all the visible items. - for (i = 0; i < visibleItems.length; i++) { - item = visibleItems[i]; - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); - } - - // debug - //console.log("new line") - //if (this.groupId == null) { - // for (i = 0; i < orderedItems.byStart.length; i++) { - // item = orderedItems.byStart[i].data; - // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") - // } - // for (i = 0; i < orderedItems.byEnd.length; i++) { - // item = orderedItems.byEnd[i].data; - // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") - // } - //} - - return visibleItems; + exports.correctTimeForHidden = function (hiddenDates, range, time) { + time = moment(time).toDate().valueOf(); + time -= exports.getHiddenDurationBefore(hiddenDates, range, time); + return time; }; - Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { - var item; - var i; + exports.getHiddenDurationBefore = function (hiddenDates, range, time) { + var timeOffset = 0; + time = moment(time).toDate().valueOf(); - if (initialPos != -1) { - for (i = initialPos; i >= 0; i--) { - item = items[i]; - if (breakCondition(item)) { - break; - } else { - if (visibleItemsLookup[item.id] === undefined) { - visibleItemsLookup[item.id] = true; - visibleItems.push(item); - } + 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) { + if (time >= endDate) { + timeOffset += endDate - startDate; } } + } + return timeOffset; + }; - for (i = initialPos + 1; i < items.length; i++) { - item = items[i]; - if (breakCondition(item)) { + /** + * sum the duration from start to finish, including the hidden duration, + * until the required amount has been reached, return the accumulated hidden duration + * @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 { - if (visibleItemsLookup[item.id] === undefined) { - visibleItemsLookup[item.id] = true; - visibleItems.push(item); - } + hiddenDuration += endDate - startDate; } } } + + return hiddenDuration; }; /** - * 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 + * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true + * @param hiddenDates + * @param time + * @param direction + * @param correctionEnabled + * @returns {*} */ - Group.prototype._checkIfVisible = function (item, visibleItems, range) { - if (item.isVisible(range)) { - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); - visibleItems.push(item); + exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { + var isHidden = exports.isHidden(time, hiddenDates); + if (isHidden.hidden == true) { + if (direction < 0) { + if (correctionEnabled == true) { + return isHidden.startDate - (isHidden.endDate - time) - 1; + } else { + return isHidden.startDate - 1; + } + } else { + if (correctionEnabled == true) { + return isHidden.endDate + (time - isHidden.startDate) + 1; + } else { + return isHidden.endDate + 1; + } + } } else { - if (item.displayed) item.hide(); + return time; } }; /** - * 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. + * Check if a time is hidden * - * @param {Item} item - * @param {Array} visibleItems - * @param {{start:number, end:number}} range - * @private + * @param time + * @param hiddenDates + * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} */ - Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { - if (item.isVisible(range)) { - if (visibleItemsLookup[item.id] === undefined) { - visibleItemsLookup[item.id] = true; - visibleItems.push(item); + exports.isHidden = function (time, hiddenDates) { + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + + if (time >= startDate && time < endDate) { + // if the start is entering a hidden zone + return { hidden: true, startDate: startDate, endDate: endDate }; + break; } - } else { - if (item.displayed) item.hide(); } + return { hidden: false, startDate: startDate, endDate: endDate }; }; - module.exports = Group; - /***/ }, -/* 36 */ +/* 38 */ /***/ function(module, exports, __webpack_require__) { - // Utility functions for ordering and stacking of items 'use strict'; - var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors + var Emitter = __webpack_require__(25); + var Hammer = __webpack_require__(10); + var hammerUtil = __webpack_require__(36); + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + var Range = __webpack_require__(35); + var ItemSet = __webpack_require__(39); + var TimeAxis = __webpack_require__(47); + var Activator = __webpack_require__(48); + var DateUtil = __webpack_require__(37); + var CustomTime = __webpack_require__(50); /** - * Order items by their start data - * @param {Item[]} items + * Create a timeline visualization + * @constructor */ - exports.orderByStart = function (items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); - }; + function Core() {} + + // turn Core into an event emitter + Emitter(Core.prototype); /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items + * Create the main DOM for the Core: a root panel containing left, right, + * top, bottom, content, and background panel. + * @param {Element} container The container element where the Core will + * be attached. + * @protected */ - 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; + Core.prototype._create = function (container) { + this.dom = {}; - return aTime - bTime; - }); - }; + this.dom.container = container; - /** - * 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; + this.dom.root = document.createElement('div'); + this.dom.background = document.createElement('div'); + this.dom.backgroundVertical = document.createElement('div'); + this.dom.backgroundHorizontal = document.createElement('div'); + this.dom.centerContainer = document.createElement('div'); + this.dom.leftContainer = document.createElement('div'); + this.dom.rightContainer = document.createElement('div'); + this.dom.center = document.createElement('div'); + this.dom.left = document.createElement('div'); + this.dom.right = document.createElement('div'); + this.dom.top = document.createElement('div'); + this.dom.bottom = document.createElement('div'); + this.dom.shadowTop = document.createElement('div'); + this.dom.shadowBottom = document.createElement('div'); + this.dom.shadowTopLeft = document.createElement('div'); + this.dom.shadowBottomLeft = document.createElement('div'); + this.dom.shadowTopRight = document.createElement('div'); + this.dom.shadowBottomRight = document.createElement('div'); - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } - } + this.dom.root.className = 'vis-timeline'; + this.dom.background.className = 'vis-panel vis-background'; + this.dom.backgroundVertical.className = 'vis-panel vis-background vis-vertical'; + this.dom.backgroundHorizontal.className = 'vis-panel vis-background vis-horizontal'; + this.dom.centerContainer.className = 'vis-panel vis-center'; + this.dom.leftContainer.className = 'vis-panel vis-left'; + this.dom.rightContainer.className = 'vis-panel vis-right'; + this.dom.top.className = 'vis-panel vis-top'; + this.dom.bottom.className = 'vis-panel vis-bottom'; + this.dom.left.className = 'vis-content'; + this.dom.center.className = 'vis-content'; + this.dom.right.className = 'vis-content'; + this.dom.shadowTop.className = 'vis-shadow vis-top'; + this.dom.shadowBottom.className = 'vis-shadow vis-bottom'; + this.dom.shadowTopLeft.className = 'vis-shadow vis-top'; + this.dom.shadowBottomLeft.className = 'vis-shadow vis-bottom'; + this.dom.shadowTopRight.className = 'vis-shadow vis-top'; + this.dom.shadowBottomRight.className = 'vis-shadow vis-bottom'; - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.stack && item.top === null) { - // initialize top position - item.top = margin.axis; + this.dom.root.appendChild(this.dom.background); + this.dom.root.appendChild(this.dom.backgroundVertical); + this.dom.root.appendChild(this.dom.backgroundHorizontal); + this.dom.root.appendChild(this.dom.centerContainer); + this.dom.root.appendChild(this.dom.leftContainer); + this.dom.root.appendChild(this.dom.rightContainer); + this.dom.root.appendChild(this.dom.top); + this.dom.root.appendChild(this.dom.bottom); - 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.stack && exports.collision(item, other, margin.item)) { - collidingItem = other; - break; - } - } + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); - 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); - } - } - }; + this.dom.centerContainer.appendChild(this.dom.shadowTop); + this.dom.centerContainer.appendChild(this.dom.shadowBottom); + this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); + this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); + this.dom.rightContainer.appendChild(this.dom.shadowTopRight); + this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); - /** - * 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. - */ - exports.nostack = function (items, margin, subgroups) { - var i, iMax, newTop; + this.on('rangechange', this.redraw.bind(this)); + this.on('touch', this._onTouch.bind(this)); + this.on('pan', this._onDrag.bind(this)); - // 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 subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } + var me = this; + this.on('change', function (properties) { + if (properties && properties.queue == true) { + // redraw once on next tick + if (!me._redrawTimer) { + me._redrawTimer = setTimeout(function () { + me._redrawTimer = null; + me._redraw(); + }, 0); } - items[i].top = newTop; } else { - items[i].top = margin.axis; + // redraw immediately + me._redraw(); + } + }); + + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = new Hammer(this.dom.root); + this.hammer.get('pinch').set({ enable: true }); + this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. + this.listeners = {}; + + var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend' + // TODO: cleanup + //'touch', 'pinch', + //'tap', 'doubletap', 'hold', + //'dragstart', 'drag', 'dragend', + //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (type) { + var listener = function listener(event) { + if (me.isActive()) { + me.emit(type, event); + } + }; + me.hammer.on(type, listener); + me.listeners[type] = listener; + }); + + // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) + hammerUtil.onTouch(this.hammer, (function (event) { + me.emit('touch', event); + }).bind(this)); + + // emulate a release event (emitted after a pan, pinch, tap, or press) + hammerUtil.onRelease(this.hammer, (function (event) { + me.emit('release', event); + }).bind(this)); + + function onMouseWheel(event) { + if (me.isActive()) { + me.emit('mousewheel', event); } } + this.dom.root.addEventListener('mousewheel', onMouseWheel); + this.dom.root.addEventListener('DOMMouseScroll', onMouseWheel); + + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + + this.customTimes = []; + + // store state information needed for touch events + this.touch = {}; + + this.redrawCount = 0; + + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); }; /** - * 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 + * Set options. Options will be passed to all components loaded in the Timeline. + * @param {Object} [options] + * {String} orientation + * Vertical orientation for the Timeline, + * can be 'bottom' (default) or 'top'. + * {String | Number} width + * Width for the timeline, a number in pixels or + * a css string like '1000px' or '75%'. '100%' by default. + * {String | Number} height + * Fixed height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. If undefined, + * The Timeline will automatically size such that + * its contents fit. + * {String | Number} minHeight + * Minimum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {String | Number} maxHeight + * Maximum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {Number | Date | String} start + * Start date for the visible window + * {Number | Date | String} end + * End date for the visible window */ - 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; - }; + Core.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'clickToUse', 'dataAttributes', 'hiddenDates']; + util.selectiveExtend(fields, this.options, options); -/***/ }, -/* 37 */ -/***/ function(module, exports, __webpack_require__) { + if ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation = { + item: options.orientation, + axis: options.orientation + }; + } else if (typeof options.orientation === 'object') { + if ('item' in options.orientation) { + this.options.orientation.item = options.orientation.item; + } + if ('axis' in options.orientation) { + this.options.orientation.axis = options.orientation.axis; + } + } + } - 'use strict'; + if (this.options.orientation.axis === 'both') { + if (!this.timeAxis2) { + var timeAxis2 = this.timeAxis2 = new TimeAxis(this.body); + timeAxis2.setOptions = function (options) { + var _options = options ? util.extend({}, options) : {}; + _options.orientation = 'top'; // override the orientation option, always top + TimeAxis.prototype.setOptions.call(timeAxis2, _options); + }; + this.components.push(timeAxis2); + } + } else { + if (this.timeAxis2) { + var index = this.components.indexOf(this.timeAxis2); + if (index !== -1) { + this.components.splice(index, 1); + } + this.timeAxis2.destroy(); + this.timeAxis2 = null; + } + } - var Hammer = __webpack_require__(5); - var Item = __webpack_require__(4); + // if the graph2d's drawPoints is a function delegate the callback to the onRender property + if (typeof options.drawPoints == 'function') { + options.drawPoints = { + onRender: options.drawPoints + }; + } - /** - * @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 - */ - function RangeItem(data, conversion, options) { - this.props = { - content: { - width: 0 + if ('hiddenDates' in this.options) { + DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); } - }; - 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 ('clickToUse' in options) { + if (options.clickToUse) { + if (!this.activator) { + this.activator = new Activator(this.dom.root); + } + } else { + if (this.activator) { + this.activator.destroy(); + delete this.activator; + } + } } - if (data.end == undefined) { - throw new Error('Property "end" missing in item ' + data.id); + + if ('showCustomTime' in options) { + throw new Error('Option `showCustomTime` is deprecated. Create a custom time bar via timeline.addCustomTime(time [, id])'); } + + // enable/disable autoResize + this._initAutoResize(); } - Item.call(this, data, conversion, options); - } + // propagate options to all components + this.components.forEach(function (component) { + return component.setOptions(options); + }); - RangeItem.prototype = new Item(null, null, null); + // enable/disable configure + if ('configure' in options) { + if (!this.configurator) { + this.configurator = this._createConfigurator(); + } - RangeItem.prototype.baseClassName = 'vis-item vis-range'; + this.configurator.setOptions(options.configure); + + // collect the settings of all components, and pass them to the configuration system + var appliedOptions = util.deepExtend({}, this.options); + this.components.forEach(function (component) { + util.deepExtend(appliedOptions, component.options); + }); + this.configurator.setModuleOptions({ global: appliedOptions }); + } + + // redraw everything + this._redraw(); + }; /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible + * Returns true when the Timeline is active. + * @returns {boolean} */ - RangeItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; + Core.prototype.isActive = function () { + return !this.activator || this.activator.active; }; /** - * Repaint the item + * Destroy the Core, clean up all DOM elements and event listeners. */ - 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() - - // frame box (to prevent the item contents from overflowing - dom.frame = document.createElement('div'); - dom.frame.className = 'vis-item-overflow'; - dom.box.appendChild(dom.frame); + Core.prototype.destroy = function () { + // unbind datasets + this.setItems(null); + this.setGroups(null); - // contents box - dom.content = document.createElement('div'); - dom.content.className = 'vis-item-content'; - dom.frame.appendChild(dom.content); + // remove all event listeners + this.off(); - // attach this item as attribute - dom.box['timeline-item'] = this; + // stop checking for changed size + this._stopAutoResize(); - this.dirty = true; + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); } + this.dom = null; - // append DOM to parent DOM - if (!this.parent) { - throw new Error('Cannot redraw item: no parent attached'); + // remove Activator + if (this.activator) { + this.activator.destroy(); + delete this.activator; } - if (!dom.box.parentNode) { - var foreground = this.parent.dom.foreground; - if (!foreground) { - throw new Error('Cannot redraw item: parent has no foreground container element'); + + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; } - 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); - - var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; - - // update class - var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); - dom.box.className = this.baseClassName + className; - - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.frame).overflow !== 'hidden'; - - // recalculate size - // turn off max-width to be able to calculate the real width - // this causes an extra browser repaint/reflow, but so be it - this.dom.content.style.maxWidth = 'none'; - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; - this.dom.content.style.maxWidth = ''; + this.listeners = null; + this.hammer = null; - this.dirty = false; - } + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + return component.destroy(); + }); - this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); + this.body = null; }; /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * Set a custom time bar + * @param {Date} time + * @param {number} [id=undefined] Optional id of the custom time bar to be adjusted. */ - RangeItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); + Core.prototype.setCustomTime = function (time, id) { + var customTimes = this.customTimes.filter(function (component) { + return id === component.options.id; + }); + + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); + } + + if (customTimes.length > 0) { + customTimes[0].setCustomTime(time); } }; /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed + * Retrieve the current custom time. + * @param {number} [id=undefined] Id of the custom time bar. + * @return {Date | undefined} customTime */ - RangeItem.prototype.hide = function () { - if (this.displayed) { - var box = this.dom.box; - - if (box.parentNode) { - box.parentNode.removeChild(box); - } + Core.prototype.getCustomTime = function (id) { + var customTimes = this.customTimes.filter(function (component) { + return component.options.id === id; + }); - this.displayed = false; + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); } + return customTimes[0].getCustomTime(); }; /** - * Reposition the item horizontally - * @param {boolean} [limitSize=true] If true (default), the width of the range - * item will be limited, as the browser cannot - * display very wide divs. This means though - * that the applied left and width may - * not correspond to the ranges start and end - * @Override + * Add custom vertical bar + * @param {Date | String | Number} [time] A Date, unix timestamp, or + * ISO date string. Time point where + * the new bar should be placed. + * If not provided, `new Date()` will + * be used. + * @param {Number | String} [id=undefined] Id of the new bar. Optional + * @return {Number | String} Returns the id of the new bar */ - RangeItem.prototype.repositionX = function (limitSize) { - 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 range, as browsers cannot draw very wide divs - if (limitSize === undefined || limitSize === true) { - if (start < -parentWidth) { - start = -parentWidth; - } - if (end > 2 * parentWidth) { - end = 2 * parentWidth; - } - } - 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; + Core.prototype.addCustomTime = function (time, id) { + var timestamp = time !== undefined ? util.convert(time, 'Date').valueOf() : new Date(); - // 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 { - this.left = start; - this.width = boxWidth; - contentWidth = Math.min(end - start, this.props.content.width); + var exists = this.customTimes.some(function (customTime) { + return customTime.options.id === id; + }); + if (exists) { + throw new Error('A custom time with id ' + JSON.stringify(id) + ' already exists'); } - 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; - - case 'right': - this.dom.content.style.left = Math.max(boxWidth - contentWidth, 0) + 'px'; - break; + var customTime = new CustomTime(this.body, { + time: timestamp, + id: id + }); - case 'center': - this.dom.content.style.left = Math.max((boxWidth - contentWidth) / 2, 0) + 'px'; - break; + this.customTimes.push(customTime); + this.components.push(customTime); + this.redraw(); - default: - // 'auto' - // when range exceeds left of the window, position the contents at the left of the visible area - if (this.overflow) { - if (end > 0) { - contentLeft = Math.max(-start, 0); - } else { - contentLeft = -contentWidth; // ensure it's not visible anymore - } - } else { - if (start < 0) { - contentLeft = -start; - } else { - contentLeft = 0; - } - } - this.dom.content.style.left = contentLeft + 'px'; - } + return id; }; /** - * Reposition the item vertically - * @Override + * Remove previously added custom bar + * @param {int} id ID of the custom bar to be removed + * @return {boolean} True if the bar exists and is removed, false otherwise */ - RangeItem.prototype.repositionY = function () { - var orientation = this.options.orientation.item; - var box = this.dom.box; + Core.prototype.removeCustomTime = function (id) { + var customTimes = this.customTimes.filter(function (bar) { + return bar.options.id === id; + }); - if (orientation == 'top') { - box.style.top = this.top + 'px'; - } else { - box.style.top = this.parent.height - this.top - this.height + 'px'; + if (customTimes.length === 0) { + throw new Error('No custom time bar found with id ' + JSON.stringify(id)); } + + customTimes.forEach((function (customTime) { + this.customTimes.splice(this.customTimes.indexOf(customTime), 1); + this.components.splice(this.components.indexOf(customTime), 1); + customTime.destroy(); + }).bind(this)); }; /** - * Repaint a drag area on the left side of the range when the range is selected - * @protected + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - 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 = 'vis-drag-left'; - dragLeft.dragLeftItem = this; - - 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; - } + Core.prototype.getVisibleItems = function () { + return this.itemSet && this.itemSet.getVisibleItems() || []; }; /** - * Repaint a drag area on the right side of the range when the range is selected - * @protected + * Set Core window such that it fits all items + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. */ - 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 = 'vis-drag-right'; - dragRight.dragRightItem = this; + Core.prototype.fit = function (options) { + var range = this.getDataRange(); - 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; + // skip range set if there is no min and max date + if (range.min === null && range.max === null) { + return; } - }; - - module.exports = RangeItem; -/***/ }, -/* 38 */ -/***/ function(module, exports, __webpack_require__) { + // apply a margin of 1% left and right of the data + var interval = range.max - range.min; + var min = new Date(range.min.valueOf() - interval * 0.01); + var max = new Date(range.max.valueOf() + interval * 0.01); - 'use strict'; + var animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(min, max, animation); + }; - var moment = __webpack_require__(10); - var DateUtil = __webpack_require__(32); - var util = __webpack_require__(9); + /** + * Calculate the data range of the items start and end dates + * @returns {{min: Date | null, max: Date | null}} + * @protected + */ + Core.prototype.getDataRange = function () { + // must be implemented by Timeline and Graph2d + throw new Error('Cannot invoke abstract method getDataRange'); + }; /** - * @constructor TimeStep - * The class TimeStep is an iterator for dates. You provide a start date and an - * end date. The class itself determines the best scale (step size) based on the - * provided start Date, end Date, and minimumStep. - * - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters + * Set the visible window. Both parameters are optional, you can change only + * start or only end. Syntax: * - * Alternatively, you can set a scale by hand. - * After creation, you can initialize the class by executing first(). Then you - * can iterate from the start date to the end date via next(). You can check if - * the end date is reached with the function hasNext(). After each step, you can - * retrieve the current date via getCurrent(). - * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, - * days, to years. + * TimeLine.setWindow(start, end) + * TimeLine.setWindow(start, end, options) + * TimeLine.setWindow(range) * - * Version: 1.2 + * Where start and end can be a Date, number, or string, and range is an + * object with properties start and end. * - * @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 + * @param {Date | Number | String | Object} [start] Start date of visible window + * @param {Date | Number | String} [end] End date of visible window + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. */ - function TimeStep(start, end, minimumStep, hiddenDates) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); - - this.autoScale = true; - this.scale = 'day'; - this.step = 1; + Core.prototype.setWindow = function (start, end, options) { + var animation; + if (arguments.length == 1) { + var range = arguments[0]; + animation = range.animation !== undefined ? range.animation : true; + this.range.setRange(range.start, range.end, animation); + } else { + animation = options && options.animation !== undefined ? options.animation : true; + this.range.setRange(start, end, animation); + } + }; - // initialize the range - this.setRange(start, end, minimumStep); + /** + * Move the window such that given time is centered on screen. + * @param {Date | Number | String} time + * @param {Object} [options] Available options: + * `animation: boolean | {duration: number, easingFunction: string}` + * If true (default), the range is animated + * smoothly to the new window. An object can be + * provided to specify duration and easing function. + * Default duration is 500 ms, and default easing + * function is 'easeInOutQuad'. + */ + Core.prototype.moveTo = function (time, options) { + var interval = this.range.end - this.range.start; + var t = util.convert(time, 'Date').valueOf(); - // hidden Dates options - this.switchedDay = false; - this.switchedMonth = false; - this.switchedYear = false; - this.hiddenDates = hiddenDates; - if (hiddenDates === undefined) { - this.hiddenDates = []; - } + var start = t - interval / 2; + var end = t + interval / 2; + var animation = options && options.animation !== undefined ? options.animation : true; - this.format = TimeStep.FORMAT; // default formatting - } + this.range.setRange(start, end, animation); + }; - // Time formatting - TimeStep.FORMAT = { - minorLabels: { - millisecond: 'SSS', - second: 's', - minute: 'HH:mm', - hour: 'HH:mm', - weekday: 'ddd D', - day: 'D', - month: 'MMM', - year: 'YYYY' - }, - majorLabels: { - millisecond: 'HH:mm:ss', - second: 'D MMMM HH:mm', - minute: 'ddd D MMMM', - hour: 'ddd D MMMM', - weekday: 'MMMM YYYY', - day: 'MMMM YYYY', - month: 'YYYY', - year: '' - } + /** + * Get the visible window + * @return {{start: Date, end: Date}} Visible range + */ + Core.prototype.getWindow = function () { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; }; /** - * Set custom formatting for the minor an major labels of the TimeStep. - * Both `minorLabels` and `majorLabels` are an Object with properties: - * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'. - * @param {{minorLabels: Object, majorLabels: Object}} format + * Force a redraw. Can be overridden by implementations of Core */ - TimeStep.prototype.setFormat = function (format) { - var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); - this.format = util.deepExtend(defaultFormat, format); + Core.prototype.redraw = function () { + this._redraw(); }; /** - * Set a new range - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * @param {Date} [start] The start date and time. - * @param {Date} [end] The end date and time. - * @param {int} [minimumStep] Optional. Minimum step size in milliseconds + * Redraw for internal use. Redraws all components. See also the public + * method redraw. + * @protected */ - TimeStep.prototype.setRange = function (start, end, minimumStep) { - if (!(start instanceof Date) || !(end instanceof Date)) { - throw 'No legal start or end date in method setRange'; + Core.prototype._redraw = function () { + var resized = false; + var options = this.options; + var props = this.props; + var dom = this.dom; + + if (!dom) return; // when destroyed + + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + + // update class names + if (options.orientation == 'top') { + util.addClassName(dom.root, 'vis-top'); + util.removeClassName(dom.root, 'vis-bottom'); + } else { + util.removeClassName(dom.root, 'vis-top'); + util.addClassName(dom.root, 'vis-bottom'); } - this._start = start != undefined ? new Date(start.valueOf()) : new Date(); - this._end = end != undefined ? new Date(end.valueOf()) : new Date(); + // update root width and height options + dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ''); + dom.root.style.minHeight = util.option.asSize(options.minHeight, ''); + dom.root.style.width = util.option.asSize(options.width, ''); - if (this.autoScale) { - this.setMinimumStep(minimumStep); + // calculate border widths + props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; + props.border.right = props.border.left; + props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; + props.border.bottom = props.border.top; + var borderRootHeight = dom.root.offsetHeight - dom.root.clientHeight; + var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; + + // workaround for a bug in IE: the clientWidth of an element with + // a height:0px and overflow:hidden is not calculated and always has value 0 + if (dom.centerContainer.clientHeight === 0) { + props.border.left = props.border.top; + props.border.right = props.border.left; + } + if (dom.root.clientHeight === 0) { + borderRootWidth = borderRootHeight; } - }; - /** - * Set the range iterator to the start date. - */ - TimeStep.prototype.first = function () { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); - }; + // calculate the heights. If any of the side panels is empty, we set the height to + // minus the border width, such that the border will be invisible + props.center.height = dom.center.offsetHeight; + props.left.height = dom.left.offsetHeight; + props.right.height = dom.right.offsetHeight; + props.top.height = dom.top.clientHeight || -props.border.top; + props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; - /** - * Round the current date to the first minor date value - * This must be executed once when the current date is set to start Date - */ - TimeStep.prototype.roundToMinor = function () { - // round to floor - // IMPORTANT: we have no breaks in this switch! (this is no bug) - // noinspection FallThroughInSwitchStatementJS - switch (this.scale) { - case 'year': - this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); - this.current.setMonth(0); - case 'month': - this.current.setDate(1); - case 'day': // intentional fall through - case 'weekday': - this.current.setHours(0); - case 'hour': - this.current.setMinutes(0); - case 'minute': - this.current.setSeconds(0); - case 'second': - this.current.setMilliseconds(0); - //case 'millisecond': // nothing to do for milliseconds + // TODO: compensate borders when any of the panels is empty. + + // apply auto height + // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) + var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); + var autoHeight = props.top.height + contentHeight + props.bottom.height + borderRootHeight + props.border.top + props.border.bottom; + dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px'); + + // calculate heights of the content panels + props.root.height = dom.root.offsetHeight; + props.background.height = props.root.height - borderRootHeight; + var containerHeight = props.root.height - props.top.height - props.bottom.height - borderRootHeight; + props.centerContainer.height = containerHeight; + props.leftContainer.height = containerHeight; + props.rightContainer.height = props.leftContainer.height; + + // calculate the widths of the panels + props.root.width = dom.root.offsetWidth; + props.background.width = props.root.width - borderRootWidth; + props.left.width = dom.leftContainer.clientWidth || -props.border.left; + props.leftContainer.width = props.left.width; + props.right.width = dom.rightContainer.clientWidth || -props.border.right; + props.rightContainer.width = props.right.width; + var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; + props.center.width = centerWidth; + props.centerContainer.width = centerWidth; + props.top.width = centerWidth; + props.bottom.width = centerWidth; + + // resize the panels + dom.background.style.height = props.background.height + 'px'; + dom.backgroundVertical.style.height = props.background.height + 'px'; + dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px'; + dom.centerContainer.style.height = props.centerContainer.height + 'px'; + dom.leftContainer.style.height = props.leftContainer.height + 'px'; + dom.rightContainer.style.height = props.rightContainer.height + 'px'; + + dom.background.style.width = props.background.width + 'px'; + dom.backgroundVertical.style.width = props.centerContainer.width + 'px'; + dom.backgroundHorizontal.style.width = props.background.width + 'px'; + dom.centerContainer.style.width = props.center.width + 'px'; + dom.top.style.width = props.top.width + 'px'; + dom.bottom.style.width = props.bottom.width + 'px'; + + // reposition the panels + dom.background.style.left = '0'; + dom.background.style.top = '0'; + dom.backgroundVertical.style.left = props.left.width + props.border.left + 'px'; + dom.backgroundVertical.style.top = '0'; + dom.backgroundHorizontal.style.left = '0'; + dom.backgroundHorizontal.style.top = props.top.height + 'px'; + dom.centerContainer.style.left = props.left.width + 'px'; + dom.centerContainer.style.top = props.top.height + 'px'; + dom.leftContainer.style.left = '0'; + dom.leftContainer.style.top = props.top.height + 'px'; + dom.rightContainer.style.left = props.left.width + props.center.width + 'px'; + dom.rightContainer.style.top = props.top.height + 'px'; + dom.top.style.left = props.left.width + 'px'; + dom.top.style.top = '0'; + dom.bottom.style.left = props.left.width + 'px'; + dom.bottom.style.top = props.top.height + props.centerContainer.height + 'px'; + + // update the scrollTop, feasible range for the offset can be changed + // when the height of the Core or of the contents of the center changed + this._updateScrollTop(); + + // reposition the scrollable contents + var offset = this.props.scrollTop; + if (options.orientation.item != 'top') { + offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); } + dom.center.style.left = '0'; + dom.center.style.top = offset + 'px'; + dom.left.style.left = '0'; + dom.left.style.top = offset + 'px'; + dom.right.style.left = '0'; + dom.right.style.top = offset + 'px'; - if (this.step != 1) { - // round down to the first minor value that is a multiple of the current step size - switch (this.scale) { - case 'millisecond': - this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);break; - case 'second': - this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);break; - case 'minute': - this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);break; - case 'hour': - this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);break; - case 'weekday': // intentional fall through - case 'day': - this.current.setDate(this.current.getDate() - 1 - (this.current.getDate() - 1) % this.step + 1);break; - case 'month': - this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);break; - case 'year': - this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step);break; - default: - break; + // show shadows when vertical scrolling is available + var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : ''; + var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : ''; + dom.shadowTop.style.visibility = visibilityTop; + dom.shadowBottom.style.visibility = visibilityBottom; + dom.shadowTopLeft.style.visibility = visibilityTop; + dom.shadowBottomLeft.style.visibility = visibilityBottom; + dom.shadowTopRight.style.visibility = visibilityTop; + dom.shadowBottomRight.style.visibility = visibilityBottom; + + // redraw all components + this.components.forEach(function (component) { + resized = component.redraw() || resized; + }); + if (resized) { + // keep repainting until all sizes are settled + var MAX_REDRAWS = 3; // maximum number of consecutive redraws + if (this.redrawCount < MAX_REDRAWS) { + this.redrawCount++; + this._redraw(); + } else { + console.log('WARNING: infinite loop in redraw?'); } + this.redrawCount = 0; } }; - /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date - */ - TimeStep.prototype.hasNext = function () { - return this.current.valueOf() <= this._end.valueOf(); + // TODO: deprecated since version 1.1.0, remove some day + Core.prototype.repaint = function () { + throw new Error('Function repaint is deprecated. Use redraw instead.'); }; /** - * Do the next step + * Set a current time. This can be used for example to ensure that a client's + * time is synchronized with a shared server time. + * Only applicable when option `showCurrentTime` is true. + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. */ - TimeStep.prototype.next = function () { - var prev = this.current.valueOf(); - - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case 'millisecond': - - this.current = new Date(this.current.valueOf() + this.step);break; - case 'second': - this.current = new Date(this.current.valueOf() + this.step * 1000);break; - case 'minute': - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60);break; - case 'hour': - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - h % this.step); - break; - case 'weekday': // intentional fall through - case 'day': - this.current.setDate(this.current.getDate() + this.step);break; - case 'month': - this.current.setMonth(this.current.getMonth() + this.step);break; - case 'year': - this.current.setFullYear(this.current.getFullYear() + this.step);break; - default: - break; - } - } else { - switch (this.scale) { - case 'millisecond': - this.current = new Date(this.current.valueOf() + this.step);break; - case 'second': - this.current.setSeconds(this.current.getSeconds() + this.step);break; - case 'minute': - this.current.setMinutes(this.current.getMinutes() + this.step);break; - case 'hour': - this.current.setHours(this.current.getHours() + this.step);break; - case 'weekday': // intentional fall through - case 'day': - this.current.setDate(this.current.getDate() + this.step);break; - case 'month': - this.current.setMonth(this.current.getMonth() + this.step);break; - case 'year': - this.current.setFullYear(this.current.getFullYear() + this.step);break; - default: - break; - } + Core.prototype.setCurrentTime = function (time) { + if (!this.currentTime) { + throw new Error('Option showCurrentTime must be true'); } - if (this.step != 1) { - // round down to the correct major value - switch (this.scale) { - case 'millisecond': - if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; - case 'second': - if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; - case 'minute': - if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; - case 'hour': - if (this.current.getHours() < this.step) this.current.setHours(0);break; - case 'weekday': // intentional fall through - case 'day': - if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; - case 'month': - if (this.current.getMonth() < this.step) this.current.setMonth(0);break; - case 'year': - break; // nothing to do for year - default: - break; - } - } + this.currentTime.setCurrentTime(time); + }; - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); + /** + * Get the current time. + * Only applicable when option `showCurrentTime` is true. + * @return {Date} Returns the current time. + */ + Core.prototype.getCurrentTime = function () { + if (!this.currentTime) { + throw new Error('Option showCurrentTime must be true'); } - DateUtil.stepOverHiddenDates(this, prev); + return this.currentTime.getCurrentTime(); }; /** - * Get the current datetime - * @return {Date} current The current date + * Convert a position on screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + * @protected */ - TimeStep.prototype.getCurrent = function () { - return this.current; + // TODO: move this function to Range + Core.prototype._toTime = function (x) { + return DateUtil.toTime(this, x, this.props.center.width); }; /** - * Set a custom scale. Autoscaling will be disabled. - * For example setScale('minute', 5) will result - * in minor steps of 5 minutes, and major steps of an hour. - * - * @param {{scale: string, step: number}} params - * An object containing two properties: - * - A string 'scale'. Choose from 'millisecond', 'second', - * 'minute', 'hour', 'weekday', 'day', 'month', 'year'. - * - A number 'step'. A step size, by default 1. - * Choose for example 1, 2, 5, or 10. + * Convert a position on the global screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x + * @protected */ - TimeStep.prototype.setScale = function (params) { - if (params && typeof params.scale == 'string') { - this.scale = params.scale; - this.step = params.step > 0 ? params.step : 1; - this.autoScale = false; - } + // TODO: move this function to Range + Core.prototype._toGlobalTime = function (x) { + return DateUtil.toTime(this, x, this.props.root.width); + //var conversion = this.range.conversion(this.props.root.width); + //return new Date(x / conversion.scale + conversion.offset); }; /** - * Enable or disable autoscaling - * @param {boolean} enable If true, autoascaling is set true + * Convert a datetime (Date object) into a position on the screen + * @param {Date} time A date + * @return {int} x The position on the screen in pixels which corresponds + * with the given date. + * @protected */ - TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; + // TODO: move this function to Range + Core.prototype._toScreen = function (time) { + return DateUtil.toScreen(this, time, this.props.center.width); }; /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds + * Convert a datetime (Date object) into a position on the root + * This is used to get the pixel density estimate for the screen, not the center panel + * @param {Date} time A date + * @return {int} x The position on root in pixels which corresponds + * with the given date. + * @protected */ - TimeStep.prototype.setMinimumStep = function (minimumStep) { - if (minimumStep == undefined) { - return; - } - - //var b = asc + ds; - - var stepYear = 1000 * 60 * 60 * 24 * 30 * 12; - var stepMonth = 1000 * 60 * 60 * 24 * 30; - var stepDay = 1000 * 60 * 60 * 24; - var stepHour = 1000 * 60 * 60; - var stepMinute = 1000 * 60; - var stepSecond = 1000; - var stepMillisecond = 1; + // TODO: move this function to Range + Core.prototype._toGlobalScreen = function (time) { + return DateUtil.toScreen(this, time, this.props.root.width); + //var conversion = this.range.conversion(this.props.root.width); + //return (time.valueOf() - conversion.offset) * conversion.scale; + }; - // find the smallest step that is larger than the provided minimumStep - if (stepYear * 1000 > minimumStep) { - this.scale = 'year';this.step = 1000; - } - if (stepYear * 500 > minimumStep) { - this.scale = 'year';this.step = 500; - } - if (stepYear * 100 > minimumStep) { - this.scale = 'year';this.step = 100; - } - if (stepYear * 50 > minimumStep) { - this.scale = 'year';this.step = 50; - } - if (stepYear * 10 > minimumStep) { - this.scale = 'year';this.step = 10; - } - if (stepYear * 5 > minimumStep) { - this.scale = 'year';this.step = 5; - } - if (stepYear > minimumStep) { - this.scale = 'year';this.step = 1; - } - if (stepMonth * 3 > minimumStep) { - this.scale = 'month';this.step = 3; - } - if (stepMonth > minimumStep) { - this.scale = 'month';this.step = 1; - } - if (stepDay * 5 > minimumStep) { - this.scale = 'day';this.step = 5; - } - if (stepDay * 2 > minimumStep) { - this.scale = 'day';this.step = 2; - } - if (stepDay > minimumStep) { - this.scale = 'day';this.step = 1; - } - if (stepDay / 2 > minimumStep) { - this.scale = 'weekday';this.step = 1; - } - if (stepHour * 4 > minimumStep) { - this.scale = 'hour';this.step = 4; - } - if (stepHour > minimumStep) { - this.scale = 'hour';this.step = 1; - } - if (stepMinute * 15 > minimumStep) { - this.scale = 'minute';this.step = 15; - } - if (stepMinute * 10 > minimumStep) { - this.scale = 'minute';this.step = 10; - } - if (stepMinute * 5 > minimumStep) { - this.scale = 'minute';this.step = 5; - } - if (stepMinute > minimumStep) { - this.scale = 'minute';this.step = 1; - } - if (stepSecond * 15 > minimumStep) { - this.scale = 'second';this.step = 15; - } - if (stepSecond * 10 > minimumStep) { - this.scale = 'second';this.step = 10; - } - if (stepSecond * 5 > minimumStep) { - this.scale = 'second';this.step = 5; - } - if (stepSecond > minimumStep) { - this.scale = 'second';this.step = 1; - } - if (stepMillisecond * 200 > minimumStep) { - this.scale = 'millisecond';this.step = 200; - } - if (stepMillisecond * 100 > minimumStep) { - this.scale = 'millisecond';this.step = 100; - } - if (stepMillisecond * 50 > minimumStep) { - this.scale = 'millisecond';this.step = 50; - } - if (stepMillisecond * 10 > minimumStep) { - this.scale = 'millisecond';this.step = 10; - } - if (stepMillisecond * 5 > minimumStep) { - this.scale = 'millisecond';this.step = 5; - } - if (stepMillisecond > minimumStep) { - this.scale = 'millisecond';this.step = 1; + /** + * Initialize watching when option autoResize is true + * @private + */ + Core.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } else { + this._stopAutoResize(); } }; /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * Static function - * @param {Date} date the date to be snapped. - * @param {string} scale Current scale, can be 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day', 'month', 'year'. - * @param {number} step Current step (1, 2, 4, 5, ... - * @return {Date} snappedDate + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ - TimeStep.snap = function (date, scale, step) { - var clone = new Date(date.valueOf()); + Core.prototype._startAutoResize = function () { + var me = this; - if (scale == 'year') { - var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); - clone.setFullYear(Math.round(year / step) * step); - clone.setMonth(0); - clone.setDate(0); - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == 'month') { - if (clone.getDate() > 15) { - clone.setDate(1); - clone.setMonth(clone.getMonth() + 1); - // important: first set Date to 1, after that change the month. - } else { - clone.setDate(1); - } + this._stopAutoResize(); - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == 'day') { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 24) * 24);break; - default: - clone.setHours(Math.round(clone.getHours() / 12) * 12);break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == 'weekday') { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 12) * 12);break; - default: - clone.setHours(Math.round(clone.getHours() / 6) * 6);break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == 'hour') { - switch (step) { - case 4: - clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60);break; - default: - clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30);break; - } - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == 'minute') { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 15: - case 10: - clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); - clone.setSeconds(0); - break; - case 5: - clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60);break; - default: - clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30);break; + this._onResize = function () { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; } - clone.setMilliseconds(0); - } else if (scale == 'second') { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 15: - case 10: - clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); - clone.setMilliseconds(0); - break; - case 5: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000);break; - default: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500);break; + + if (me.dom.root) { + // check whether the frame is resized + // Note: we compare offsetWidth here, not clientWidth. For some reason, + // IE does not restore the clientWidth from 0 to the actual width after + // changing the timeline's container display style from none to visible + if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { + me.props.lastWidth = me.dom.root.offsetWidth; + me.props.lastHeight = me.dom.root.offsetHeight; + + me.emit('change'); + } } - } else if (scale == 'millisecond') { - var _step = step > 5 ? step / 2 : 1; - clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); - } + }; - return clone; + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); + + this.watchTimer = setInterval(this._onResize, 1000); }; /** - * 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. + * Stop watching for a resize of the frame. + * @private */ - TimeStep.prototype.isMajor = function () { - if (this.switchedYear == true) { - this.switchedYear = false; - switch (this.scale) { - case 'year': - case 'month': - case 'weekday': - case 'day': - case 'hour': - case 'minute': - case 'second': - case 'millisecond': - return true; - default: - return false; - } - } else if (this.switchedMonth == true) { - this.switchedMonth = false; - switch (this.scale) { - case 'weekday': - case 'day': - case 'hour': - case 'minute': - case 'second': - case 'millisecond': - return true; - default: - return false; - } - } else if (this.switchedDay == true) { - this.switchedDay = false; - switch (this.scale) { - case 'millisecond': - case 'second': - case 'minute': - case 'hour': - return true; - default: - return false; - } + Core.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; } - switch (this.scale) { - case 'millisecond': - return this.current.getMilliseconds() == 0; - case 'second': - return this.current.getSeconds() == 0; - case 'minute': - return this.current.getHours() == 0 && this.current.getMinutes() == 0; - case 'hour': - return this.current.getHours() == 0; - case 'weekday': // intentional fall through - case 'day': - return this.current.getDate() == 1; - case 'month': - return this.current.getMonth() == 0; - case 'year': - return false; - default: - return false; - } + // remove event listener on window.resize + util.removeEventListener(window, 'resize', this._onResize); + this._onResize = null; }; /** - * Returns formatted text for the minor axislabel, depending on the current - * date and the scale. For example when scale is MINUTE, the current time is - * formatted as "hh:mm". - * @param {Date} [date] custom date. if not provided, current date is taken + * Start moving the timeline vertically + * @param {Event} event + * @private */ - TimeStep.prototype.getLabelMinor = function (date) { - if (date == undefined) { - date = this.current; - } - - var format = this.format.minorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ''; + Core.prototype._onTouch = function (event) { + this.touch.allowDragging = true; + this.touch.initialScrollTop = this.props.scrollTop; }; /** - * Returns formatted text for the major axis label, depending on the current - * date and the scale. For example when scale is MINUTE, the major scale is - * hours, and the hour will be formatted as "hh". - * @param {Date} [date] custom date. if not provided, current date is taken + * Start moving the timeline vertically + * @param {Event} event + * @private */ - TimeStep.prototype.getLabelMajor = function (date) { - if (date == undefined) { - date = this.current; - } - - var format = this.format.majorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ''; + Core.prototype._onPinch = function (event) { + this.touch.allowDragging = false; }; - TimeStep.prototype.getClassName = function () { - var m = moment(this.current); - var date = m.locale ? m.locale('en') : m.lang('en'); // old versions of moment have .lang() function - var step = this.step; + /** + * Move the timeline vertically + * @param {Event} event + * @private + */ + Core.prototype._onDrag = function (event) { + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.touch.allowDragging) return; - function even(value) { - return value / step % 2 == 0 ? ' vis-even' : ' vis-odd'; - } + var delta = event.deltaY; - function today(date) { - if (date.isSame(new Date(), 'day')) { - return ' vis-today'; - } - if (date.isSame(moment().add(1, 'day'), 'day')) { - return ' vis-tomorrow'; - } - if (date.isSame(moment().add(-1, 'day'), 'day')) { - return ' vis-yesterday'; - } - return ''; - } + var oldScrollTop = this._getScrollTop(); + var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - function currentWeek(date) { - return date.isSame(new Date(), 'week') ? ' vis-current-week' : ''; + if (newScrollTop != oldScrollTop) { + this._redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already + this.emit('verticalDrag'); } + }; - function currentMonth(date) { - return date.isSame(new Date(), 'month') ? ' vis-current-month' : ''; - } + /** + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ + Core.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; + }; - function currentYear(date) { - return date.isSame(new Date(), 'year') ? ' vis-current-year' : ''; + /** + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ + Core.prototype._updateScrollTop = function () { + // recalculate the scrollTopMin + var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero + if (scrollTopMin != this.props.scrollTopMin) { + // in case of bottom orientation, change the scrollTop such that the contents + // do not move relative to the time axis at the bottom + if (this.options.orientation.item != 'top') { + this.props.scrollTop += scrollTopMin - this.props.scrollTopMin; + } + this.props.scrollTopMin = scrollTopMin; } - switch (this.scale) { - case 'millisecond': - return even(date.milliseconds()).trim(); - - case 'second': - return even(date.seconds()).trim(); - - case 'minute': - return even(date.minutes()).trim(); - - case 'hour': - var hours = date.hours(); - if (this.step == 4) { - hours = hours + '-h' + (hours + 4); - } - return 'vis-h' + hours + today(date) + even(date.hours()); - - case 'weekday': - return 'vis-' + date.format('dddd').toLowerCase() + today(date) + currentWeek(date) + even(date.date()); - - case 'day': - var day = date.date(); - var month = date.format('MMMM').toLowerCase(); - return 'vis-day' + day + ' vis-' + month + currentMonth(date) + even(day - 1); + // limit the scrollTop to the feasible scroll range + if (this.props.scrollTop > 0) this.props.scrollTop = 0; + if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin; - case 'month': - return 'vis-' + date.format('MMMM').toLowerCase() + currentMonth(date) + even(date.month()); + return this.props.scrollTop; + }; - case 'year': - var year = date.year(); - return 'vis-year' + year + currentYear(date) + even(year); + /** + * Get the current scrollTop + * @returns {number} scrollTop + * @private + */ + Core.prototype._getScrollTop = function () { + return this.props.scrollTop; + }; - default: - return ''; - } + /** + * Load a configurator + * @return {Object} + * @private + */ + Core.prototype._createConfigurator = function () { + throw new Error('Cannot invoke abstract method _createConfigurator'); }; - module.exports = TimeStep; + module.exports = Core; /***/ }, /* 39 */ @@ -20156,1553 +19111,1618 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var util = __webpack_require__(9); - var Group = __webpack_require__(35); - - /** - * @constructor BackgroundGroup - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet - */ - function BackgroundGroup(groupId, data, itemSet) { - Group.call(this, groupId, data, itemSet); - - this.width = 0; - this.height = 0; - this.top = 0; - this.left = 0; - } + var Hammer = __webpack_require__(10); + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + var TimeStep = __webpack_require__(43); + var Component = __webpack_require__(33); + var Group = __webpack_require__(40); + var BackgroundGroup = __webpack_require__(44); + var BoxItem = __webpack_require__(45); + var PointItem = __webpack_require__(6); + var RangeItem = __webpack_require__(42); + var BackgroundItem = __webpack_require__(46); - BackgroundGroup.prototype = Object.create(Group.prototype); + var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + var BACKGROUND = '__background__'; // reserved group id for background items without group /** - * 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 + * An ItemSet holds a set of items and ranges which can be displayed in a + * range. The width is determined by the parent of the ItemSet, and the height + * is determined by the size of the items. + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body + * @param {Object} [options] See ItemSet.setOptions for the available options. + * @constructor ItemSet + * @extends Component */ - BackgroundGroup.prototype.redraw = function (range, margin, restack) { - var resized = false; + function ItemSet(body, options) { + this.body = body; - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + this.defaultOptions = { + type: null, // 'box', 'point', 'range', 'background' + orientation: { + item: 'bottom' // item orientation: 'top' or 'bottom' + }, + align: 'auto', // alignment of box items + stack: true, + groupOrder: null, - // calculate actual size - this.width = this.dom.background.offsetWidth; + selectable: true, + multiselect: false, - // apply new height (just always zero for BackgroundGroup - this.dom.background.style.height = '0'; + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, - // update vertical position of items after they are re-stacked and the height of the group is calculated - for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { - var item = this.visibleItems[i]; - item.repositionY(margin); - } + snap: TimeStep.snap, - return resized; - }; + onAdd: function onAdd(item, callback) { + callback(item); + }, + onUpdate: function onUpdate(item, callback) { + callback(item); + }, + onMove: function onMove(item, callback) { + callback(item); + }, + onRemove: function onRemove(item, callback) { + callback(item); + }, + onMoving: function onMoving(item, callback) { + callback(item); + }, - /** - * Show this group: attach to the DOM - */ - BackgroundGroup.prototype.show = function () { - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } - }; + margin: { + item: { + horizontal: 10, + vertical: 10 + }, + axis: 20 + } + }; - module.exports = BackgroundGroup; + // options is shared by this ItemSet and all its items + this.options = util.extend({}, this.defaultOptions); -/***/ }, -/* 40 */ -/***/ function(module, exports, __webpack_require__) { + // options for getting items from the DataSet with the correct type + this.itemOptions = { + type: { start: 'Date', end: 'Date' } + }; - 'use strict'; + this.conversion = { + toScreen: body.util.toScreen, + toTime: body.util.toTime + }; + this.dom = {}; + this.props = {}; + this.hammer = null; - var Item = __webpack_require__(4); - var util = __webpack_require__(9); + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - /** - * @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 + // listeners for the DataSet of the items + this.itemListeners = { + 'add': function add(event, params, senderId) { + me._onAdd(params.items); }, - line: { - width: 0, - height: 0 + 'update': function update(event, params, senderId) { + me._onUpdate(params.items); + }, + 'remove': function remove(event, params, senderId) { + me._onRemove(params.items); } }; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + // listeners for the DataSet of the groups + this.groupListeners = { + 'add': function add(event, params, senderId) { + me._onAddGroups(params.items); + }, + 'update': function update(event, params, senderId) { + me._onUpdateGroups(params.items); + }, + 'remove': function remove(event, params, senderId) { + me._onRemoveGroups(params.items); } - } + }; - Item.call(this, data, conversion, options); + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group + this.groupIds = []; + + this.selection = []; // list with the ids of all selected nodes + this.stackDirty = true; // if true, all items will be restacked on next redraw + + this.touchParams = {}; // stores properties while dragging + // create the HTML DOM + + this._create(); + + this.setOptions(options); } - BoxItem.prototype = new Item(null, null, null); + ItemSet.prototype = new Component(); - /** - * 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; + // available item types will be registered here + ItemSet.types = { + background: BackgroundItem, + box: BoxItem, + range: RangeItem, + point: PointItem }; /** - * Repaint the item + * Create the HTML DOM for the ItemSet */ - BoxItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + ItemSet.prototype._create = function () { + var frame = document.createElement('div'); + frame.className = 'vis-itemset'; + frame['timeline-itemset'] = this; + this.dom.frame = frame; - // create main box - dom.box = document.createElement('DIV'); + // create background panel + var background = document.createElement('div'); + background.className = 'vis-background'; + frame.appendChild(background); + this.dom.background = background; - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'vis-item-content'; - dom.box.appendChild(dom.content); + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'vis-foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'vis-line'; + // create axis panel + var axis = document.createElement('div'); + axis.className = 'vis-axis'; + this.dom.axis = axis; - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'vis-dot'; + // create labelset + var labelSet = document.createElement('div'); + labelSet.className = 'vis-labelset'; + this.dom.labelSet = labelSet; - // attach this item as attribute - dom.box['timeline-item'] = this; + // create ungrouped Group + this._updateUngrouped(); - this.dirty = true; - } + // create background Group + var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); + backgroundGroup.show(); + this.groups[BACKGROUND] = backgroundGroup; - // 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; + // attach event listeners + // Note: we bind to the centerContainer for the case where the height + // of the center container is larger than of the ItemSet, so we + // can click in the empty area to create a new item or deselect an item. + this.hammer = new Hammer(this.body.dom.centerContainer); + + // drag items when selected + this.hammer.on('hammer.input', (function (event) { + if (event.isFirst) { + this._onTouch(event); + } + }).bind(this)); + this.hammer.on('panstart', this._onDragStart.bind(this)); + this.hammer.on('panmove', this._onDrag.bind(this)); + this.hammer.on('panend', this._onDragEnd.bind(this)); + this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. + + // single select (or unselect) when tapping an item + this.hammer.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.hammer.on('press', this._onMultiSelectItem.bind(this)); + + // add item on doubletap + this.hammer.on('doubletap', this._onAddItem.bind(this)); + + // attach to the DOM + this.show(); + }; + + /** + * Set options for the ItemSet. Existing options will be extended/overwritten. + * @param {Object} [options] The following options are available: + * {String} type + * Default type for the items. Choose from 'box' + * (default), 'point', 'range', or 'background'. + * The default style can be overwritten by + * individual items. + * {String} align + * Alignment for the items, only applicable for + * BoxItem. Choose 'center' (default), 'left', or + * 'right'. + * {String} orientation.item + * Orientation of the item set. Choose 'top' or + * 'bottom' (default). + * {Function} groupOrder + * A sorting function for ordering groups + * {Boolean} stack + * If true (default), items will be stacked on + * top of each other. + * {Number} margin.axis + * Margin between the axis and the items in pixels. + * Default is 20. + * {Number} margin.item.horizontal + * Horizontal margin between items in pixels. + * Default is 10. + * {Number} margin.item.vertical + * Vertical Margin between items in pixels. + * Default is 10. + * {Number} margin.item + * Margin between items in pixels in both horizontal + * and vertical direction. Default is 10. + * {Number} margin + * Set margin for both axis and items in pixels. + * {Boolean} selectable + * If true (default), items can be selected. + * {Boolean} multiselect + * If true, multiple items can be selected. + * False by default. + * {Boolean} editable + * Set all editable options to true or false + * {Boolean} editable.updateTime + * Allow dragging an item to an other moment in time + * {Boolean} editable.updateGroup + * Allow dragging an item to an other group + * {Boolean} editable.add + * Allow creating new items on double tap + * {Boolean} editable.remove + * Allow removing items by clicking the delete button + * top right of a selected item. + * {Function(item: Item, callback: Function)} onAdd + * Callback function triggered when an item is about to be added: + * when the user double taps an empty space in the Timeline. + * {Function(item: Item, callback: Function)} onUpdate + * Callback function fired when an item is about to be updated. + * This function typically has to show a dialog where the user + * change the item. If not implemented, nothing happens. + * {Function(item: Item, callback: Function)} onMove + * Fired when an item has been moved. If not implemented, + * the move action will be accepted. + * {Function(item: Item, callback: Function)} onRemove + * Fired when an item is about to be deleted. + * If not implemented, the item will be always removed. + */ + ItemSet.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + var fields = ['type', 'align', 'order', 'stack', 'selectable', 'multiselect', 'groupOrder', 'dataAttributes', 'template', 'hide', 'snap']; + util.selectiveExtend(fields, this.options, options); - // 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 ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation.item = options.orientation === 'top' ? 'top' : 'bottom'; + } else if (typeof options.orientation === 'object' && 'item' in options.orientation) { + this.options.orientation.item = options.orientation.item; + } + } - var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; + if ('margin' in options) { + if (typeof options.margin === 'number') { + this.options.margin.axis = options.margin; + this.options.margin.item.horizontal = options.margin; + this.options.margin.item.vertical = options.margin; + } else if (typeof options.margin === 'object') { + util.selectiveExtend(['axis'], this.options.margin, options.margin); + if ('item' in options.margin) { + if (typeof options.margin.item === 'number') { + this.options.margin.item.horizontal = options.margin.item; + this.options.margin.item.vertical = options.margin.item; + } else if (typeof options.margin.item === 'object') { + util.selectiveExtend(['horizontal', 'vertical'], this.options.margin.item, options.margin.item); + } + } + } + } - // update class - var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); - dom.box.className = 'vis-item vis-box' + className; - dom.line.className = 'vis-item vis-line' + className; - dom.dot.className = 'vis-item vis-dot' + className; + if ('editable' in options) { + if (typeof options.editable === 'boolean') { + this.options.editable.updateTime = options.editable; + this.options.editable.updateGroup = options.editable; + this.options.editable.add = options.editable; + this.options.editable.remove = options.editable; + } else if (typeof options.editable === 'object') { + util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable); + } + } - // 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; + // callback functions + var addCallback = (function (name) { + var fn = options[name]; + if (fn) { + if (!(fn instanceof Function)) { + throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)'); + } + this.options[name] = fn; + } + }).bind(this); + ['onAdd', 'onUpdate', 'onRemove', 'onMove', 'onMoving'].forEach(addCallback); - this.dirty = false; + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); } - - this._repaintDeleteButton(dom.box); }; /** - * Show the item in the DOM (when not already displayed). The items DOM will - * be created when needed. + * Mark the ItemSet dirty so it will refresh everything with next redraw. + * Optionally, all items can be marked as dirty and be refreshed. + * @param {{refreshItems: boolean}} [options] */ - BoxItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); + ItemSet.prototype.markDirty = function (options) { + this.groupIds = []; + this.stackDirty = true; + + if (options && options.refreshItems) { + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); } }; /** - * Hide the item from the DOM (when visible) + * Destroy the ItemSet */ - BoxItem.prototype.hide = function () { - if (this.displayed) { - var dom = this.dom; + ItemSet.prototype.destroy = function () { + this.hide(); + this.setItems(null); + this.setGroups(null); - 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.hammer = null; - this.displayed = false; - } + this.body = null; + this.conversion = null; }; /** - * Reposition the item horizontally - * @Override + * Hide the component from the DOM */ - BoxItem.prototype.repositionX = function () { - var start = this.conversion.toScreen(this.data.start); - var align = this.options.align; - var left; - - // 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; + ItemSet.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); } - // reposition box - this.dom.box.style.left = this.left + 'px'; - - // reposition line - this.dom.line.style.left = start - this.props.line.width / 2 + 'px'; + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } - // reposition dot - this.dom.dot.style.left = start - this.props.dot.width / 2 + 'px'; + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + } }; /** - * Reposition the item vertically - * @Override + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed */ - BoxItem.prototype.repositionY = function () { - var orientation = this.options.orientation.item; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; - - if (orientation == 'top') { - box.style.top = (this.top || 0) + 'px'; - - 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; - - box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; - line.style.top = itemSetHeight - lineHeight + 'px'; - line.style.bottom = '0'; + ItemSet.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); } - dot.style.top = -this.props.dot.height / 2 + 'px'; - }; + // show axis with dots + if (!this.dom.axis.parentNode) { + this.body.dom.backgroundVertical.appendChild(this.dom.axis); + } - /** - * Return the width of the item left from its start date - * @return {number} - */ - BoxItem.prototype.getWidthLeft = function () { - return this.width / 2; + // show labelset containing labels + if (!this.dom.labelSet.parentNode) { + this.body.dom.left.appendChild(this.dom.labelSet); + } }; /** - * Return the width of the item right from its start date - * @return {number} + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {string[] | string} [ids] An array with zero or more id's of the items to be + * selected, or a single item id. If ids is undefined + * or an empty array, all items will be unselected. */ - BoxItem.prototype.getWidthRight = function () { - return this.width / 2; - }; - - module.exports = BoxItem; - -/***/ }, -/* 41 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; + ItemSet.prototype.setSelection = function (ids) { + var i, ii, id, item; - var Hammer = __webpack_require__(5); - var Item = __webpack_require__(4); - var BackgroundGroup = __webpack_require__(39); - var RangeItem = __webpack_require__(37); + if (ids == undefined) ids = []; + if (!Array.isArray(ids)) ids = [ids]; - /** - * @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 - } - }; - this.overflow = false; // if contents can overflow (css styling), this flag is set to true + // unselect currently selected items + for (i = 0, ii = this.selection.length; i < ii; i++) { + id = this.selection[i]; + item = this.items[id]; + if (item) item.unselect(); + } - // 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); + // select items + this.selection = []; + for (i = 0, ii = ids.length; i < ii; i++) { + id = ids[i]; + item = this.items[id]; + if (item) { + this.selection.push(id); + item.select(); } } - - Item.call(this, data, conversion, options); - } - - BackgroundItem.prototype = new Item(null, null, null); - - BackgroundItem.prototype.baseClassName = 'vis-item vis-background'; - BackgroundItem.prototype.stack = false; + }; /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ - BackgroundItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; + ItemSet.prototype.getSelection = function () { + return this.selection.concat([]); }; /** - * Repaint the item + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - 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() - - // frame box (to prevent the item contents from overflowing - dom.frame = document.createElement('div'); - dom.frame.className = 'vis-item-overflow'; - dom.box.appendChild(dom.frame); - - // contents box - dom.content = document.createElement('div'); - dom.content.className = 'vis-item-content'; - dom.frame.appendChild(dom.content); - - // Note: we do NOT attach this item as attribute to the DOM, - // such that background items cannot be selected - //dom.box['timeline-item'] = this; + ItemSet.prototype.getVisibleItems = function () { + var range = this.body.range.getRange(); + var left = this.body.util.toScreen(range.start); + var right = this.body.util.toScreen(range.end); - this.dirty = true; - } + var ids = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + var group = this.groups[groupId]; + var rawVisibleItems = group.visibleItems; - // 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'); + // filter the "raw" set with visibleItems into a set which is really + // visible by pixels + for (var i = 0; i < rawVisibleItems.length; i++) { + var item = rawVisibleItems[i]; + // TODO: also check whether visible vertically + if (item.left < right && item.left + item.width > left) { + ids.push(item.id); + } + } } - 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 ? ' vis-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; - } + return ids; }; /** - * 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 + * Deselect a selected item + * @param {String | Number} id + * @private */ - BackgroundItem.prototype.repositionY = function (margin) { - var onTop = this.options.orientation.item === '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) { - // TODO: instead of calculating the top position of the subgroups here for every BackgroundItem, calculate the top of the subgroup once in Itemset - - var itemSubgroup = this.data.subgroup; - var subgroups = this.parent.subgroups; - var subgroupIndex = subgroups[itemSubgroup].index; - // 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[itemSubgroup].height + margin.item.vertical; - height += subgroupIndex == 0 ? margin.axis - 0.5 * margin.item.vertical : 0; - var newTop = this.parent.top; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroupIndex) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } - } - - // the others will have to be offset downwards with this same distance. - newTop += subgroupIndex != 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; - var totalHeight = 0; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true) { - var newHeight = subgroups[subgroup].height + margin.item.vertical; - totalHeight += newHeight; - if (subgroups[subgroup].index > subgroupIndex) { - newTop += newHeight; - } - } - } - } - height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - this.dom.box.style.top = this.parent.height - totalHeight + newTop + 'px'; - this.dom.box.style.bottom = ''; - } - } - // and in the case of no subgroups: - else { - // we want backgrounds with groups to only show in groups. - if (this.parent instanceof BackgroundGroup) { - // if the item is not in a group: - height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); - this.dom.box.style.top = onTop ? '0' : ''; - this.dom.box.style.bottom = onTop ? '' : '0'; - } else { - 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 = ''; + ItemSet.prototype._deselect = function (id) { + var selection = this.selection; + for (var i = 0, ii = selection.length; i < ii; i++) { + if (selection[i] == id) { + // non-strict comparison! + selection.splice(i, 1); + break; } } - this.dom.box.style.height = height + 'px'; }; - module.exports = BackgroundItem; + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + ItemSet.prototype.redraw = function () { + var margin = this.options.margin, + range = this.body.range, + asSize = util.option.asSize, + options = this.options, + orientation = options.orientation.item, + resized = false, + frame = this.dom.frame; -/***/ }, -/* 42 */ -/***/ function(module, exports, __webpack_require__) { + // recalculate absolute position (before redrawing groups) + this.props.top = this.body.domProps.top.height + this.body.domProps.border.top; + this.props.left = this.body.domProps.left.width + this.body.domProps.border.left; - 'use strict'; + // update class name + frame.className = 'vis-itemset'; - var util = __webpack_require__(9); - var Component = __webpack_require__(28); - var TimeStep = __webpack_require__(38); - var DateUtil = __webpack_require__(32); - var moment = __webpack_require__(10); + // reorder the groups (if needed) + resized = this._orderGroups() || resized; - /** - * A horizontal time axis - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body - * @param {Object} [options] See TimeAxis.setOptions for the available - * options. - * @constructor TimeAxis - * @extends Component - */ - function TimeAxis(body, options) { - this.dom = { - foreground: null, - lines: [], - majorTexts: [], - minorTexts: [], - redundant: { - lines: [], - majorTexts: [], - minorTexts: [] - } - }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 - }, - lineTop: 0 + // check whether zoomed (in that case we need to re-stack everything) + // TODO: would be nicer to get this as a trigger from Range + var visibleInterval = range.end - range.start; + var zoomed = visibleInterval != this.lastVisibleInterval || this.props.width != this.props.lastWidth; + if (zoomed) this.stackDirty = true; + this.lastVisibleInterval = visibleInterval; + this.props.lastWidth = this.props.width; + + var restack = this.stackDirty; + var firstGroup = this._firstGroup(); + var firstMargin = { + item: margin.item, + axis: margin.axis }; - - this.defaultOptions = { - orientation: { - axis: 'bottom' - }, // axis orientation: 'top' or 'bottom' - showMinorLabels: true, - showMajorLabels: true, - format: TimeStep.FORMAT, - timeAxis: null + var nonFirstMargin = { + item: margin.item, + axis: margin.item.vertical / 2 }; - this.options = util.extend({}, this.defaultOptions); + var height = 0; + var minHeight = margin.axis + margin.item.vertical; - this.body = body; + // redraw the background group + this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); - // create the HTML DOM - this._create(); + // redraw all regular groups + util.forEach(this.groups, function (group) { + var groupMargin = group == firstGroup ? firstMargin : nonFirstMargin; + var groupResized = group.redraw(range, groupMargin, restack); + resized = groupResized || resized; + height += group.height; + }); + height = Math.max(height, minHeight); + this.stackDirty = false; - this.setOptions(options); - } + // update frame height + frame.style.height = asSize(height); - TimeAxis.prototype = new Component(); + // calculate actual size + this.props.width = frame.offsetWidth; + this.props.height = height; + + // reposition axis + this.dom.axis.style.top = asSize(orientation == 'top' ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); + this.dom.axis.style.left = '0'; + + // check if this component is resized + resized = this._isResized() || resized; + + return resized; + }; /** - * Set options for the TimeAxis. - * Parameters will be merged in current options. - * @param {Object} options Available options: - * {string} [orientation.axis] - * {boolean} [showMinorLabels] - * {boolean} [showMajorLabels] + * Get the first group, aligned with the axis + * @return {Group | null} firstGroup + * @private */ - TimeAxis.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['showMinorLabels', 'showMajorLabels', 'hiddenDates', 'timeAxis'], this.options, options); + ItemSet.prototype._firstGroup = function () { + var firstGroupIndex = this.options.orientation.item == 'top' ? 0 : this.groupIds.length - 1; + var firstGroupId = this.groupIds[firstGroupIndex]; + var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; - // deep copy the format options - util.selectiveDeepExtend(['format'], this.options, options); + return firstGroup || null; + }; - if ('orientation' in options) { - if (typeof options.orientation === 'string') { - this.options.orientation.axis = options.orientation; - } else if (typeof options.orientation === 'object' && 'axis' in options.orientation) { - this.options.orientation.axis = options.orientation.axis; + /** + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. + * @protected + */ + ItemSet.prototype._updateUngrouped = function () { + var ungrouped = this.groups[UNGROUPED]; + var background = this.groups[BACKGROUND]; + var item, itemId; + + if (this.groupsData) { + // remove the group holding all ungrouped items + if (ungrouped) { + ungrouped.hide(); + delete this.groups[UNGROUPED]; + + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + item.parent && item.parent.remove(item); + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + group && group.add(item) || item.hide(); + } } } + } else { + // create a group holding all (unfiltered) items + if (!ungrouped) { + var id = null; + var data = null; + ungrouped = new Group(id, data, this); + this.groups[UNGROUPED] = ungrouped; - // apply locale to moment.js - // TODO: not so nice, this is applied globally to moment.js - if ('locale' in options) { - if (typeof moment.locale === 'function') { - // moment.js 2.8.1+ - moment.locale(options.locale); - } else { - moment.lang(options.locale); + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + ungrouped.add(item); + } } + + ungrouped.show(); } } }; /** - * Create the HTML DOM for the TimeAxis - */ - TimeAxis.prototype._create = function () { - this.dom.foreground = document.createElement('div'); - this.dom.background = document.createElement('div'); - - this.dom.foreground.className = 'vis-time-axis vis-foreground'; - this.dom.background.className = 'vis-time-axis vis-background'; - }; - - /** - * Destroy the TimeAxis + * Get the element for the labelset + * @return {HTMLElement} labelSet */ - TimeAxis.prototype.destroy = function () { - // remove from DOM - if (this.dom.foreground.parentNode) { - this.dom.foreground.parentNode.removeChild(this.dom.foreground); - } - if (this.dom.background.parentNode) { - this.dom.background.parentNode.removeChild(this.dom.background); - } - - this.body = null; + ItemSet.prototype.getLabelSet = function () { + return this.dom.labelSet; }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Set items + * @param {vis.DataSet | null} items */ - TimeAxis.prototype.redraw = function () { - var props = this.props; - var foreground = this.dom.foreground; - var background = this.dom.background; - - // determine the correct parent DOM element (depending on option orientation) - var parent = this.options.orientation.axis == 'top' ? this.body.dom.top : this.body.dom.bottom; - var parentChanged = foreground.parentNode !== parent; - - // calculate character width and height - this._calculateCharSize(); - - // TODO: recalculate sizes only needed when parent is resized or options is changed - var showMinorLabels = this.options.showMinorLabels && this.options.orientation.axis !== 'none'; - var showMajorLabels = this.options.showMajorLabels && this.options.orientation.axis !== 'none'; + ItemSet.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; - // determine the width and height of the elemens for the axis - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - props.height = props.minorLabelHeight + props.majorLabelHeight; - props.width = foreground.offsetWidth; + // replace the dataset + if (!items) { + this.itemsData = null; + } else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } else { + throw new TypeError('Data must be an instance of DataSet or DataView'); + } - props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - (this.options.orientation.axis == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height); - props.minorLineWidth = 1; // TODO: really calculate width - props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight; - props.majorLineWidth = 1; // TODO: really calculate width + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - // take foreground and background offline while updating (is almost twice as fast) - var foregroundNextSibling = foreground.nextSibling; - var backgroundNextSibling = background.nextSibling; - foreground.parentNode && foreground.parentNode.removeChild(foreground); - background.parentNode && background.parentNode.removeChild(background); + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } - foreground.style.height = this.props.height + 'px'; + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - this._repaintLabels(); + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); - // put DOM online again (at the same place) - if (foregroundNextSibling) { - parent.insertBefore(foreground, foregroundNextSibling); - } else { - parent.appendChild(foreground); - } - if (backgroundNextSibling) { - this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling); - } else { - this.body.dom.backgroundVertical.appendChild(background); + // update the group holding all ungrouped items + this._updateUngrouped(); } + }; - return this._isResized() || parentChanged; + /** + * Get the current items + * @returns {vis.DataSet | null} + */ + ItemSet.prototype.getItems = function () { + return this.itemsData; }; /** - * Repaint major and minor text labels and vertical grid lines - * @private + * Set groups + * @param {vis.DataSet} groups */ - TimeAxis.prototype._repaintLabels = function () { - var orientation = this.options.orientation.axis; + ItemSet.prototype.setGroups = function (groups) { + var me = this, + ids; - // calculate range and step (step such that we have space for 7 characters per label) - var start = util.convert(this.body.range.start, 'Number'); - var end = util.convert(this.body.range.end, 'Number'); - var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf(); - var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this.body.range, timeLabelsize); - minimumStep -= this.body.util.toTime(0).valueOf(); + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.off(event, callback); + }); - var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates); - if (this.options.format) { - step.setFormat(this.options.format); - } - if (this.options.timeAxis) { - step.setScale(this.options.timeAxis); + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw } - this.step = step; - - // Move all DOM elements to a "redundant" list, where they - // can be picked for re-use, and clear the lists with lines and texts. - // At the end of the function _repaintLabels, left over elements will be cleaned up - var dom = this.dom; - dom.redundant.lines = dom.lines; - dom.redundant.majorTexts = dom.majorTexts; - dom.redundant.minorTexts = dom.minorTexts; - dom.lines = []; - dom.majorTexts = []; - dom.minorTexts = []; - var cur; - var x = 0; - var isMajor; - var xPrev = 0; - var width = 0; - var prevLine; - var xFirstMajorLabel = undefined; - var max = 0; - var className; - - step.first(); - while (step.hasNext() && max < 1000) { - max++; + // 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'); + } - cur = step.getCurrent(); - isMajor = step.isMajor(); - className = step.getClassName(); + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); - xPrev = x; - x = this.body.util.toScreen(cur); - width = x - xPrev; - if (prevLine) { - prevLine.style.width = width + 'px'; - } + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } - if (this.options.showMinorLabels) { - this._repaintMinorText(x, step.getLabelMinor(), orientation, className); - } + // update the group holding all ungrouped items + this._updateUngrouped(); - if (isMajor && this.options.showMajorLabels) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; - } - this._repaintMajorText(x, step.getLabelMajor(), orientation, className); - } - prevLine = this._repaintMajorLine(x, orientation, className); - } else { - prevLine = this._repaintMinorLine(x, orientation, className); - } + // update the order of all items in each group + this._order(); - step.next(); - } + this.body.emitter.emit('change', { queue: true }); + }; - // create a major label on the left when needed - if (this.options.showMajorLabels) { - var leftTime = this.body.util.toTime(0), - leftText = step.getLabelMajor(leftTime), - widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation + /** + * Get the current groups + * @returns {vis.DataSet | null} groups + */ + ItemSet.prototype.getGroups = function () { + return this.groupsData; + }; - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText, orientation, className); - } - } + /** + * Remove an item by its id + * @param {String | Number} id + */ + ItemSet.prototype.removeItem = function (id) { + var item = this.itemsData.get(id), + dataset = this.itemsData.getDataSet(); - // Cleanup leftover DOM elements from the redundant list - util.forEach(this.dom.redundant, function (arr) { - while (arr.length) { - var elem = arr.pop(); - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); + if (item) { + // confirm deletion + this.options.onRemove(item, function (item) { + if (item) { + // remove by id here, it is possible that an item has no id defined + // itself, so better not delete by the item itself + dataset.remove(id); } - } - }); + }); + } }; /** - * Create a minor label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className + * Get the time of an item based on it's data and options.type + * @param {Object} itemData + * @returns {string} Returns the type * @private */ - TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); + ItemSet.prototype._getType = function (itemData) { + return itemData.type || this.options.type || (itemData.end ? 'range' : 'box'); + }; - if (!label) { - // create new label - var content = document.createTextNode(''); - label = document.createElement('div'); - label.appendChild(content); - this.dom.foreground.appendChild(label); + /** + * Get the group id for an item + * @param {Object} itemData + * @returns {string} Returns the groupId + * @private + */ + ItemSet.prototype._getGroupId = function (itemData) { + var type = this._getType(itemData); + if (type == 'background' && itemData.group == undefined) { + return BACKGROUND; + } else { + return this.groupsData ? itemData.group : UNGROUPED; } - this.dom.minorTexts.push(label); - - label.childNodes[0].nodeValue = text; - - label.style.top = orientation == 'top' ? this.props.majorLabelHeight + 'px' : '0'; - label.style.left = x + 'px'; - label.className = 'vis-text vis-minor ' + className; - //label.title = title; // TODO: this is a heavy operation }; /** - * Create a Major label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className - * @private + * Handle updated items + * @param {Number[]} ids + * @protected */ - TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); + ItemSet.prototype._onUpdate = function (ids) { + var me = this; - if (!label) { - // create label - var content = document.createTextNode(text); - label = document.createElement('div'); - label.appendChild(content); - this.dom.foreground.appendChild(label); - } - this.dom.majorTexts.push(label); + ids.forEach((function (id) { + var itemData = me.itemsData.get(id, me.itemOptions); + var item = me.items[id]; + var type = me._getType(itemData); - label.childNodes[0].nodeValue = text; - label.className = 'vis-text vis-major ' + className; - //label.title = title; // TODO: this is a heavy operation + var constructor = ItemSet.types[type]; + var selected; - label.style.top = orientation == 'top' ? '0' : this.props.minorLabelHeight + 'px'; - label.style.left = x + 'px'; + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, delete the item and recreate it + selected = item.selected; // preserve selection of this item + me._removeItem(item); + item = null; + } else { + me._updateItem(item, itemData); + } + } + + if (!item) { + // create item + if (constructor) { + item = new constructor(itemData, me.conversion, me.options); + item.id = id; // TODO: not so nice setting id afterwards + me._addItem(item); + if (selected) { + this.selection.push(id); + item.select(); + } + } else if (type == 'rangeoverflow') { + // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day + throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' + '.vis-item.vis-range .vis-item-content {overflow: visible;}'); + } else { + throw new TypeError('Unknown item type "' + type + '"'); + } + } + }).bind(this)); + + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); }; /** - * Create a minor line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className - * @return {Element} Returns the created line - * @private + * Handle added items + * @param {Number[]} ids + * @protected */ - TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) { - // reuse redundant line - var line = this.dom.redundant.lines.shift(); - if (!line) { - // create vertical line - line = document.createElement('div'); - this.dom.background.appendChild(line); - } - this.dom.lines.push(line); + ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; + + /** + * Handle removed items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onRemove = function (ids) { + var count = 0; + var me = this; + ids.forEach(function (id) { + var item = me.items[id]; + if (item) { + count++; + me._removeItem(item); + } + }); - var props = this.props; - if (orientation == 'top') { - line.style.top = props.majorLabelHeight + 'px'; - } else { - line.style.top = this.body.domProps.top.height + 'px'; + if (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change', { queue: true }); } - line.style.height = props.minorLineHeight + 'px'; - line.style.left = x - props.minorLineWidth / 2 + 'px'; - - line.className = 'vis-grid vis-vertical vis-minor ' + className; - - return line; }; /** - * Create a Major line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className - * @return {Element} Returns the created line + * Update the order of item in all groups * @private */ - TimeAxis.prototype._repaintMajorLine = function (x, orientation, className) { - // reuse redundant line - var line = this.dom.redundant.lines.shift(); - if (!line) { - // create vertical line - line = document.createElement('div'); - this.dom.background.appendChild(line); - } - this.dom.lines.push(line); - - var props = this.props; - if (orientation == 'top') { - line.style.top = '0'; - } else { - line.style.top = this.body.domProps.top.height + 'px'; - } - line.style.left = x - props.majorLineWidth / 2 + 'px'; - line.style.height = props.majorLineHeight + 'px'; - - line.className = 'vis-grid vis-vertical vis-major ' + className; - - return line; + ItemSet.prototype._order = function () { + // reorder the items in all groups + // TODO: optimization: only reorder groups affected by the changed items + util.forEach(this.groups, function (group) { + group.order(); + }); }; /** - * 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. + * Handle updated groups + * @param {Number[]} ids * @private */ - TimeAxis.prototype._calculateCharSize = function () { - // Note: We calculate char size with every redraw. Size may change, for - // example when any of the timelines parents had display:none for example. + ItemSet.prototype._onUpdateGroups = function (ids) { + this._onAddGroups(ids); + }; - // determine the char width and height on the minor axis - if (!this.dom.measureCharMinor) { - this.dom.measureCharMinor = document.createElement('DIV'); - this.dom.measureCharMinor.className = 'vis-text vis-minor vis-measure'; - this.dom.measureCharMinor.style.position = 'absolute'; + /** + * Handle changed groups (added or updated) + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onAddGroups = function (ids) { + var me = this; - this.dom.measureCharMinor.appendChild(document.createTextNode('0')); - this.dom.foreground.appendChild(this.dom.measureCharMinor); - } - this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; - this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; - // determine the char width and height on the major axis - if (!this.dom.measureCharMajor) { - this.dom.measureCharMajor = document.createElement('DIV'); - this.dom.measureCharMajor.className = 'vis-text vis-major vis-measure'; - this.dom.measureCharMajor.style.position = 'absolute'; + if (!group) { + // check for reserved ids + if (id == UNGROUPED || id == BACKGROUND) { + throw new Error('Illegal group id. ' + id + ' is a reserved id.'); + } - this.dom.measureCharMajor.appendChild(document.createTextNode('0')); - this.dom.foreground.appendChild(this.dom.measureCharMajor); - } - this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; - this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; - }; + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); - module.exports = TimeAxis; + group = new Group(id, groupData, me); + me.groups[id] = group; -/***/ }, -/* 43 */ -/***/ function(module, exports, __webpack_require__) { + // add items with this groupId to the new group + for (var itemId in me.items) { + if (me.items.hasOwnProperty(itemId)) { + var item = me.items[itemId]; + if (item.data.group == id) { + group.add(item); + } + } + } - 'use strict'; + group.order(); + group.show(); + } else { + // update group + group.setData(groupData); + } + }); - var keycharm = __webpack_require__(44); - var Emitter = __webpack_require__(20); - var Hammer = __webpack_require__(5); - var util = __webpack_require__(9); + this.body.emitter.emit('change', { queue: true }); + }; /** - * 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 + * Handle removed groups + * @param {Number[]} ids + * @private */ - function Activator(container) { - this.active = false; - - this.dom = { - container: container - }; + ItemSet.prototype._onRemoveGroups = function (ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; - this.dom.overlay = document.createElement('div'); - this.dom.overlay.className = 'vis-overlay'; + if (group) { + group.hide(); + delete groups[id]; + } + }); - this.dom.container.appendChild(this.dom.overlay); + this.markDirty(); - this.hammer = Hammer(this.dom.overlay); - this.hammer.on('tap', this._onTapOverlay.bind(this)); + this.body.emitter.emit('change', { queue: true }); + }; - // block all touch events (except tap) - var me = this; - var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend']; - events.forEach(function (event) { - me.hammer.on(event, function (event) { - event.stopPropagation(); + /** + * Reorder the groups if needed + * @return {boolean} changed + * @private + */ + ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder }); - }); - // attach a click event to the window, in order to deactivate when clicking outside the timeline - if (document && document.body) { - this.onClick = function (event) { - if (!_hasParent(event.target, container)) { - me.deactivate(); - } - }; - document.body.addEventListener('click', this.onClick); - } + var changed = !util.equalArray(groupIds, this.groupIds); + if (changed) { + // hide all groups, removes them from the DOM + var groups = this.groups; + groupIds.forEach(function (groupId) { + groups[groupId].hide(); + }); - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } - this.keycharm = keycharm(); + // show the groups again, attach them to the DOM in correct order + groupIds.forEach(function (groupId) { + groups[groupId].show(); + }); - // keycharm listener only bounded when active) - this.escListener = this.deactivate.bind(this); - } + this.groupIds = groupIds; + } - // turn into an event emitter - Emitter(Activator.prototype); + return changed; + } else { + return false; + } + }; - // The currently active activator - Activator.current = null; + /** + * Add a new item + * @param {Item} item + * @private + */ + ItemSet.prototype._addItem = function (item) { + this.items[item.id] = item; + + // add to group + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + }; /** - * Destroy the activator. Cleans up all created DOM and event listeners + * Update an existing item + * @param {Item} item + * @param {Object} itemData + * @private */ - Activator.prototype.destroy = function () { - this.deactivate(); + ItemSet.prototype._updateItem = function (item, itemData) { + var oldGroupId = item.data.group; + var oldSubGroupId = item.data.subgroup; - // remove dom - this.dom.overlay.parentNode.removeChild(this.dom.overlay); + // update the items data (will redraw the item when displayed) + item.setData(itemData); - // remove global event listener - if (this.onClick) { - document.body.removeEventListener('click', this.onClick); - } + // update group + if (oldGroupId != item.data.group || oldSubGroupId != item.data.subgroup) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); - // cleanup hammer instances - this.hammer.destroy(); - this.hammer = null; - // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + } }; /** - * Activate the element - * Overlay is hidden, element is decorated with a blue shadow border + * Delete an item from the ItemSet: remove it from the DOM, from the map + * with items, and from the map with visible items, and from the selection + * @param {Item} item + * @private */ - Activator.prototype.activate = function () { - // we allow only one active activator at a time - if (Activator.current) { - Activator.current.deactivate(); - } - Activator.current = this; + ItemSet.prototype._removeItem = function (item) { + // remove from DOM + item.hide(); - this.active = true; - this.dom.overlay.style.display = 'none'; - util.addClassName(this.dom.container, 'vis-active'); + // remove from items + delete this.items[item.id]; - this.emit('change'); - this.emit('activate'); + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); - // ugly hack: bind ESC after emitting the events, as the Network rebinds all - // keyboard events on a 'change' event - this.keycharm.bind('esc', this.escListener); + // remove from group + item.parent && item.parent.remove(item); }; /** - * Deactivate the element - * Overlay is displayed on top of the element + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} + * @private */ - Activator.prototype.deactivate = function () { - this.active = false; - this.dom.overlay.style.display = ''; - util.removeClassName(this.dom.container, 'vis-active'); - this.keycharm.unbind('esc', this.escListener); + ItemSet.prototype._constructByEndArray = function (array) { + var endArray = []; - this.emit('change'); - this.emit('deactivate'); + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof RangeItem) { + endArray.push(array[i]); + } + } + return endArray; }; /** - * Handle a tap event: activate the container - * @param event + * Register the clicked item on touch, before dragStart is initiated. + * + * dragStart is initiated from a mousemove event, AFTER the mouse/touch is + * already moving. Therefore, the mouse/touch can sometimes be above an other + * DOM element than the item itself. + * + * @param {Event} event * @private */ - Activator.prototype._onTapOverlay = function (event) { - // activate the container - this.activate(); - event.stopPropagation(); + ItemSet.prototype._onTouch = function (event) { + // store the touched item, used in _onDragStart + this.touchParams.item = this.itemFromTarget(event); + this.touchParams.dragLeftItem = event.target.dragLeftItem || false; + this.touchParams.dragRightItem = event.target.dragRightItem || false; + this.touchParams.itemProps = null; }; /** - * 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. + * Start dragging the selected events + * @param {Event} event * @private */ - function _hasParent(element, parent) { - while (element) { - if (element === parent) { - return true; + ItemSet.prototype._onDragStart = function (event) { + var item = this.touchParams.item || null; + var me = this; + var props; + + if (item && item.selected) { + + if (!this.options.editable.updateTime && !this.options.editable.updateGroup && !item.editable) { + return; } - element = element.parentNode; - } - return false; - } - module.exports = Activator; + // override options.editable + if (item.editable === false) { + return; + } -/***/ }, -/* 44 */ -/***/ function(module, exports, __webpack_require__) { + var dragLeftItem = this.touchParams.dragLeftItem; + var dragRightItem = this.touchParams.dragRightItem; - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; - /** - * Created by Alex on 11/6/2014. - */ + if (dragLeftItem) { + props = { + item: dragLeftItem, + initialX: event.center.x, + dragLeft: true, + data: util.extend({}, item.data) // clone the items data + }; - // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 - // if the module has no dependencies, the above pattern can be simplified to - (function (root, factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - root.keycharm = factory(); + this.touchParams.itemProps = [props]; + } else if (dragRightItem) { + props = { + item: dragRightItem, + initialX: event.center.x, + dragRight: true, + data: util.extend({}, item.data) // clone the items data + }; + + this.touchParams.itemProps = [props]; + } else { + this.touchParams.itemProps = this.getSelection().map(function (id) { + var item = me.items[id]; + var props = { + item: item, + initialX: event.center.x, + data: util.extend({}, item.data) // clone the items data + }; + + return props; + }); + } + + event.stopPropagation(); + } else if (this.options.editable.add && (event.srcEvent.ctrlKey || event.srcEvent.metaKey)) { + // create a new range item when dragging with ctrl key down + this._onDragStartAddItem(event); } - }(this, function () { + }; - function keycharm(options) { - var preventDefault = options && options.preventDefault || false; + /** + * Start creating a new range item by dragging. + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragStartAddItem = function (event) { + var snap = this.options.snap || null; + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.center.x - xAbs - 10; // minus 10 to compensate for the drag starting as soon as you've moved 10px + var time = this.body.util.toTime(x); + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var start = snap ? snap(time, scale, step) : start; + var end = start; - var container = options && options.container || window; + var itemData = { + type: 'range', + start: start, + end: end, + content: 'new item' + }; - var _exportFunctions = {}; - var _bound = {keydown:{}, keyup:{}}; - var _keys = {}; - var i; + var id = util.randomUUID(); + itemData[this.itemsData._fieldId] = id; - // a - z - for (i = 97; i <= 122; i++) {_keys[String.fromCharCode(i)] = {code:65 + (i - 97), shift: false};} - // A - Z - for (i = 65; i <= 90; i++) {_keys[String.fromCharCode(i)] = {code:i, shift: true};} - // 0 - 9 - for (i = 0; i <= 9; i++) {_keys['' + i] = {code:48 + i, shift: false};} - // F1 - F12 - for (i = 1; i <= 12; i++) {_keys['F' + i] = {code:111 + i, shift: false};} - // num0 - num9 - for (i = 0; i <= 9; i++) {_keys['num' + i] = {code:96 + i, shift: false};} + var group = this.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; + } - // numpad misc - _keys['num*'] = {code:106, shift: false}; - _keys['num+'] = {code:107, shift: false}; - _keys['num-'] = {code:109, shift: false}; - _keys['num/'] = {code:111, shift: false}; - _keys['num.'] = {code:110, shift: false}; - // arrows - _keys['left'] = {code:37, shift: false}; - _keys['up'] = {code:38, shift: false}; - _keys['right'] = {code:39, shift: false}; - _keys['down'] = {code:40, shift: false}; - // extra keys - _keys['space'] = {code:32, shift: false}; - _keys['enter'] = {code:13, shift: false}; - _keys['shift'] = {code:16, shift: undefined}; - _keys['esc'] = {code:27, shift: false}; - _keys['backspace'] = {code:8, shift: false}; - _keys['tab'] = {code:9, shift: false}; - _keys['ctrl'] = {code:17, shift: false}; - _keys['alt'] = {code:18, shift: false}; - _keys['delete'] = {code:46, shift: false}; - _keys['pageup'] = {code:33, shift: false}; - _keys['pagedown'] = {code:34, shift: false}; - // symbols - _keys['='] = {code:187, shift: false}; - _keys['-'] = {code:189, shift: false}; - _keys[']'] = {code:221, shift: false}; - _keys['['] = {code:219, shift: false}; + var newItem = new RangeItem(itemData, this.conversion, this.options); + newItem.id = id; // TODO: not so nice setting id afterwards + newItem.data = itemData; + this._addItem(newItem); + + var props = { + item: newItem, + dragRight: true, + initialX: event.center.x, + data: util.extend({}, itemData) + }; + this.touchParams.itemProps = [props]; + event.stopPropagation(); + }; + /** + * Drag selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); - var down = function(event) {handleEvent(event,'keydown');}; - var up = function(event) {handleEvent(event,'keyup');}; + var me = this; + var snap = this.options.snap || null; + var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); - // handle the actualy bound key with the event - var handleEvent = function(event,type) { - if (_bound[type][event.keyCode] !== undefined) { - var bound = _bound[type][event.keyCode]; - for (var i = 0; i < bound.length; i++) { - if (bound[i].shift === undefined) { - bound[i].fn(event); - } - else if (bound[i].shift == true && event.shiftKey == true) { - bound[i].fn(event); - } - else if (bound[i].shift == false && event.shiftKey == false) { - bound[i].fn(event); - } - } + // move + this.touchParams.itemProps.forEach(function (props) { + var newProps = {}; + var current = me.body.util.toTime(event.center.x - xOffset); + var initial = me.body.util.toTime(props.initialX - xOffset); + var offset = current - initial; - if (preventDefault == true) { - event.preventDefault(); - } - } - }; + var itemData = util.extend({}, props.item.data); // clone the data - // bind a key to a callback - _exportFunctions.bind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; - } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); - } - if (_bound[type][_keys[key].code] === undefined) { - _bound[type][_keys[key].code] = []; + if (props.item.editable === false) { + return; } - _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); - }; - - // bind all keys to a call back (demo purposes) - _exportFunctions.bindAll = function(callback, type) { - if (type === undefined) { - type = 'keydown'; - } - for (var key in _keys) { - if (_keys.hasOwnProperty(key)) { - _exportFunctions.bind(key,callback,type); - } - } - }; + var updateTimeAllowed = me.options.editable.updateTime || props.item.editable === true; - // get the key label from an event - _exportFunctions.getKey = function(event) { - for (var key in _keys) { - if (_keys.hasOwnProperty(key)) { - if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { - return key; + if (updateTimeAllowed) { + if (props.dragLeft) { + // drag left side of a range item + if (itemData.start != undefined) { + var initialStart = util.convert(props.data.start, 'Date'); + var start = new Date(initialStart.valueOf() + offset); + itemData.start = snap ? snap(start, scale, step) : start; } - else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { - return key; + } else if (props.dragRight) { + // drag right side of a range item + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var end = new Date(initialEnd.valueOf() + offset); + itemData.end = snap ? snap(end, scale, step) : end; } - else if (event.keyCode == _keys[key].code && key == 'shift') { - return key; + } else { + // drag both start and end + if (itemData.start != undefined) { + var initialStart = util.convert(props.data.start, 'Date').valueOf(); + var start = new Date(initialStart + offset); + + if (itemData.end != undefined) { + var initialEnd = util.convert(props.data.end, 'Date'); + var duration = initialEnd.valueOf() - initialStart.valueOf(); + + itemData.start = snap ? snap(start, scale, step) : start; + itemData.end = new Date(itemData.start.valueOf() + duration); + } else { + itemData.start = snap ? snap(start, scale, step) : start; + } } } } - return "unknown key, currently not supported"; - }; - // unbind either a specific callback from a key or all of them (by leaving callback undefined) - _exportFunctions.unbind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; - } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); - } - if (callback !== undefined) { - var newBindings = []; - var bound = _bound[type][_keys[key].code]; - if (bound !== undefined) { - for (var i = 0; i < bound.length; i++) { - if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { - newBindings.push(_bound[type][_keys[key].code][i]); - } + var updateGroupAllowed = me.options.editable.updateGroup || props.item.editable === true; + + if (updateGroupAllowed && (!props.dragLeft && !props.dragRight)) { + if (itemData.group != undefined) { + // drag from one group to another + var group = me.groupFromTarget(event); + if (group) { + itemData.group = group.groupId; } } - _bound[type][_keys[key].code] = newBindings; - } - else { - _bound[type][_keys[key].code] = []; } - }; - // reset all bound variables. - _exportFunctions.reset = function() { - _bound = {keydown:{}, keyup:{}}; - }; + // confirm moving the item + me.options.onMoving(itemData, function (itemData) { + if (itemData) { + props.item.setData(itemData); + } + }); + }); - // unbind all listeners and reset all variables. - _exportFunctions.destroy = function() { - _bound = {keydown:{}, keyup:{}}; - container.removeEventListener('keydown', down, true); - container.removeEventListener('keyup', up, true); - }; + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); + } + }; - // create listeners. - container.addEventListener('keydown',down,true); - container.addEventListener('keyup',up,true); + /** + * Move an item to another group + * @param {Item} item + * @param {String | Number} groupId + * @private + */ + ItemSet.prototype._moveToGroup = function (item, groupId) { + var group = this.groups[groupId]; + if (group && group.groupId != item.data.group) { + var oldGroup = item.parent; + oldGroup.remove(item); + oldGroup.order(); + group.add(item); + group.order(); - // return the public functions. - return _exportFunctions; + item.data.group = group.groupId; } + }; - return keycharm; - })); + /** + * End of dragging selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + event.stopPropagation(); + // prepare a change set for the changed items + var changes = []; + var me = this; + var dataset = this.itemsData.getDataSet(); + var itemProps = this.touchParams.itemProps; + this.touchParams.itemProps = null; + itemProps.forEach(function (props) { + var id = props.item.id; + var exists = me.itemsData.get(id, me.itemOptions) != null; + if (!exists) { + // add a new item + me.options.onAdd(props.item.data, function (itemData) { + me._removeItem(props.item); // remove temporary item + if (itemData) { + me.itemsData.getDataSet().add(itemData); + } -/***/ }, -/* 45 */ -/***/ function(module, exports, __webpack_require__) { + // force re-stacking of all items next redraw + me.stackDirty = true; + me.body.emitter.emit('change'); + }); + } else { + // update existing item + var itemData = util.extend({}, props.item.data); // clone the data + me.options.onMove(itemData, function (itemData) { + if (itemData) { + // apply changes + itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) + changes.push(itemData); + } else { + // restore original values + props.item.setData(props.data); - 'use strict'; + me.stackDirty = true; // force re-stacking of all items next redraw + me.body.emitter.emit('change'); + } + }); + } + }); - var Hammer = __webpack_require__(5); - var util = __webpack_require__(9); - var Component = __webpack_require__(28); - var moment = __webpack_require__(10); - var locales = __webpack_require__(29); + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); + } + } + }; /** - * A custom time bar - * @param {{range: Range, dom: Object}} body - * @param {Object} [options] Available parameters: - * {number | string} id - * {string} locales - * {string} locale - * @constructor CustomTime - * @extends Component + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private */ + ItemSet.prototype._onSelectItem = function (event) { + if (!this.options.selectable) return; - function CustomTime(body, options) { - this.body = body; - - // default options - this.defaultOptions = { - locales: locales, - locale: 'en', - id: undefined - }; - this.options = util.extend({}, this.defaultOptions); - - if (options && options.time) { - this.customTime = options.time; - } else { - this.customTime = new Date(); + var ctrlKey = event.srcEvent && (event.srcEvent.ctrlKey || event.srcEvent.metaKey); + var shiftKey = event.srcEvent && event.srcEvent.shiftKey; + if (ctrlKey || shiftKey) { + this._onMultiSelectItem(event); + return; } - this.eventParams = {}; // stores state parameters while dragging the bar - - this.setOptions(options); + var oldSelection = this.getSelection(); - // create the DOM - this._create(); - } + var item = this.itemFromTarget(event); + var selection = item ? [item.id] : []; + this.setSelection(selection); - CustomTime.prototype = new Component(); + var newSelection = this.getSelection(); - /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {number | string} id - * {string} locales - * {string} locale - */ - CustomTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['locale', 'locales', 'id'], this.options, options); + // emit a select event, + // except when old selection is empty and new selection is still empty + if (newSelection.length > 0 || oldSelection.length > 0) { + this.body.emitter.emit('select', { + items: newSelection, + event: event + }); } }; /** - * Create the DOM for the custom time + * Handle creation and updates of an item on double tap + * @param event * @private */ - CustomTime.prototype._create = function () { - var bar = document.createElement('div'); - bar['custom-time'] = this; - bar.className = 'vis-custom-time ' + (this.options.id || ''); - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; - this.bar = bar; + ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; - var drag = document.createElement('div'); - drag.style.position = 'relative'; - drag.style.top = '0px'; - drag.style.left = '-10px'; - drag.style.height = '100%'; - drag.style.width = '20px'; - bar.appendChild(drag); + var me = this; + var snap = this.options.snap || null; + var item = this.itemFromTarget(event); - // attach event listeners - this.hammer = new Hammer(drag); - this.hammer.on('panstart', this._onDragStart.bind(this)); - this.hammer.on('panmove', this._onDrag.bind(this)); - this.hammer.on('panend', this._onDragEnd.bind(this)); - this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. - // TODO: cleanup - //this.hammer.on('pan', function (event) { - // event.preventDefault(); - //}); - }; + event.stopPropagation(); - /** - * Destroy the CustomTime bar - */ - CustomTime.prototype.destroy = function () { - this.hide(); + if (item) { + // update item - this.hammer.destroy(); - this.hammer = null; + // execute async handler to update the item (or cancel it) + var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset + this.options.onUpdate(itemData, function (itemData) { + if (itemData) { + me.itemsData.getDataSet().update(itemData); + } + }); + } else { + // add item + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.center.x - xAbs; + var start = this.body.util.toTime(x); + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); - this.body = null; - }; + var newItem = { + start: snap ? snap(start, scale, step) : start, + content: 'new item' + }; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - CustomTime.prototype.redraw = function () { - var parent = this.body.dom.backgroundVertical; - if (this.bar.parentNode != parent) { - // attach to the dom - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); + // when default type is a range, add a default end date to the new item + if (this.options.type === 'range') { + var end = this.body.util.toTime(x + this.props.width / 5); + newItem.end = snap ? snap(end, scale, step) : end; } - parent.appendChild(this.bar); - } - var x = this.body.util.toScreen(this.customTime); + newItem[this.itemsData._fieldId] = util.randomUUID(); - var locale = this.options.locales[this.options.locale]; - if (!locale) { - if (!this.warned) { - console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); - this.warned = true; + var group = this.groupFromTarget(event); + if (group) { + newItem.group = group.groupId; } - locale = this.options.locales['en']; // fall back on english when not available - } - var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); - title = title.charAt(0).toUpperCase() + title.substring(1); - - this.bar.style.left = x + 'px'; - this.bar.title = title; - - return false; - }; - /** - * Remove the CustomTime from the DOM - */ - CustomTime.prototype.hide = function () { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); + // execute async handler to customize (or cancel) adding an item + this.options.onAdd(newItem, function (item) { + if (item) { + me.itemsData.getDataSet().add(item); + // TODO: need to trigger a redraw? + } + }); } }; /** - * Set custom time. - * @param {Date | number | string} time - */ - CustomTime.prototype.setCustomTime = function (time) { - this.customTime = util.convert(time, 'Date'); - this.redraw(); - }; - - /** - * Retrieve the current custom time. - * @return {Date} customTime - */ - CustomTime.prototype.getCustomTime = function () { - return new Date(this.customTime.valueOf()); - }; - - /** - * Start moving horizontally + * Handle selecting/deselecting multiple items when holding an item * @param {Event} event * @private */ - CustomTime.prototype._onDragStart = function (event) { - this.eventParams.dragging = true; - this.eventParams.customTime = this.customTime; + ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; - event.stopPropagation(); + var item = this.itemFromTarget(event); + + if (item) { + // multi select items (if allowed) + + var selection = this.options.multiselect ? this.getSelection() // take current selection + : []; // deselect current selection + + var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; + + if (shiftKey && this.options.multiselect) { + // select all items between the old selection and the tapped item + + // determine the selection range + selection.push(item.id); + var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + + // select all items within the selection range + selection = []; + for (var id in this.items) { + if (this.items.hasOwnProperty(id)) { + var _item = this.items[id]; + var start = _item.data.start; + var end = _item.data.end !== undefined ? _item.data.end : start; + + if (start >= range.min && end <= range.max && !(_item instanceof BackgroundItem)) { + selection.push(_item.id); // do not use id but item.id, id itself is stringified + } + } + } + } else { + // add/remove this item from the current selection + var index = selection.indexOf(item.id); + if (index == -1) { + // item is not yet selected -> select it + selection.push(item.id); + } else { + // item is already selected -> deselect it + selection.splice(index, 1); + } + } + + this.setSelection(selection); + + this.body.emitter.emit('select', { + items: this.getSelection(), + event: event + }); + } }; /** - * Perform moving operating. - * @param {Event} event + * Calculate the time range of a list of items + * @param {Array.} itemsData + * @return {{min: Date, max: Date}} Returns the range of the provided items * @private */ - CustomTime.prototype._onDrag = function (event) { - if (!this.eventParams.dragging) return; - - var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; - var time = this.body.util.toTime(x); + ItemSet._getItemRange = function (itemsData) { + var max = null; + var min = null; - this.setCustomTime(time); + itemsData.forEach(function (data) { + if (min == null || data.start < min) { + min = data.start; + } - // fire a timechange event - this.body.emitter.emit('timechange', { - id: this.options.id, - time: new Date(this.customTime.valueOf()) + if (data.end != undefined) { + if (max == null || data.end > max) { + max = data.end; + } + } else { + if (max == null || data.start > max) { + max = data.start; + } + } }); - event.stopPropagation(); + return { + min: min, + max: max + }; }; /** - * Stop moving operating. + * Find an item from an event target: + * searches for the attribute 'timeline-item' in the event target's element tree * @param {Event} event - * @private + * @return {Item | null} item */ - CustomTime.prototype._onDragEnd = function (event) { - if (!this.eventParams.dragging) return; + ItemSet.prototype.itemFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } - // fire a timechanged event - this.body.emitter.emit('timechanged', { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + return null; + }; - event.stopPropagation(); + /** + * Find the Group from an event target: + * searches for the attribute 'timeline-group' in the event target's element tree + * @param {Event} event + * @return {Group | null} group + */ + ItemSet.prototype.groupFromTarget = function (event) { + var clientY = event.center ? event.center.y : event.clientY; + for (var i = 0; i < this.groupIds.length; i++) { + var groupId = this.groupIds[i]; + var group = this.groups[groupId]; + var foreground = group.dom.foreground; + var top = util.getAbsoluteTop(foreground); + if (clientY > top && clientY < top + foreground.offsetHeight) { + return group; + } + + if (this.options.orientation.item === 'top') { + if (i === this.groupIds.length - 1 && clientY > top) { + return group; + } + } else { + if (i === 0 && clientY < top + foreground.offset) { + return group; + } + } + } + + return null; }; /** - * Find a custom time from an event target: - * searches for the attribute 'custom-time' in the event target's element tree + * Find the ItemSet from an event target: + * searches for the attribute 'timeline-itemset' in the event target's element tree * @param {Event} event - * @return {CustomTime | null} customTime + * @return {ItemSet | null} item */ - CustomTime.customTimeFromTarget = function (event) { + ItemSet.itemSetFromTarget = function (event) { var target = event.target; while (target) { - if (target.hasOwnProperty('custom-time')) { - return target['custom-time']; + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; } target = target.parentNode; } @@ -21710,5200 +20730,5476 @@ return /******/ (function(modules) { // webpackBootstrap return null; }; - module.exports = CustomTime; + module.exports = ItemSet; /***/ }, -/* 46 */ +/* 40 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var _ColorPicker = __webpack_require__(47); - - var _ColorPicker2 = _interopRequireDefault(_ColorPicker); - - var util = __webpack_require__(9); + var util = __webpack_require__(14); + var stack = __webpack_require__(41); + var RangeItem = __webpack_require__(42); /** - * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. - * Boolean options are recognised as Boolean - * Number options should be written as array: [default value, min value, max value, stepsize] - * Colors should be written as array: ['color', '#ffffff'] - * Strings with should be written as array: [option1, option2, option3, ..] - * - * The options are matched with their counterparts in each of the modules and the values used in the configuration are - * - * @param parentModule | the location where parentModule.setOptions() can be called - * @param defaultContainer | the default container of the module - * @param configureOptions | the fully configured and predefined options set found in allOptions.js - * @param pixelRatio | canvas pixel ratio + * @constructor Group + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ + function Group(groupId, data, itemSet) { + this.groupId = groupId; + this.subgroups = {}; + this.subgroupIndex = 0; + this.subgroupOrderer = data && data.subgroupOrder; + this.itemSet = itemSet; - var Configurator = (function () { - function Configurator(parentModule, defaultContainer, configureOptions) { - var pixelRatio = arguments[3] === undefined ? 1 : arguments[3]; - - _classCallCheck(this, Configurator); - - this.parent = parentModule; - this.changedOptions = []; - this.container = defaultContainer; - this.allowCreation = false; - - this.options = {}; - this.defaultOptions = { - enabled: false, - filter: true, - container: undefined, - showButton: true - }; - util.extend(this.options, this.defaultOptions); - - this.configureOptions = configureOptions; - this.moduleOptions = {}; - this.domElements = []; - this.colorPicker = new _ColorPicker2['default'](pixelRatio); - this.wrapper = undefined; - } - - _createClass(Configurator, [{ - key: 'setOptions', - - /** - * refresh all options. - * Because all modules parse their options by themselves, we just use their options. We copy them here. - * - * @param options - */ - value: function setOptions(options) { - if (options !== undefined) { - var enabled = true; - if (typeof options === 'string') { - this.options.filter = options; - } else if (options instanceof Array) { - this.options.filter = options.join(); - } else if (typeof options === 'object') { - if (options.container !== undefined) { - this.options.container = options.container; - } - if (options.filter !== undefined) { - this.options.filter = options.filter; - } - if (options.showButton !== undefined) { - this.options.showButton = options.showButton; - } - if (options.enabled !== undefined) { - enabled = options.enabled; - } - } else if (typeof options === 'boolean') { - this.options.filter = true; - enabled = options; - } else if (typeof options === 'function') { - this.options.filter = options; - enabled = true; - } - if (this.options.filter === false) { - enabled = false; - } - - this.options.enabled = enabled; - } - this._clean(); - } - }, { - key: 'setModuleOptions', - value: function setModuleOptions(moduleOptions) { - this.moduleOptions = moduleOptions; - if (this.options.enabled === true) { - this._clean(); - if (this.options.container !== undefined) { - this.container = this.options.container; - } - this._create(); - } + this.dom = {}; + this.props = { + label: { + width: 0, + height: 0 } - }, { - key: '_create', + }; + this.className = null; - /** - * Create all DOM elements - * @private - */ - value: function _create() { - var _this = this; + this.items = {}; // items filtered by groupId of this group + this.visibleItems = []; // items currently visible in window + this.orderedItems = { + byStart: [], + byEnd: [] + }; + this.checkRangedItems = false; // needed to refresh the ranged items if the window is programatically changed with NO overlap. + var me = this; + this.itemSet.body.emitter.on('checkRangedItems', function () { + me.checkRangedItems = true; + }); - this._clean(); - this.changedOptions = []; + this._create(); - var filter = this.options.filter; - var counter = 0; - var show = false; - for (var option in this.configureOptions) { - if (this.configureOptions.hasOwnProperty(option)) { - this.allowCreation = false; - show = false; - if (typeof filter === 'function') { - show = filter(option, []); - show = show || this._handleObject(this.configureOptions[option], [option], true); - } else if (filter === true || filter.indexOf(option) !== -1) { - show = true; - } + this.setData(data); + } - if (show !== false) { - this.allowCreation = true; + /** + * Create DOM elements for the group + * @private + */ + Group.prototype._create = function () { + var label = document.createElement('div'); + label.className = 'vis-label'; + this.dom.label = label; - // linebreak between categories - if (counter > 0) { - this._makeItem([]); - } - // a header for the category - this._makeHeader(option); + var inner = document.createElement('div'); + inner.className = 'vis-inner'; + label.appendChild(inner); + this.dom.inner = inner; - // get the suboptions - this._handleObject(this.configureOptions[option], [option]); - } - counter++; - } - } + var foreground = document.createElement('div'); + foreground.className = 'vis-group'; + foreground['timeline-group'] = this; + this.dom.foreground = foreground; - if (this.options.showButton === true) { - (function () { - var generateButton = document.createElement('div'); - generateButton.className = 'vis-network-configuration button'; - generateButton.innerHTML = 'generate options'; - generateButton.onclick = function () { - _this._printOptions(); - }; - generateButton.onmouseover = function () { - generateButton.className = 'vis-network-configuration button hover'; - }; - generateButton.onmouseout = function () { - generateButton.className = 'vis-network-configuration button'; - }; + this.dom.background = document.createElement('div'); + this.dom.background.className = 'vis-group'; - _this.optionsContainer = document.createElement('div'); - _this.optionsContainer.className = 'vis-network-configuration vis-option-container'; + this.dom.axis = document.createElement('div'); + this.dom.axis.className = 'vis-group'; - _this.domElements.push(_this.optionsContainer); - _this.domElements.push(generateButton); - })(); - } + // create a hidden marker to detect when the Timelines container is attached + // to the DOM, or the style of a parent of the Timeline is changed from + // display:none is changed to visible. + this.dom.marker = document.createElement('div'); + this.dom.marker.style.visibility = 'hidden'; + this.dom.marker.innerHTML = '?'; + this.dom.background.appendChild(this.dom.marker); + }; - this._push(); - this.colorPicker.insertTo(this.container); - } - }, { - key: '_push', + /** + * 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 { + this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null + } - /** - * draw all DOM elements on the screen - * @private - */ - value: function _push() { - this.wrapper = document.createElement('div'); - this.wrapper.className = 'vis-network-configuration-wrapper'; - this.container.appendChild(this.wrapper); - for (var i = 0; i < this.domElements.length; i++) { - this.wrapper.appendChild(this.domElements[i]); - } - } - }, { - key: '_clean', + // update title + this.dom.label.title = data && data.title || ''; - /** - * delete all DOM elements - * @private - */ - value: function _clean() { - for (var i = 0; i < this.domElements.length; i++) { - this.wrapper.removeChild(this.domElements[i]); - } + if (!this.dom.inner.firstChild) { + util.addClassName(this.dom.inner, 'vis-hidden'); + } else { + util.removeClassName(this.dom.inner, 'vis-hidden'); + } - if (this.wrapper !== undefined) { - this.container.removeChild(this.wrapper); - this.wrapper = undefined; - } - this.domElements = []; + // 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); } - }, { - key: '_getValue', + 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; + } - /** - * get the value from the actualOptions if it exists - * @param {array} path | where to look for the actual option - * @returns {*} - * @private - */ - value: function _getValue(path) { - var base = this.moduleOptions; - for (var i = 0; i < path.length; i++) { - if (base[path[i]] !== undefined) { - base = base[path[i]]; - } else { - base = undefined; - break; - } - } - return base; - } - }, { - key: '_makeItem', + // update style + if (this.style) { + util.removeCssText(this.dom.label, 'vis-' + this.style); + this.style = null; + } + if (data && data.style) { + util.addCssText(this.dom.label, 'vis-' + data.style); + this.style = data.style; + } + }; - /** - * all option elements are wrapped in an item - * @param path - * @param domElements - * @private - */ - value: function _makeItem(path) { - var _this2 = this; + /** + * Get the width of the group label + * @return {number} width + */ + Group.prototype.getLabelWidth = function () { + return this.props.label.width; + }; - for (var _len = arguments.length, domElements = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - domElements[_key - 1] = arguments[_key]; - } + /** + * 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 + */ + Group.prototype.redraw = function (range, margin, restack) { + var resized = false; - if (this.allowCreation === true) { - (function () { - var item = document.createElement('div'); - item.className = 'vis-network-configuration item s' + path.length; - domElements.forEach(function (element) { - item.appendChild(element); - }); - _this2.domElements.push(item); - })(); - } - } - }, { - key: '_makeHeader', + // 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; - /** - * header for major subjects - * @param name - * @private - */ - value: function _makeHeader(name) { - var div = document.createElement('div'); - div.className = 'vis-network-configuration header'; - div.innerHTML = name; - this._makeItem([], div); - } - }, { - key: '_makeLabel', + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); - /** - * make a label, if it is an object label, it gets different styling. - * @param name - * @param path - * @param objectLabel - * @returns {HTMLElement} - * @private - */ - value: function _makeLabel(name, path) { - var objectLabel = arguments[2] === undefined ? false : arguments[2]; + restack = true; + } - var div = document.createElement('div'); - div.className = 'vis-network-configuration label s' + path.length; - if (objectLabel === true) { - div.innerHTML = '' + name + ':'; - } else { - div.innerHTML = name + ':'; - } - return div; - } - }, { - key: '_makeDropdown', + // reposition visible items vertically + if (typeof this.itemSet.options.order === 'function') { + // a custom order function - /** - * make a dropdown list for multiple possible string optoins - * @param arr - * @param value - * @param path - * @private - */ - value: function _makeDropdown(arr, value, path) { - var select = document.createElement('select'); - select.className = 'vis-network-configuration select'; - var selectedValue = 0; - if (value !== undefined) { - if (arr.indexOf(value) !== -1) { - selectedValue = arr.indexOf(value); - } - } + if (restack) { + // brute force restack of all items - for (var i = 0; i < arr.length; i++) { - var option = document.createElement('option'); - option.value = arr[i]; - if (i === selectedValue) { - option.selected = 'selected'; + // show all items + var me = this; + var limitSize = false; + util.forEach(this.items, function (item) { + if (!item.displayed) { + item.redraw(); + me.visibleItems.push(item); } - option.innerHTML = arr[i]; - select.appendChild(option); - } + item.repositionX(limitSize); + }); - var me = this; - select.onchange = function () { - me._update(this.value, path); - }; + // order all items and force a restacking + var customOrderedItems = this.orderedItems.byStart.slice().sort(function (a, b) { + return me.itemSet.options.order(a.data, b.data); + }); + stack.stack(customOrderedItems, margin, true /* restack=true */); + } - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, select); + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + } else { + // no custom order function, lazy stacking + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + + 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); } - }, { - key: '_makeRange', + } - /** - * make a range object for numeric options - * @param arr - * @param value - * @param path - * @private - */ - value: function _makeRange(arr, value, path) { - var defaultValue = arr[0]; - var min = arr[1]; - var max = arr[2]; - var step = arr[3]; - var range = document.createElement('input'); - range.className = 'vis-network-configuration range'; - try { - range.type = 'range'; // not supported on IE9 - range.min = min; - range.max = max; - } catch (err) {} - range.step = step; + // recalculate the height of the group + var height = this._calculateHeight(margin); - if (value !== undefined) { - if (value < 0 && value * 2 < min) { - range.min = value * 2; - } else if (value * 0.1 < min) { - range.min = value / 10; - } - if (value * 2 > max && max !== 1) { - range.max = value * 2; - } - range.value = value; - } else { - range.value = defaultValue; - } + // 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; - var input = document.createElement('input'); - input.className = 'vis-network-configuration rangeinput'; - input.value = range.value; + // 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; - var me = this; - range.onchange = function () { - input.value = this.value;me._update(Number(this.value), path); - }; - range.oninput = function () { - input.value = this.value; - }; + // apply new height + this.dom.background.style.height = height + 'px'; + this.dom.foreground.style.height = height + 'px'; + this.dom.label.style.height = height + 'px'; - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, range, input); - } - }, { - key: '_makeCheckbox', + // 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); + } - /** - * make a checkbox for boolean options. - * @param defaultValue - * @param value - * @param path - * @private - */ - value: function _makeCheckbox(defaultValue, value, path) { - var checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.className = 'vis-network-configuration checkbox'; - checkbox.checked = defaultValue; - if (value !== undefined) { - checkbox.checked = value; - if (value !== defaultValue) { - if (typeof defaultValue === 'object') { - if (value !== defaultValue.enabled) { - this.changedOptions.push({ path: path, value: value }); - } - } else { - this.changedOptions.push({ path: path, value: value }); - } - } + return resized; + }; + + /** + * recalculate the height of the group + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @returns {number} Returns the height + * @private + */ + Group.prototype._calculateHeight = function (margin) { + // 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 > 0) { + 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 (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 = 0; + } + height = Math.max(height, this.props.label.height); - var me = this; - checkbox.onchange = function () { - me._update(this.checked, path); - }; + return height; + }; - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, checkbox); - } - }, { - key: '_makeTextInput', + /** + * Show this group: attach to the DOM + */ + Group.prototype.show = function () { + if (!this.dom.label.parentNode) { + this.itemSet.dom.labelSet.appendChild(this.dom.label); + } - /** - * make a text input field for string options. - * @param defaultValue - * @param value - * @param path - * @private - */ - value: function _makeTextInput(defaultValue, value, path) { - var checkbox = document.createElement('input'); - checkbox.type = 'text'; - checkbox.className = 'vis-network-configuration text'; - checkbox.value = value; - if (value !== defaultValue) { - this.changedOptions.push({ path: path, value: value }); - } + if (!this.dom.foreground.parentNode) { + this.itemSet.dom.foreground.appendChild(this.dom.foreground); + } - var me = this; - checkbox.onchange = function () { - me._update(this.value, path); - }; + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); + } - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, checkbox); - } - }, { - key: '_makeColorField', + if (!this.dom.axis.parentNode) { + this.itemSet.dom.axis.appendChild(this.dom.axis); + } + }; - /** - * make a color field with a color picker for color fields - * @param arr - * @param value - * @param path - * @private - */ - value: function _makeColorField(arr, value, path) { - var _this3 = this; + /** + * Hide this group: remove from the DOM + */ + Group.prototype.hide = function () { + var label = this.dom.label; + if (label.parentNode) { + label.parentNode.removeChild(label); + } - var defaultColor = arr[1]; - var div = document.createElement('div'); - value = value === undefined ? defaultColor : value; + var foreground = this.dom.foreground; + if (foreground.parentNode) { + foreground.parentNode.removeChild(foreground); + } - if (value !== 'none') { - div.className = 'vis-network-configuration colorBlock'; - div.style.backgroundColor = value; - } else { - div.className = 'vis-network-configuration colorBlock none'; - } + var background = this.dom.background; + if (background.parentNode) { + background.parentNode.removeChild(background); + } - value = value === undefined ? defaultColor : value; - div.onclick = function () { - _this3._showColorPicker(value, div, path); - }; + var axis = this.dom.axis; + if (axis.parentNode) { + axis.parentNode.removeChild(axis); + } + }; - var label = this._makeLabel(path[path.length - 1], path); - this._makeItem(path, label, div); + /** + * Add an item to the group + * @param {Item} item + */ + Group.prototype.add = function (item) { + this.items[item.id] = item; + item.setParent(this); + + // add to + if (item.data.subgroup !== undefined) { + if (this.subgroups[item.data.subgroup] === undefined) { + this.subgroups[item.data.subgroup] = { height: 0, visible: false, index: this.subgroupIndex, items: [] }; + this.subgroupIndex++; } - }, { - key: '_showColorPicker', + this.subgroups[item.data.subgroup].items.push(item); + } + this.orderSubgroups(); - /** - * used by the color buttons to call the color picker. - * @param event - * @param value - * @param div - * @param path - * @private - */ - value: function _showColorPicker(value, div, path) { - var _this4 = this; + 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); + } + }; - var rect = div.getBoundingClientRect(); - var bodyRect = document.body.getBoundingClientRect(); - var pickerX = rect.left + rect.width + 5; - var pickerY = rect.top - bodyRect.top + rect.height * 0.5; - this.colorPicker.show(pickerX, pickerY); - this.colorPicker.setColor(value); - this.colorPicker.setCallback(function (color) { - var colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')'; - div.style.backgroundColor = colorString; - _this4._update(colorString, path); + Group.prototype.orderSubgroups = function () { + if (this.subgroupOrderer !== undefined) { + var sortArray = []; + if (typeof this.subgroupOrderer == 'string') { + for (var subgroup in this.subgroups) { + sortArray.push({ subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer] }); + } + sortArray.sort(function (a, b) { + return a.sortField - b.sortField; }); + } else if (typeof this.subgroupOrderer == 'function') { + for (var subgroup in this.subgroups) { + sortArray.push(this.subgroups[subgroup].items[0].data); + } + sortArray.sort(this.subgroupOrderer); } - }, { - key: '_handleObject', - /** - * parse an object and draw the correct items - * @param obj - * @param path - * @private - */ - value: function _handleObject(obj) { - var path = arguments[1] === undefined ? [] : arguments[1]; - var checkOnly = arguments[2] === undefined ? false : arguments[2]; + if (sortArray.length > 0) { + for (var i = 0; i < sortArray.length; i++) { + this.subgroups[sortArray[i].subgroup].index = i; + } + } + } + }; - var show = false; - var filter = this.options.filter; - var visibleInSet = false; - for (var subObj in obj) { - if (obj.hasOwnProperty(subObj)) { - show = true; - var item = obj[subObj]; - var newPath = util.copyAndExtendArray(path, subObj); - if (typeof filter === 'function') { - show = filter(subObj, path); + Group.prototype.resetSubgroups = function () { + for (var subgroup in this.subgroups) { + if (this.subgroups.hasOwnProperty(subgroup)) { + this.subgroups[subgroup].visible = false; + } + } + }; - // if needed we must go deeper into the object. - if (show === false) { - if (!(item instanceof Array) && typeof item !== 'string' && typeof item !== 'boolean' && item instanceof Object) { - this.allowCreation = false; - show = this._handleObject(item, newPath, true); - this.allowCreation = checkOnly === false; - } - } - } + /** + * Remove an item from the group + * @param {Item} item + */ + Group.prototype.remove = function (item) { + delete this.items[item.id]; + item.setParent(null); - if (show !== false) { - visibleInSet = true; - var value = this._getValue(newPath); + // remove from visible items + var index = this.visibleItems.indexOf(item); + if (index != -1) this.visibleItems.splice(index, 1); - if (item instanceof Array) { - this._handleArray(item, value, newPath); - } else if (typeof item === 'string') { - this._makeTextInput(item, value, newPath); - } else if (typeof item === 'boolean') { - this._makeCheckbox(item, value, newPath); - } else if (item instanceof Object) { - // collapse the physics options that are not enabled - var draw = true; - if (path.indexOf('physics') !== -1) { - if (this.moduleOptions.physics.solver !== subObj) { - draw = false; - } - } + // TODO: also remove from ordered items? + }; - if (draw === true) { - // initially collapse options with an disabled enabled option. - if (item.enabled !== undefined) { - var enabledPath = util.copyAndExtendArray(newPath, 'enabled'); - var enabledValue = this._getValue(enabledPath); - if (enabledValue === true) { - var label = this._makeLabel(subObj, newPath, true); - this._makeItem(newPath, label); - visibleInSet = this._handleObject(item, newPath) || visibleInSet; - } else { - this._makeCheckbox(item, enabledValue, newPath); - } - } else { - var label = this._makeLabel(subObj, newPath, true); - this._makeItem(newPath, label); - visibleInSet = this._handleObject(item, newPath) || visibleInSet; - } - } - } else { - console.error('dont know how to handle', item, subObj, newPath); - } - } - } - } - return visibleInSet; + /** + * Remove an item from the corresponding DataSet + * @param {Item} item + */ + Group.prototype.removeFromDataSet = function (item) { + this.itemSet.removeItem(item.id); + }; + + /** + * Reorder the items + */ + Group.prototype.order = function () { + var array = util.toArray(this.items); + var startArray = []; + var endArray = []; + + for (var i = 0; i < array.length; i++) { + if (array[i].data.end !== undefined) { + endArray.push(array[i]); } - }, { - key: '_handleArray', + startArray.push(array[i]); + } + this.orderedItems = { + byStart: startArray, + byEnd: endArray + }; - /** - * handle the array type of option - * @param optionName - * @param arr - * @param value - * @param path - * @private - */ - value: function _handleArray(arr, value, path) { - if (typeof arr[0] === 'string' && arr[0] === 'color') { - this._makeColorField(arr, value, path); - if (arr[1] !== value) { - this.changedOptions.push({ path: path, value: value }); - } - } else if (typeof arr[0] === 'string') { - this._makeDropdown(arr, value, path); - if (arr[0] !== value) { - this.changedOptions.push({ path: path, value: value }); - } - } else if (typeof arr[0] === 'number') { - this._makeRange(arr, value, path); - if (arr[0] !== value) { - this.changedOptions.push({ path: path, value: Number(value) }); - } - } + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); + }; + + /** + * 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, oldVisibleItems, range) { + var visibleItems = []; + var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems + var interval = (range.end - range.start) / 4; + var lowerBound = range.start - interval; + var upperBound = range.end + interval; + var item, i; + + // this function is used to do the binary search. + var searchFunction = function searchFunction(value) { + if (value < lowerBound) { + return -1; + } else if (value <= upperBound) { + return 0; + } else { + return 1; } - }, { - key: '_update', + }; + + // first check if the items that were in view previously are still in view. + // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! + // also cleans up invisible items. + if (oldVisibleItems.length > 0) { + for (i = 0; i < oldVisibleItems.length; i++) { + this._checkIfVisibleWithReference(oldVisibleItems[i], visibleItems, visibleItemsLookup, range); + } + } + + // we do a binary search for the items that have only start values. + var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, 'data', 'start'); + + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. + this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { + return item.data.start < lowerBound || item.data.start > upperBound; + }); - /** - * called to update the network with the new settings. - * @param value - * @param path - * @private - */ - value: function _update(value, path) { - var options = this._constructOptions(value, path); - this.parent.setOptions(options); + // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. + // We therefore have to brute force check all items in the byEnd list + if (this.checkRangedItems == true) { + this.checkRangedItems = false; + for (i = 0; i < orderedItems.byEnd.length; i++) { + this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); } - }, { - key: '_constructOptions', - value: function _constructOptions(value, path) { - var optionsObj = arguments[2] === undefined ? {} : arguments[2]; + } else { + // we do a binary search for the items that have defined end times. + var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, 'data', 'end'); - var pointer = optionsObj; + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. + this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { + return item.data.end < lowerBound || item.data.end > upperBound; + }); + } - // when dropdown boxes can be string or boolean, we typecast it into correct types - value = value === 'true' ? true : value; - value = value === 'false' ? false : value; + // finally, we reposition all the visible items. + for (i = 0; i < visibleItems.length; i++) { + item = visibleItems[i]; + if (!item.displayed) item.show(); + // reposition item horizontally + item.repositionX(); + } - for (var i = 0; i < path.length; i++) { - if (path[i] !== 'global') { - if (pointer[path[i]] === undefined) { - pointer[path[i]] = {}; - } - if (i !== path.length - 1) { - pointer = pointer[path[i]]; - } else { - pointer[path[i]] = value; - } + // debug + //console.log("new line") + //if (this.groupId == null) { + // for (i = 0; i < orderedItems.byStart.length; i++) { + // item = orderedItems.byStart[i].data; + // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") + // } + // for (i = 0; i < orderedItems.byEnd.length; i++) { + // item = orderedItems.byEnd[i].data; + // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") + // } + //} + + return visibleItems; + }; + + Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { + var item; + var i; + + if (initialPos != -1) { + for (i = initialPos; i >= 0; i--) { + item = items[i]; + if (breakCondition(item)) { + break; + } else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); } } - return optionsObj; } - }, { - key: '_printOptions', - value: function _printOptions() { - var options = {}; - for (var i = 0; i < this.changedOptions.length; i++) { - this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options); + + for (i = initialPos + 1; i < items.length; i++) { + item = items[i]; + if (breakCondition(item)) { + break; + } else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } } - this.optionsContainer.innerHTML = '
var options = ' + JSON.stringify(options, null, 2) + '
'; } - }]); + } + }; - return Configurator; - })(); + /** + * 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(); + } + }; - exports['default'] = Configurator; - module.exports = exports['default']; + /** + * 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._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { + if (item.isVisible(range)) { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } + } else { + if (item.displayed) item.hide(); + } + }; + + module.exports = Group; /***/ }, -/* 47 */ +/* 41 */ /***/ function(module, exports, __webpack_require__) { + // Utility functions for ordering and stacking of items 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var Hammer = __webpack_require__(5); - var hammerUtil = __webpack_require__(31); - var util = __webpack_require__(9); + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors - var ColorPicker = (function () { - function ColorPicker() { - var pixelRatio = arguments[0] === undefined ? 1 : arguments[0]; + /** + * 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; + }); + }; - _classCallCheck(this, ColorPicker); + /** + * 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; - this.pixelRatio = pixelRatio; - this.generated = false; - this.centerCoordinates = { x: 289 / 2, y: 289 / 2 }; - this.r = 289 * 0.49; - this.color = { r: 255, g: 255, b: 255, a: 1 }; - this.hueCircle = undefined; - this.initialColor = { r: 255, g: 255, b: 255, a: 1 }; - this.previousColor = undefined; - this.applied = false; + return aTime - bTime; + }); + }; - // bound by - this.updateCallback = function () {}; + /** + * 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; - // create all DOM elements - this._create(); + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; + } } - _createClass(ColorPicker, [{ - key: 'insertTo', - - /** - * this inserts the colorPicker into a div from the DOM - * @param container - */ - value: function insertTo(container) { - if (this.hammer !== undefined) { - this.hammer.destroy(); - this.hammer = undefined; - } - this.container = container; - this.container.appendChild(this.frame); - this._bindHammer(); + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + if (item.stack && item.top === null) { + // initialize top position + item.top = margin.axis; - this._setSize(); - } - }, { - key: 'setCallback', + 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.stack && exports.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } - /** - * the callback is executed on apply and save. Bind it to the application - * @param callback - */ - value: function setCallback(callback) { - if (typeof callback === 'function') { - this.updateCallback = callback; - } else { - throw new Error('Function attempted to set as colorPicker callback is not a function.'); - } - } - }, { - key: '_isColorString', - value: function _isColorString(color) { - var htmlColors = { black: '#000000', navy: '#000080', darkblue: '#00008B', mediumblue: '#0000CD', blue: '#0000FF', darkgreen: '#006400', green: '#008000', teal: '#008080', darkcyan: '#008B8B', deepskyblue: '#00BFFF', darkturquoise: '#00CED1', mediumspringgreen: '#00FA9A', lime: '#00FF00', springgreen: '#00FF7F', aqua: '#00FFFF', cyan: '#00FFFF', midnightblue: '#191970', dodgerblue: '#1E90FF', lightseagreen: '#20B2AA', forestgreen: '#228B22', seagreen: '#2E8B57', darkslategray: '#2F4F4F', limegreen: '#32CD32', mediumseagreen: '#3CB371', turquoise: '#40E0D0', royalblue: '#4169E1', steelblue: '#4682B4', darkslateblue: '#483D8B', mediumturquoise: '#48D1CC', indigo: '#4B0082', darkolivegreen: '#556B2F', cadetblue: '#5F9EA0', cornflowerblue: '#6495ED', mediumaquamarine: '#66CDAA', dimgray: '#696969', slateblue: '#6A5ACD', olivedrab: '#6B8E23', slategray: '#708090', lightslategray: '#778899', mediumslateblue: '#7B68EE', lawngreen: '#7CFC00', chartreuse: '#7FFF00', aquamarine: '#7FFFD4', maroon: '#800000', purple: '#800080', olive: '#808000', gray: '#808080', skyblue: '#87CEEB', lightskyblue: '#87CEFA', blueviolet: '#8A2BE2', darkred: '#8B0000', darkmagenta: '#8B008B', saddlebrown: '#8B4513', darkseagreen: '#8FBC8F', lightgreen: '#90EE90', mediumpurple: '#9370D8', darkviolet: '#9400D3', palegreen: '#98FB98', darkorchid: '#9932CC', yellowgreen: '#9ACD32', sienna: '#A0522D', brown: '#A52A2A', darkgray: '#A9A9A9', lightblue: '#ADD8E6', greenyellow: '#ADFF2F', paleturquoise: '#AFEEEE', lightsteelblue: '#B0C4DE', powderblue: '#B0E0E6', firebrick: '#B22222', darkgoldenrod: '#B8860B', mediumorchid: '#BA55D3', rosybrown: '#BC8F8F', darkkhaki: '#BDB76B', silver: '#C0C0C0', mediumvioletred: '#C71585', indianred: '#CD5C5C', peru: '#CD853F', chocolate: '#D2691E', tan: '#D2B48C', lightgrey: '#D3D3D3', palevioletred: '#D87093', thistle: '#D8BFD8', orchid: '#DA70D6', goldenrod: '#DAA520', crimson: '#DC143C', gainsboro: '#DCDCDC', plum: '#DDA0DD', burlywood: '#DEB887', lightcyan: '#E0FFFF', lavender: '#E6E6FA', darksalmon: '#E9967A', violet: '#EE82EE', palegoldenrod: '#EEE8AA', lightcoral: '#F08080', khaki: '#F0E68C', aliceblue: '#F0F8FF', honeydew: '#F0FFF0', azure: '#F0FFFF', sandybrown: '#F4A460', wheat: '#F5DEB3', beige: '#F5F5DC', whitesmoke: '#F5F5F5', mintcream: '#F5FFFA', ghostwhite: '#F8F8FF', salmon: '#FA8072', antiquewhite: '#FAEBD7', linen: '#FAF0E6', lightgoldenrodyellow: '#FAFAD2', oldlace: '#FDF5E6', red: '#FF0000', fuchsia: '#FF00FF', magenta: '#FF00FF', deeppink: '#FF1493', orangered: '#FF4500', tomato: '#FF6347', hotpink: '#FF69B4', coral: '#FF7F50', darkorange: '#FF8C00', lightsalmon: '#FFA07A', orange: '#FFA500', lightpink: '#FFB6C1', pink: '#FFC0CB', gold: '#FFD700', peachpuff: '#FFDAB9', navajowhite: '#FFDEAD', moccasin: '#FFE4B5', bisque: '#FFE4C4', mistyrose: '#FFE4E1', blanchedalmond: '#FFEBCD', papayawhip: '#FFEFD5', lavenderblush: '#FFF0F5', seashell: '#FFF5EE', cornsilk: '#FFF8DC', lemonchiffon: '#FFFACD', floralwhite: '#FFFAF0', snow: '#FFFAFA', yellow: '#FFFF00', lightyellow: '#FFFFE0', ivory: '#FFFFF0', white: '#FFFFFF' }; - if (typeof color === 'string') { - return htmlColors[color]; - } + 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); } - }, { - key: 'setColor', - - /** - * Set the color of the colorPicker - * Supported formats: - * 'red' --> HTML color string - * '#ffffff' --> hex string - * 'rbg(255,255,255)' --> rgb string - * 'rgba(255,255,255,1.0)' --> rgba string - * {r:255,g:255,b:255} --> rgb object - * {r:255,g:255,b:255,a:1.0} --> rgba object - * @param color - * @param setInitial - */ - value: function setColor(color) { - var setInitial = arguments[1] === undefined ? true : arguments[1]; - - if (color === 'none') { - return; - } - - var rgba = undefined; + } + }; - // if a html color shorthand is used, convert to hex - var htmlColor = this._isColorString(color); - if (htmlColor !== undefined) { - color = htmlColor; - } + /** + * 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. + */ + exports.nostack = function (items, margin, subgroups) { + var i, iMax, newTop; - // check format - if (util.isString(color) === true) { - if (util.isValidRGB(color) === true) { - var rgbaArray = color.substr(4).substr(0, color.length - 5).split(','); - rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: 1 }; - } else if (util.isValidRGBA(color) === true) { - var rgbaArray = color.substr(5).substr(0, color.length - 6).split(','); - rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: rgbaArray[3] }; - } else if (util.isValidHex(color) === true) { - var rgbObj = util.hexToRGB(color); - rgba = { r: rgbObj.r, g: rgbObj.g, b: rgbObj.b, a: 1 }; - } - } else { - if (color instanceof Object) { - if (color.r !== undefined && color.g !== undefined && color.b !== undefined) { - var alpha = color.a !== undefined ? color.a : '1.0'; - rgba = { r: color.r, g: color.g, b: color.b, a: alpha }; + // 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 subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { + newTop += subgroups[subgroup].height + margin.item.vertical; } } } - - // set color - if (rgba === undefined) { - throw new Error('Unknown color passed to the colorPicker. Supported are strings: rgb, hex, rgba. Object: rgb ({r:r,g:g,b:b,[a:a]}). Supplied: ' + JSON.stringify(color)); - } else { - this._setColor(rgba, setInitial); - } - } - }, { - key: 'show', - - /** - * this shows the color picker at a location. The hue circle is constructed once and stored. - * @param x - * @param y - */ - value: function show(x, y) { - this.applied = false; - this.frame.style.display = 'block'; - this.frame.style.top = y + 'px'; - this.frame.style.left = x + 'px'; - this._generateHueCircle(); + items[i].top = newTop; + } else { + items[i].top = margin.axis; } - }, { - key: '_hide', - - // ------------------------------------------ PRIVATE ----------------------------- // + } + }; - /** - * Hide the picker. Is called by the cancel button. - * Optional boolean to store the previous color for easy access later on. - * @param storePrevious - * @private - */ - value: function _hide() { - var storePrevious = arguments[0] === undefined ? true : arguments[0]; + /** + * 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 + */ + 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; + }; - // store the previous color for next time; - if (storePrevious === true) { - this.previousColor = util.extend({}, this.color); - } +/***/ }, +/* 42 */ +/***/ function(module, exports, __webpack_require__) { - if (this.applied === true) { - this.updateCallback(this.initialColor); - } + 'use strict'; - this.frame.style.display = 'none'; - } - }, { - key: '_save', + var Hammer = __webpack_require__(10); + var Item = __webpack_require__(8); - /** - * bound to the save button. Saves and hides. - * @private - */ - value: function _save() { - this.updateCallback(this.color); - this.applied = false; - this._hide(); + /** + * @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 + */ + function RangeItem(data, conversion, options) { + this.props = { + content: { + width: 0 } - }, { - key: '_apply', + }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true - /** - * Bound to apply button. Saves but does not close. Is undone by the cancel button. - * @private - */ - value: function _apply() { - this.applied = true; - this.updateCallback(this.color); - this._updatePicker(this.color); + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data.id); } - }, { - key: '_loadLast', - - /** - * load the color from the previous session. - * @private - */ - value: function _loadLast() { - if (this.previousColor !== undefined) { - this.setColor(this.previousColor, false); - } else { - alert('There is no last color to load...'); - } + if (data.end == undefined) { + throw new Error('Property "end" missing in item ' + data.id); } - }, { - key: '_setColor', - - /** - * set the color, place the picker - * @param rgba - * @param setInitial - * @private - */ - value: function _setColor(rgba) { - var setInitial = arguments[1] === undefined ? true : arguments[1]; - - // store the initial color - if (setInitial === true) { - this.initialColor = util.extend({}, rgba); - } - - this.color = rgba; - var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); + } - var angleConvert = 2 * Math.PI; - var radius = this.r * hsv.s; - var x = this.centerCoordinates.x + radius * Math.sin(angleConvert * hsv.h); - var y = this.centerCoordinates.y + radius * Math.cos(angleConvert * hsv.h); + Item.call(this, data, conversion, options); + } - this.colorPickerSelector.style.left = x - 0.5 * this.colorPickerSelector.clientWidth + 'px'; - this.colorPickerSelector.style.top = y - 0.5 * this.colorPickerSelector.clientHeight + 'px'; + RangeItem.prototype = new Item(null, null, null); - this._updatePicker(rgba); - } - }, { - key: '_setOpacity', + RangeItem.prototype.baseClassName = 'vis-item vis-range'; - /** - * bound to opacity control - * @param value - * @private - */ - value: function _setOpacity(value) { - this.color.a = value / 100; - this._updatePicker(this.color); - } - }, { - key: '_setBrightness', + /** + * 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; + }; - /** - * bound to brightness control - * @param value - * @private - */ - value: function _setBrightness(value) { - var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); - hsv.v = value / 100; - var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); - rgba['a'] = this.color.a; - this.color = rgba; - this._updatePicker(); - } - }, { - key: '_updatePicker', + /** + * Repaint the item + */ + RangeItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - /** - * update the colorpicker. A black circle overlays the hue circle to mimic the brightness decreasing. - * @param rgba - * @private - */ - value: function _updatePicker() { - var rgba = arguments[0] === undefined ? this.color : arguments[0]; + // background box + dom.box = document.createElement('div'); + // className is updated in redraw() - var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); - var ctx = this.colorPickerCanvas.getContext('2d'); - if (this.pixelRation === undefined) { - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - } - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + // frame box (to prevent the item contents from overflowing + dom.frame = document.createElement('div'); + dom.frame.className = 'vis-item-overflow'; + dom.box.appendChild(dom.frame); - // clear the canvas - var w = this.colorPickerCanvas.clientWidth; - var h = this.colorPickerCanvas.clientHeight; - ctx.clearRect(0, 0, w, h); + // contents box + dom.content = document.createElement('div'); + dom.content.className = 'vis-item-content'; + dom.frame.appendChild(dom.content); - ctx.putImageData(this.hueCircle, 0, 0); - ctx.fillStyle = 'rgba(0,0,0,' + (1 - hsv.v) + ')'; - ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); - ctx.fill(); + // attach this item as attribute + dom.box['timeline-item'] = this; - this.brightnessRange.value = 100 * hsv.v; - this.opacityRange.value = 100 * rgba.a; + this.dirty = true; + } - this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; - this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; + // 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'); } - }, { - key: '_setSize', + foreground.appendChild(dom.box); + } + this.displayed = true; - /** - * used by create to set the size of the canvas. - * @private - */ - value: function _setSize() { - this.colorPickerCanvas.style.width = '100%'; - this.colorPickerCanvas.style.height = '100%'; + // 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); - this.colorPickerCanvas.width = 289 * this.pixelRatio; - this.colorPickerCanvas.height = 289 * this.pixelRatio; - } - }, { - key: '_create', + var editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; - /** - * create all dom elements - * TODO: cleanup, lots of similar dom elements - * @private - */ - value: function _create() { - this.frame = document.createElement('div'); - this.frame.className = 'vis-color-picker'; + // update class + var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); + dom.box.className = this.baseClassName + className; - this.colorPickerDiv = document.createElement('div'); - this.colorPickerSelector = document.createElement('div'); - this.colorPickerSelector.className = 'vis-selector'; - this.colorPickerDiv.appendChild(this.colorPickerSelector); + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.frame).overflow !== 'hidden'; - this.colorPickerCanvas = document.createElement('canvas'); - this.colorPickerDiv.appendChild(this.colorPickerCanvas); + // recalculate size + // turn off max-width to be able to calculate the real width + // this causes an extra browser repaint/reflow, but so be it + this.dom.content.style.maxWidth = 'none'; + this.props.content.width = this.dom.content.offsetWidth; + this.height = this.dom.box.offsetHeight; + this.dom.content.style.maxWidth = ''; - if (!this.colorPickerCanvas.getContext) { - var noCanvas = document.createElement('DIV'); - noCanvas.style.color = 'red'; - noCanvas.style.fontWeight = 'bold'; - noCanvas.style.padding = '10px'; - noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; - this.colorPickerCanvas.appendChild(noCanvas); - } else { - var ctx = this.colorPickerCanvas.getContext('2d'); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + this.dirty = false; + } - this.colorPickerCanvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - } + this._repaintDeleteButton(dom.box); + this._repaintDragLeft(); + this._repaintDragRight(); + }; - this.colorPickerDiv.className = 'vis-color'; + /** + * 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(); + } + }; - this.opacityDiv = document.createElement('div'); - this.opacityDiv.className = 'vis-opacity'; + /** + * Hide the item from the DOM (when visible) + * @return {Boolean} changed + */ + RangeItem.prototype.hide = function () { + if (this.displayed) { + var box = this.dom.box; - this.brightnessDiv = document.createElement('div'); - this.brightnessDiv.className = 'vis-brightness'; + if (box.parentNode) { + box.parentNode.removeChild(box); + } - this.arrowDiv = document.createElement('div'); - this.arrowDiv.className = 'vis-arrow'; + this.displayed = false; + } + }; - this.opacityRange = document.createElement('input'); - try { - this.opacityRange.type = 'range'; // Not supported on IE9 - this.opacityRange.min = '0'; - this.opacityRange.max = '100'; - } catch (err) {} - this.opacityRange.value = '100'; - this.opacityRange.className = 'vis-range'; + /** + * Reposition the item horizontally + * @param {boolean} [limitSize=true] If true (default), the width of the range + * item will be limited, as the browser cannot + * display very wide divs. This means though + * that the applied left and width may + * not correspond to the ranges start and end + * @Override + */ + RangeItem.prototype.repositionX = function (limitSize) { + 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; - this.brightnessRange = document.createElement('input'); - try { - this.brightnessRange.type = 'range'; // Not supported on IE9 - this.brightnessRange.min = '0'; - this.brightnessRange.max = '100'; - } catch (err) {} - this.brightnessRange.value = '100'; - this.brightnessRange.className = 'vis-range'; + // limit the width of the range, as browsers cannot draw very wide divs + if (limitSize === undefined || limitSize === true) { + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } + } + var boxWidth = Math.max(end - start, 1); - this.opacityDiv.appendChild(this.opacityRange); - this.brightnessDiv.appendChild(this.brightnessRange); + if (this.overflow) { + this.left = start; + this.width = boxWidth + this.props.content.width; + contentWidth = this.props.content.width; - var me = this; - this.opacityRange.onchange = function () { - me._setOpacity(this.value); - }; - this.opacityRange.oninput = function () { - me._setOpacity(this.value); - }; - this.brightnessRange.onchange = function () { - me._setBrightness(this.value); - }; - this.brightnessRange.oninput = function () { - me._setBrightness(this.value); - }; + // 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 { + this.left = start; + this.width = boxWidth; + contentWidth = Math.min(end - start, this.props.content.width); + } - this.brightnessLabel = document.createElement('div'); - this.brightnessLabel.className = 'vis-label vis-brightness'; - this.brightnessLabel.innerHTML = 'brightness:'; + this.dom.box.style.left = this.left + 'px'; + this.dom.box.style.width = boxWidth + 'px'; - this.opacityLabel = document.createElement('div'); - this.opacityLabel.className = 'vis-label vis-opacity'; - this.opacityLabel.innerHTML = 'opacity:'; + switch (this.options.align) { + case 'left': + this.dom.content.style.left = '0'; + break; - this.newColorDiv = document.createElement('div'); - this.newColorDiv.className = 'vis-new-color'; - this.newColorDiv.innerHTML = 'new'; + case 'right': + this.dom.content.style.left = Math.max(boxWidth - contentWidth, 0) + 'px'; + break; - this.initialColorDiv = document.createElement('div'); - this.initialColorDiv.className = 'vis-initial-color'; - this.initialColorDiv.innerHTML = 'initial'; + case 'center': + this.dom.content.style.left = Math.max((boxWidth - contentWidth) / 2, 0) + 'px'; + break; - this.cancelButton = document.createElement('div'); - this.cancelButton.className = 'vis-button vis-cancel'; - this.cancelButton.innerHTML = 'cancel'; - this.cancelButton.onclick = this._hide.bind(this, false); + default: + // 'auto' + // when range exceeds left of the window, position the contents at the left of the visible area + if (this.overflow) { + if (end > 0) { + contentLeft = Math.max(-start, 0); + } else { + contentLeft = -contentWidth; // ensure it's not visible anymore + } + } else { + if (start < 0) { + contentLeft = -start; + } else { + contentLeft = 0; + } + } + this.dom.content.style.left = contentLeft + 'px'; + } + }; - this.applyButton = document.createElement('div'); - this.applyButton.className = 'vis-button vis-apply'; - this.applyButton.innerHTML = 'apply'; - this.applyButton.onclick = this._apply.bind(this); + /** + * Reposition the item vertically + * @Override + */ + RangeItem.prototype.repositionY = function () { + var orientation = this.options.orientation.item; + var box = this.dom.box; - this.saveButton = document.createElement('div'); - this.saveButton.className = 'vis-button vis-save'; - this.saveButton.innerHTML = 'save'; - this.saveButton.onclick = this._save.bind(this); + if (orientation == 'top') { + box.style.top = this.top + 'px'; + } else { + box.style.top = this.parent.height - this.top - this.height + 'px'; + } + }; - this.loadButton = document.createElement('div'); - this.loadButton.className = 'vis-button vis-load'; - this.loadButton.innerHTML = 'load last'; - this.loadButton.onclick = this._loadLast.bind(this); + /** + * Repaint a drag area on the left side of the range when the range is selected + * @protected + */ + 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 = 'vis-drag-left'; + dragLeft.dragLeftItem = this; - this.frame.appendChild(this.colorPickerDiv); - this.frame.appendChild(this.arrowDiv); - this.frame.appendChild(this.brightnessLabel); - this.frame.appendChild(this.brightnessDiv); - this.frame.appendChild(this.opacityLabel); - this.frame.appendChild(this.opacityDiv); - this.frame.appendChild(this.newColorDiv); - this.frame.appendChild(this.initialColorDiv); + 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; + } + }; - this.frame.appendChild(this.cancelButton); - this.frame.appendChild(this.applyButton); - this.frame.appendChild(this.saveButton); - this.frame.appendChild(this.loadButton); + /** + * Repaint a drag area on the right side of the range when the range is selected + * @protected + */ + 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 = 'vis-drag-right'; + dragRight.dragRightItem = this; + + 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); } - }, { - key: '_bindHammer', + this.dom.dragRight = null; + } + }; - /** - * bind hammer to the color picker - * @private - */ - value: function _bindHammer() { - var _this = this; + module.exports = RangeItem; - this.drag = {}; - this.pinch = {}; - this.hammer = new Hammer(this.colorPickerCanvas); - this.hammer.get('pinch').set({ enable: true }); +/***/ }, +/* 43 */ +/***/ function(module, exports, __webpack_require__) { - hammerUtil.onTouch(this.hammer, function (event) { - _this._moveSelector(event); - }); - this.hammer.on('tap', function (event) { - _this._moveSelector(event); - }); - this.hammer.on('panstart', function (event) { - _this._moveSelector(event); - }); - this.hammer.on('panmove', function (event) { - _this._moveSelector(event); - }); - this.hammer.on('panend', function (event) { - _this._moveSelector(event); - }); - } - }, { - key: '_generateHueCircle', + 'use strict'; - /** - * generate the hue circle. This is relatively heavy (200ms) and is done only once on the first time it is shown. - * @private - */ - value: function _generateHueCircle() { - if (this.generated === false) { - var ctx = this.colorPickerCanvas.getContext('2d'); - if (this.pixelRation === undefined) { - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - } - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + var moment = __webpack_require__(15); + var DateUtil = __webpack_require__(37); + var util = __webpack_require__(14); - // clear the canvas - var w = this.colorPickerCanvas.clientWidth; - var h = this.colorPickerCanvas.clientHeight; - ctx.clearRect(0, 0, w, h); + /** + * @constructor TimeStep + * The class TimeStep is an iterator for dates. You provide a start date and an + * end date. The class itself determines the best scale (step size) based on the + * provided start Date, end Date, and minimumStep. + * + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * + * Alternatively, you can set a scale by hand. + * After creation, you can initialize the class by executing first(). Then you + * can iterate from the start date to the end date via next(). You can check if + * the end date is reached with the function hasNext(). After each step, you can + * retrieve the current date via getCurrent(). + * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, + * days, to years. + * + * Version: 1.2 + * + * @param {Date} [start] The start date, for example new Date(2010, 9, 21) + * or new Date(2010, 9, 21, 23, 45, 00) + * @param {Date} [end] The end date + * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds + */ + function TimeStep(start, end, minimumStep, hiddenDates) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); - // draw hue circle - var x = undefined, - y = undefined, - hue = undefined, - sat = undefined; - this.centerCoordinates = { x: w * 0.5, y: h * 0.5 }; - this.r = 0.49 * w; - var angleConvert = 2 * Math.PI / 360; - var hfac = 1 / 360; - var sfac = 1 / this.r; - var rgb = undefined; - for (hue = 0; hue < 360; hue++) { - for (sat = 0; sat < this.r; sat++) { - x = this.centerCoordinates.x + sat * Math.sin(angleConvert * hue); - y = this.centerCoordinates.y + sat * Math.cos(angleConvert * hue); - rgb = util.HSVToRGB(hue * hfac, sat * sfac, 1); - ctx.fillStyle = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'; - ctx.fillRect(x - 0.5, y - 0.5, 2, 2); - } - } - ctx.strokeStyle = 'rgba(0,0,0,1)'; - ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); - ctx.stroke(); + this.autoScale = true; + this.scale = 'day'; + this.step = 1; - this.hueCircle = ctx.getImageData(0, 0, w, h); - } - this.generated = true; - } - }, { - key: '_moveSelector', + // initialize the range + this.setRange(start, end, minimumStep); - /** - * move the selector. This is called by hammer functions. - * - * @param event - * @private - */ - value: function _moveSelector(event) { - var rect = this.colorPickerDiv.getBoundingClientRect(); - var left = event.center.x - rect.left; - var top = event.center.y - rect.top; + // hidden Dates options + this.switchedDay = false; + this.switchedMonth = false; + this.switchedYear = false; + this.hiddenDates = hiddenDates; + if (hiddenDates === undefined) { + this.hiddenDates = []; + } + + this.format = TimeStep.FORMAT; // default formatting + } + + // Time formatting + TimeStep.FORMAT = { + minorLabels: { + millisecond: 'SSS', + second: 's', + minute: 'HH:mm', + hour: 'HH:mm', + weekday: 'ddd D', + day: 'D', + month: 'MMM', + year: 'YYYY' + }, + majorLabels: { + millisecond: 'HH:mm:ss', + second: 'D MMMM HH:mm', + minute: 'ddd D MMMM', + hour: 'ddd D MMMM', + weekday: 'MMMM YYYY', + day: 'MMMM YYYY', + month: 'YYYY', + year: '' + } + }; - var centerY = 0.5 * this.colorPickerDiv.clientHeight; - var centerX = 0.5 * this.colorPickerDiv.clientWidth; + /** + * Set custom formatting for the minor an major labels of the TimeStep. + * Both `minorLabels` and `majorLabels` are an Object with properties: + * 'millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'. + * @param {{minorLabels: Object, majorLabels: Object}} format + */ + TimeStep.prototype.setFormat = function (format) { + var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); + this.format = util.deepExtend(defaultFormat, format); + }; - var x = left - centerX; - var y = top - centerY; + /** + * Set a new range + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * @param {Date} [start] The start date and time. + * @param {Date} [end] The end date and time. + * @param {int} [minimumStep] Optional. Minimum step size in milliseconds + */ + TimeStep.prototype.setRange = function (start, end, minimumStep) { + if (!(start instanceof Date) || !(end instanceof Date)) { + throw 'No legal start or end date in method setRange'; + } - var angle = Math.atan2(x, y); - var radius = 0.98 * Math.min(Math.sqrt(x * x + y * y), centerX); + this._start = start != undefined ? new Date(start.valueOf()) : new Date(); + this._end = end != undefined ? new Date(end.valueOf()) : new Date(); - var newTop = Math.cos(angle) * radius + centerY; - var newLeft = Math.sin(angle) * radius + centerX; + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } + }; - this.colorPickerSelector.style.top = newTop - 0.5 * this.colorPickerSelector.clientHeight + 'px'; - this.colorPickerSelector.style.left = newLeft - 0.5 * this.colorPickerSelector.clientWidth + 'px'; + /** + * Set the range iterator to the start date. + */ + TimeStep.prototype.first = function () { + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); + }; - // set color - var h = angle / (2 * Math.PI); - h = h < 0 ? h + 1 : h; - var s = radius / this.r; - var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); - hsv.h = h; - hsv.s = s; - var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); - rgba['a'] = this.color.a; - this.color = rgba; + /** + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date + */ + TimeStep.prototype.roundToMinor = function () { + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + // noinspection FallThroughInSwitchStatementJS + switch (this.scale) { + case 'year': + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case 'month': + this.current.setDate(1); + case 'day': // intentional fall through + case 'weekday': + this.current.setHours(0); + case 'hour': + this.current.setMinutes(0); + case 'minute': + this.current.setSeconds(0); + case 'second': + this.current.setMilliseconds(0); + //case 'millisecond': // nothing to do for milliseconds + } - // update previews - this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; - this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; + if (this.step != 1) { + // round down to the first minor value that is a multiple of the current step size + switch (this.scale) { + case 'millisecond': + this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);break; + case 'second': + this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);break; + case 'minute': + this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);break; + case 'hour': + this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);break; + case 'weekday': // intentional fall through + case 'day': + this.current.setDate(this.current.getDate() - 1 - (this.current.getDate() - 1) % this.step + 1);break; + case 'month': + this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);break; + case 'year': + this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step);break; + default: + break; } - }]); - - return ColorPicker; - })(); + } + }; - exports['default'] = ColorPicker; - module.exports = exports['default']; + /** + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date + */ + TimeStep.prototype.hasNext = function () { + return this.current.valueOf() <= this._end.valueOf(); + }; -/***/ }, -/* 48 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Do the next step + */ + TimeStep.prototype.next = function () { + var prev = this.current.valueOf(); - 'use strict'; + // Two cases, needed to prevent issues with switching daylight savings + // (end of March and end of October) + if (this.current.getMonth() < 6) { + switch (this.scale) { + case 'millisecond': - Object.defineProperty(exports, '__esModule', { - value: true - }); + this.current = new Date(this.current.valueOf() + this.step);break; + case 'second': + this.current = new Date(this.current.valueOf() + this.step * 1000);break; + case 'minute': + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60);break; + case 'hour': + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); + // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) + var h = this.current.getHours(); + this.current.setHours(h - h % this.step); + break; + case 'weekday': // intentional fall through + case 'day': + this.current.setDate(this.current.getDate() + this.step);break; + case 'month': + this.current.setMonth(this.current.getMonth() + this.step);break; + case 'year': + this.current.setFullYear(this.current.getFullYear() + this.step);break; + default: + break; + } + } else { + switch (this.scale) { + case 'millisecond': + this.current = new Date(this.current.valueOf() + this.step);break; + case 'second': + this.current.setSeconds(this.current.getSeconds() + this.step);break; + case 'minute': + this.current.setMinutes(this.current.getMinutes() + this.step);break; + case 'hour': + this.current.setHours(this.current.getHours() + this.step);break; + case 'weekday': // intentional fall through + case 'day': + this.current.setDate(this.current.getDate() + this.step);break; + case 'month': + this.current.setMonth(this.current.getMonth() + this.step);break; + case 'year': + this.current.setFullYear(this.current.getFullYear() + this.step);break; + default: + break; + } + } - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case 'millisecond': + if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; + case 'second': + if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; + case 'minute': + if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; + case 'hour': + if (this.current.getHours() < this.step) this.current.setHours(0);break; + case 'weekday': // intentional fall through + case 'day': + if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; + case 'month': + if (this.current.getMonth() < this.step) this.current.setMonth(0);break; + case 'year': + break; // nothing to do for year + default: + break; + } + } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); + } - var util = __webpack_require__(9); + DateUtil.stepOverHiddenDates(this, prev); + }; - var errorFound = false; - var allOptions = undefined; - var printStyle = 'background: #FFeeee; color: #dd0000'; /** - * Used to validate options. + * Get the current datetime + * @return {Date} current The current date */ + TimeStep.prototype.getCurrent = function () { + return this.current; + }; - var Validator = (function () { - function Validator() { - _classCallCheck(this, Validator); + /** + * Set a custom scale. Autoscaling will be disabled. + * For example setScale('minute', 5) will result + * in minor steps of 5 minutes, and major steps of an hour. + * + * @param {{scale: string, step: number}} params + * An object containing two properties: + * - A string 'scale'. Choose from 'millisecond', 'second', + * 'minute', 'hour', 'weekday', 'day', 'month', 'year'. + * - A number 'step'. A step size, by default 1. + * Choose for example 1, 2, 5, or 10. + */ + TimeStep.prototype.setScale = function (params) { + if (params && typeof params.scale == 'string') { + this.scale = params.scale; + this.step = params.step > 0 ? params.step : 1; + this.autoScale = false; } + }; - _createClass(Validator, null, [{ - key: 'validate', - - /** - * Main function to be called - * @param options - * @param subObject - * @returns {boolean} - */ - value: function validate(options, referenceOptions, subObject) { - errorFound = false; - allOptions = referenceOptions; - var usedOptions = referenceOptions; - if (subObject !== undefined) { - usedOptions = referenceOptions[subObject]; - } - Validator.parse(options, usedOptions, []); - return errorFound; - } - }, { - key: 'parse', + /** + * Enable or disable autoscaling + * @param {boolean} enable If true, autoascaling is set true + */ + TimeStep.prototype.setAutoScale = function (enable) { + this.autoScale = enable; + }; - /** - * Will traverse an object recursively and check every value - * @param options - * @param referenceOptions - * @param path - */ - value: function parse(options, referenceOptions, path) { - for (var option in options) { - if (options.hasOwnProperty(option)) { - Validator.check(option, options, referenceOptions, path); - } - } - } - }, { - key: 'check', + /** + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds + */ + TimeStep.prototype.setMinimumStep = function (minimumStep) { + if (minimumStep == undefined) { + return; + } - /** - * Check every value. If the value is an object, call the parse function on that object. - * @param option - * @param options - * @param referenceOptions - * @param path - */ - value: function check(option, options, referenceOptions, path) { - if (referenceOptions[option] === undefined && referenceOptions.__any__ === undefined) { - Validator.getSuggestion(option, referenceOptions, path); - } else if (referenceOptions[option] === undefined && referenceOptions.__any__ !== undefined) { - // __any__ is a wildcard. Any value is accepted and will be further analysed by reference. - if (Validator.getType(options[option]) === 'object' && referenceOptions['__any__'].__type__ !== undefined) { - // if the any subgroup is not a predefined object int he configurator we do not look deeper into the object. - Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'].__type__, path); - } else { - Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'], path); - } - } else { - // Since all options in the reference are objects, we can check whether they are supposed to be object to look for the __type__ field. - if (referenceOptions[option].__type__ !== undefined) { - // if this should be an object, we check if the correct type has been supplied to account for shorthand options. - Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option].__type__, path); - } else { - Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option], path); - } - } - } - }, { - key: 'checkFields', + //var b = asc + ds; - /** - * - * @param {String} option | the option property - * @param {Object} options | The supplied options object - * @param {Object} referenceOptions | The reference options containing all options and their allowed formats - * @param {String} referenceOption | Usually this is the same as option, except when handling an __any__ tag. - * @param {String} refOptionType | This is the type object from the reference options - * @param {Array} path | where in the object is the option - */ - value: function checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path) { - var optionType = Validator.getType(options[option]); - var refOptionType = refOptionObj[optionType]; - if (refOptionType !== undefined) { - // if the type is correct, we check if it is supposed to be one of a few select values - if (Validator.getType(refOptionType) === 'array') { - if (refOptionType.indexOf(options[option]) === -1) { - console.log('%cInvalid option detected in "' + option + '".' + ' Allowed values are:' + Validator.print(refOptionType) + ' not "' + options[option] + '". ' + Validator.printLocation(path, option), printStyle); - errorFound = true; - } else if (optionType === 'object' && referenceOption !== '__any__') { - path = util.copyAndExtendArray(path, option); - Validator.parse(options[option], referenceOptions[referenceOption], path); - } - } else if (optionType === 'object' && referenceOption !== '__any__') { - path = util.copyAndExtendArray(path, option); - Validator.parse(options[option], referenceOptions[referenceOption], path); - } - } else if (refOptionObj['any'] === undefined) { - // type of the field is incorrect and the field cannot be any - console.log('%cInvalid type received for "' + option + '". Expected: ' + Validator.print(Object.keys(refOptionObj)) + '. Received [' + optionType + '] "' + options[option] + '"' + Validator.printLocation(path, option), printStyle); - errorFound = true; - } - } - }, { - key: 'getType', - value: function getType(object) { - var type = typeof object; + var stepYear = 1000 * 60 * 60 * 24 * 30 * 12; + var stepMonth = 1000 * 60 * 60 * 24 * 30; + var stepDay = 1000 * 60 * 60 * 24; + var stepHour = 1000 * 60 * 60; + var stepMinute = 1000 * 60; + var stepSecond = 1000; + var stepMillisecond = 1; - if (type === 'object') { - if (object === null) { - return 'null'; - } - if (object instanceof Boolean) { - return 'boolean'; - } - if (object instanceof Number) { - return 'number'; - } - if (object instanceof String) { - return 'string'; - } - if (Array.isArray(object)) { - return 'array'; - } - if (object instanceof Date) { - return 'date'; - } - if (object.nodeType !== undefined) { - return 'dom'; - } - if (object._isAMomentObject === true) { - return 'moment'; - } - return 'object'; - } else if (type === 'number') { - return 'number'; - } else if (type === 'boolean') { - return 'boolean'; - } else if (type === 'string') { - return 'string'; - } else if (type === undefined) { - return 'undefined'; - } - return type; - } - }, { - key: 'getSuggestion', - value: function getSuggestion(option, options, path) { - var localSearch = Validator.findInOptions(option, options, path, false); - var globalSearch = Validator.findInOptions(option, allOptions, [], true); + // find the smallest step that is larger than the provided minimumStep + if (stepYear * 1000 > minimumStep) { + this.scale = 'year';this.step = 1000; + } + if (stepYear * 500 > minimumStep) { + this.scale = 'year';this.step = 500; + } + if (stepYear * 100 > minimumStep) { + this.scale = 'year';this.step = 100; + } + if (stepYear * 50 > minimumStep) { + this.scale = 'year';this.step = 50; + } + if (stepYear * 10 > minimumStep) { + this.scale = 'year';this.step = 10; + } + if (stepYear * 5 > minimumStep) { + this.scale = 'year';this.step = 5; + } + if (stepYear > minimumStep) { + this.scale = 'year';this.step = 1; + } + if (stepMonth * 3 > minimumStep) { + this.scale = 'month';this.step = 3; + } + if (stepMonth > minimumStep) { + this.scale = 'month';this.step = 1; + } + if (stepDay * 5 > minimumStep) { + this.scale = 'day';this.step = 5; + } + if (stepDay * 2 > minimumStep) { + this.scale = 'day';this.step = 2; + } + if (stepDay > minimumStep) { + this.scale = 'day';this.step = 1; + } + if (stepDay / 2 > minimumStep) { + this.scale = 'weekday';this.step = 1; + } + if (stepHour * 4 > minimumStep) { + this.scale = 'hour';this.step = 4; + } + if (stepHour > minimumStep) { + this.scale = 'hour';this.step = 1; + } + if (stepMinute * 15 > minimumStep) { + this.scale = 'minute';this.step = 15; + } + if (stepMinute * 10 > minimumStep) { + this.scale = 'minute';this.step = 10; + } + if (stepMinute * 5 > minimumStep) { + this.scale = 'minute';this.step = 5; + } + if (stepMinute > minimumStep) { + this.scale = 'minute';this.step = 1; + } + if (stepSecond * 15 > minimumStep) { + this.scale = 'second';this.step = 15; + } + if (stepSecond * 10 > minimumStep) { + this.scale = 'second';this.step = 10; + } + if (stepSecond * 5 > minimumStep) { + this.scale = 'second';this.step = 5; + } + if (stepSecond > minimumStep) { + this.scale = 'second';this.step = 1; + } + if (stepMillisecond * 200 > minimumStep) { + this.scale = 'millisecond';this.step = 200; + } + if (stepMillisecond * 100 > minimumStep) { + this.scale = 'millisecond';this.step = 100; + } + if (stepMillisecond * 50 > minimumStep) { + this.scale = 'millisecond';this.step = 50; + } + if (stepMillisecond * 10 > minimumStep) { + this.scale = 'millisecond';this.step = 10; + } + if (stepMillisecond * 5 > minimumStep) { + this.scale = 'millisecond';this.step = 5; + } + if (stepMillisecond > minimumStep) { + this.scale = 'millisecond';this.step = 1; + } + }; - var localSearchThreshold = 8; - var globalSearchThreshold = 4; + /** + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * Static function + * @param {Date} date the date to be snapped. + * @param {string} scale Current scale, can be 'millisecond', 'second', + * 'minute', 'hour', 'weekday, 'day', 'month', 'year'. + * @param {number} step Current step (1, 2, 4, 5, ... + * @return {Date} snappedDate + */ + TimeStep.snap = function (date, scale, step) { + var clone = new Date(date.valueOf()); - if (localSearch.indexMatch !== undefined) { - console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was incomplete? Did you mean: "' + localSearch.indexMatch + '"?\n\n', printStyle); - } else if (globalSearch.distance <= globalSearchThreshold && localSearch.distance > globalSearch.distance) { - console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was misplaced? Matching option found at: ' + Validator.printLocation(globalSearch.path, globalSearch.closestMatch, ''), printStyle); - } else if (localSearch.distance <= localSearchThreshold) { - console.log('%cUnknown option detected: "' + option + '". Did you mean "' + localSearch.closestMatch + '"?' + Validator.printLocation(localSearch.path, option), printStyle); - } else { - console.log('%cUnknown option detected: "' + option + '". Did you mean one of these: ' + Validator.print(Object.keys(options)) + Validator.printLocation(path, option), printStyle); - } + if (scale == 'year') { + var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); + clone.setFullYear(Math.round(year / step) * step); + clone.setMonth(0); + clone.setDate(0); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == 'month') { + if (clone.getDate() > 15) { + clone.setDate(1); + clone.setMonth(clone.getMonth() + 1); + // important: first set Date to 1, after that change the month. + } else { + clone.setDate(1); + } - errorFound = true; + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == 'day') { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 24) * 24);break; + default: + clone.setHours(Math.round(clone.getHours() / 12) * 12);break; } - }, { - key: 'findInOptions', + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == 'weekday') { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 12) * 12);break; + default: + clone.setHours(Math.round(clone.getHours() / 6) * 6);break; + } + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == 'hour') { + switch (step) { + case 4: + clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60);break; + default: + clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30);break; + } + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == 'minute') { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 15: + case 10: + clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); + clone.setSeconds(0); + break; + case 5: + clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60);break; + default: + clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30);break; + } + clone.setMilliseconds(0); + } else if (scale == 'second') { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 15: + case 10: + clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); + clone.setMilliseconds(0); + break; + case 5: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000);break; + default: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500);break; + } + } else if (scale == 'millisecond') { + var _step = step > 5 ? step / 2 : 1; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); + } - /** - * traverse the options in search for a match. - * @param option - * @param options - * @param path - * @param recursive - * @returns {{closestMatch: string, path: Array, distance: number}} - */ - value: function findInOptions(option, options, path) { - var recursive = arguments[3] === undefined ? false : arguments[3]; + return clone; + }; - var min = 1000000000; - var closestMatch = ''; - var closestMatchPath = []; - var lowerCaseOption = option.toLowerCase(); - var indexMatch = undefined; - for (var op in options) { - var distance = undefined; - if (options[op].__type__ !== undefined && recursive === true) { - var result = Validator.findInOptions(option, options[op], util.copyAndExtendArray(path, op)); - if (min > result.distance) { - closestMatch = result.closestMatch; - closestMatchPath = result.path; - min = result.distance; - indexMatch = result.indexMatch; - } - } else { - if (op.toLowerCase().indexOf(lowerCaseOption) !== -1) { - indexMatch = op; - } - distance = Validator.levenshteinDistance(option, op); - if (min > distance) { - closestMatch = op; - closestMatchPath = util.copyArray(path); - min = distance; - } - } - } - return { closestMatch: closestMatch, path: closestMatchPath, distance: min, indexMatch: indexMatch }; + /** + * Check if the current value is a major value (for example when the step + * is DAY, a major value is each first day of the MONTH) + * @return {boolean} true if current date is major, else false. + */ + TimeStep.prototype.isMajor = function () { + if (this.switchedYear == true) { + this.switchedYear = false; + switch (this.scale) { + case 'year': + case 'month': + case 'weekday': + case 'day': + case 'hour': + case 'minute': + case 'second': + case 'millisecond': + return true; + default: + return false; } - }, { - key: 'printLocation', - value: function printLocation(path, option) { - var prefix = arguments[2] === undefined ? 'Problem value found at: \n' : arguments[2]; - - var str = '\n\n' + prefix + 'options = {\n'; - for (var i = 0; i < path.length; i++) { - for (var j = 0; j < i + 1; j++) { - str += ' '; - } - str += path[i] + ': {\n'; - } - for (var j = 0; j < path.length + 1; j++) { - str += ' '; - } - str += option + '\n'; - for (var i = 0; i < path.length + 1; i++) { - for (var j = 0; j < path.length - i; j++) { - str += ' '; - } - str += '}\n'; - } - return str + '\n\n'; + } else if (this.switchedMonth == true) { + this.switchedMonth = false; + switch (this.scale) { + case 'weekday': + case 'day': + case 'hour': + case 'minute': + case 'second': + case 'millisecond': + return true; + default: + return false; } - }, { - key: 'print', - value: function print(options) { - return JSON.stringify(options).replace(/(\")|(\[)|(\])|(,"__type__")/g, '').replace(/(\,)/g, ', '); + } else if (this.switchedDay == true) { + this.switchedDay = false; + switch (this.scale) { + case 'millisecond': + case 'second': + case 'minute': + case 'hour': + return true; + default: + return false; } - }, { - key: 'levenshteinDistance', - - // Compute the edit distance between the two given strings - // http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript - /* - Copyright (c) 2011 Andrei Mackenzie - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - value: function levenshteinDistance(a, b) { - if (a.length === 0) return b.length; - if (b.length === 0) return a.length; + } - var matrix = []; + switch (this.scale) { + case 'millisecond': + return this.current.getMilliseconds() == 0; + case 'second': + return this.current.getSeconds() == 0; + case 'minute': + return this.current.getHours() == 0 && this.current.getMinutes() == 0; + case 'hour': + return this.current.getHours() == 0; + case 'weekday': // intentional fall through + case 'day': + return this.current.getDate() == 1; + case 'month': + return this.current.getMonth() == 0; + case 'year': + return false; + default: + return false; + } + }; - // increment along the first column of each row - var i; - for (i = 0; i <= b.length; i++) { - matrix[i] = [i]; - } + /** + * Returns formatted text for the minor axislabel, depending on the current + * date and the scale. For example when scale is MINUTE, the current time is + * formatted as "hh:mm". + * @param {Date} [date] custom date. if not provided, current date is taken + */ + TimeStep.prototype.getLabelMinor = function (date) { + if (date == undefined) { + date = this.current; + } - // increment each column in the first row - var j; - for (j = 0; j <= a.length; j++) { - matrix[0][j] = j; - } + var format = this.format.minorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ''; + }; - // Fill in the rest of the matrix - for (i = 1; i <= b.length; i++) { - for (j = 1; j <= a.length; j++) { - if (b.charAt(i - 1) == a.charAt(j - 1)) { - matrix[i][j] = matrix[i - 1][j - 1]; - } else { - matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution - Math.min(matrix[i][j - 1] + 1, // insertion - matrix[i - 1][j] + 1)); // deletion - } - } - } + /** + * Returns formatted text for the major axis label, depending on the current + * date and the scale. For example when scale is MINUTE, the major scale is + * hours, and the hour will be formatted as "hh". + * @param {Date} [date] custom date. if not provided, current date is taken + */ + TimeStep.prototype.getLabelMajor = function (date) { + if (date == undefined) { + date = this.current; + } - return matrix[b.length][a.length]; - } - }]); + var format = this.format.majorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ''; + }; - return Validator; - })(); + TimeStep.prototype.getClassName = function () { + var m = moment(this.current); + var date = m.locale ? m.locale('en') : m.lang('en'); // old versions of moment have .lang() function + var step = this.step; - exports['default'] = Validator; - exports.printStyle = printStyle; + function even(value) { + return value / step % 2 == 0 ? ' vis-even' : ' vis-odd'; + } -/***/ }, -/* 49 */ -/***/ function(module, exports, __webpack_require__) { + function today(date) { + if (date.isSame(new Date(), 'day')) { + return ' vis-today'; + } + if (date.isSame(moment().add(1, 'day'), 'day')) { + return ' vis-tomorrow'; + } + if (date.isSame(moment().add(-1, 'day'), 'day')) { + return ' vis-yesterday'; + } + return ''; + } - /** - * This object contains all possible options. It will check if the types are correct, if required if the option is one - * of the allowed values. - * - * __any__ means that the name of the property does not matter. - * __type__ is a required field for all objects and contains the allowed types of all objects - */ - 'use strict'; + function currentWeek(date) { + return date.isSame(new Date(), 'week') ? ' vis-current-week' : ''; + } - Object.defineProperty(exports, '__esModule', { - value: true - }); - var string = 'string'; - var boolean = 'boolean'; - var number = 'number'; - var array = 'array'; - var date = 'date'; - var object = 'object'; // should only be in a __type__ property - var dom = 'dom'; - var moment = 'moment'; - var any = 'any'; + function currentMonth(date) { + return date.isSame(new Date(), 'month') ? ' vis-current-month' : ''; + } - var allOptions = { - configure: { - enabled: { boolean: boolean }, - filter: { boolean: boolean, 'function': 'function' }, - container: { dom: dom }, - __type__: { object: object, boolean: boolean, 'function': 'function' } - }, + function currentYear(date) { + return date.isSame(new Date(), 'year') ? ' vis-current-year' : ''; + } - //globals : - align: { string: string }, - autoResize: { boolean: boolean }, - clickToUse: { boolean: boolean }, - dataAttributes: { string: string, array: array }, - editable: { - add: { boolean: boolean, 'undefined': 'undefined' }, - remove: { boolean: boolean, 'undefined': 'undefined' }, - updateGroup: { boolean: boolean, 'undefined': 'undefined' }, - updateTime: { boolean: boolean, 'undefined': 'undefined' }, - __type__: { boolean: boolean, object: object } - }, - end: { number: number, date: date, string: string, moment: moment }, - format: { - minorLabels: { - millisecond: { string: string, 'undefined': 'undefined' }, - second: { string: string, 'undefined': 'undefined' }, - minute: { string: string, 'undefined': 'undefined' }, - hour: { string: string, 'undefined': 'undefined' }, - weekday: { string: string, 'undefined': 'undefined' }, - day: { string: string, 'undefined': 'undefined' }, - month: { string: string, 'undefined': 'undefined' }, - year: { string: string, 'undefined': 'undefined' }, - __type__: { object: object } - }, - majorLabels: { - millisecond: { string: string, 'undefined': 'undefined' }, - second: { string: string, 'undefined': 'undefined' }, - minute: { string: string, 'undefined': 'undefined' }, - hour: { string: string, 'undefined': 'undefined' }, - weekday: { string: string, 'undefined': 'undefined' }, - day: { string: string, 'undefined': 'undefined' }, - month: { string: string, 'undefined': 'undefined' }, - year: { string: string, 'undefined': 'undefined' }, - __type__: { object: object } - }, - __type__: { object: object } - }, - groupOrder: { string: string, 'function': 'function' }, - height: { string: string, number: number }, - hiddenDates: { object: object, array: array }, - locale: { string: string }, - locales: { - __any__: { any: any }, - __type__: { object: object } - }, - margin: { - axis: { number: number }, - item: { - horizontal: { number: number, 'undefined': 'undefined' }, - vertical: { number: number, 'undefined': 'undefined' }, - __type__: { object: object, number: number } - }, - __type__: { object: object, number: number } - }, - max: { date: date, number: number, string: string, moment: moment }, - maxHeight: { number: number, string: string }, - min: { date: date, number: number, string: string, moment: moment }, - minHeight: { number: number, string: string }, - moveable: { boolean: boolean }, - multiselect: { boolean: boolean }, - onAdd: { 'function': 'function' }, - onUpdate: { 'function': 'function' }, - onMove: { 'function': 'function' }, - onMoving: { 'function': 'function' }, - onRemove: { 'function': 'function' }, - order: { 'function': 'function' }, - orientation: { - axis: { string: string, 'undefined': 'undefined' }, - item: { string: string, 'undefined': 'undefined' }, - __type__: { string: string, object: object } - }, - selectable: { boolean: boolean }, - showCurrentTime: { boolean: boolean }, - showMajorLabels: { boolean: boolean }, - showMinorLabels: { boolean: boolean }, - stack: { boolean: boolean }, - snap: { 'function': 'function', 'null': 'null' }, - start: { date: date, number: number, string: string, moment: moment }, - template: { 'function': 'function' }, - timeAxis: { - scale: { string: string, 'undefined': 'undefined' }, - step: { number: number, 'undefined': 'undefined' }, - __type__: { object: object } - }, - type: { string: string }, - width: { string: string, number: number }, - zoomable: { boolean: boolean }, - zoomMax: { number: number }, - zoomMin: { number: number }, + switch (this.scale) { + case 'millisecond': + return even(date.milliseconds()).trim(); - __type__: { object: object } - }; + case 'second': + return even(date.seconds()).trim(); - var configureOptions = { - global: { - align: ['center', 'left', 'right'], - autoResize: true, - clickToUse: false, - // dataAttributes: ['all'], // FIXME: can be 'all' or string[] - editable: { - add: false, - remove: false, - updateGroup: false, - updateTime: false - }, - end: '', - format: { - minorLabels: { - millisecond: 'SSS', - second: 's', - minute: 'HH:mm', - hour: 'HH:mm', - weekday: 'ddd D', - day: 'D', - month: 'MMM', - year: 'YYYY' - }, - majorLabels: { - millisecond: 'HH:mm:ss', - second: 'D MMMM HH:mm', - minute: 'ddd D MMMM', - hour: 'ddd D MMMM', - weekday: 'MMMM YYYY', - day: 'MMMM YYYY', - month: 'YYYY', - year: '' - } - }, + case 'minute': + return even(date.minutes()).trim(); - //groupOrder: {string, 'function': 'function'}, - height: '', - //hiddenDates: {object, array}, - locale: '', - margin: { - axis: [20, 0, 100, 1], - item: { - horizontal: [10, 0, 100, 1], - vertical: [10, 0, 100, 1] + case 'hour': + var hours = date.hours(); + if (this.step == 4) { + hours = hours + '-h' + (hours + 4); } - }, - max: '', - maxHeight: '', - min: '', - minHeight: '', - moveable: false, - multiselect: false, - //onAdd: {'function': 'function'}, - //onUpdate: {'function': 'function'}, - //onMove: {'function': 'function'}, - //onMoving: {'function': 'function'}, - //onRename: {'function': 'function'}, - //order: {'function': 'function'}, - orientation: { - axis: ['both', 'bottom', 'top'], - item: ['bottom', 'top'] - }, - selectable: true, - showCurrentTime: false, - showMajorLabels: true, - showMinorLabels: true, - stack: true, - //snap: {'function': 'function', nada}, - start: '', - //template: {'function': 'function'}, - //timeAxis: { - // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'], - // step: [1, 1, 10, 1] - //}, - type: ['box', 'point', 'range', 'background'], - width: '100%', - zoomable: true, - zoomMax: [315360000000000, 10, 315360000000000, 1], - zoomMin: [10, 10, 315360000000000, 1] + return 'vis-h' + hours + today(date) + even(date.hours()); + + case 'weekday': + return 'vis-' + date.format('dddd').toLowerCase() + today(date) + currentWeek(date) + even(date.date()); + + case 'day': + var day = date.date(); + var month = date.format('MMMM').toLowerCase(); + return 'vis-day' + day + ' vis-' + month + currentMonth(date) + even(day - 1); + + case 'month': + return 'vis-' + date.format('MMMM').toLowerCase() + currentMonth(date) + even(date.month()); + + case 'year': + var year = date.year(); + return 'vis-year' + year + currentYear(date) + even(year); + + default: + return ''; } }; - exports.allOptions = allOptions; - exports.configureOptions = configureOptions; + module.exports = TimeStep; /***/ }, -/* 50 */ +/* 44 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var Emitter = __webpack_require__(20); - var Hammer = __webpack_require__(5); - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - var Range = __webpack_require__(30); - var Core = __webpack_require__(33); - var TimeAxis = __webpack_require__(42); - var CurrentTime = __webpack_require__(27); - var CustomTime = __webpack_require__(45); - var LineGraph = __webpack_require__(51); - - var Configurator = __webpack_require__(46); - var Validator = __webpack_require__(48)['default']; - var printStyle = __webpack_require__(48).printStyle; - var allOptions = __webpack_require__(59).allOptions; - var configureOptions = __webpack_require__(59).configureOptions; + var util = __webpack_require__(14); + var Group = __webpack_require__(40); /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | Array} [items] - * @param {Object} [options] See Graph2d.setOptions for the available options. - * @constructor - * @extends Core + * @constructor BackgroundGroup + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ - 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; + function BackgroundGroup(groupId, data, itemSet) { + Group.call(this, groupId, data, itemSet); + + this.width = 0; + this.height = 0; + this.top = 0; + this.left = 0; + } + + BackgroundGroup.prototype = Object.create(Group.prototype); + + /** + * 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 + */ + BackgroundGroup.prototype.redraw = function (range, margin, restack) { + var resized = false; + + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + + // calculate actual size + this.width = this.dom.background.offsetWidth; + + // apply new height (just always zero for BackgroundGroup + this.dom.background.style.height = '0'; + + // 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); } - var me = this; - this.defaultOptions = { - start: null, - end: null, + return resized; + }; - autoResize: true, + /** + * Show this group: attach to the DOM + */ + BackgroundGroup.prototype.show = function () { + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); + } + }; - orientation: { - axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' - item: 'bottom' // not relevant for Graph2d - }, + module.exports = BackgroundGroup; - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); +/***/ }, +/* 45 */ +/***/ function(module, exports, __webpack_require__) { - // Create the DOM, props, and emitter - this._create(container); + 'use strict'; - // all components listed here will be repainted automatically - this.components = []; + var Item = __webpack_require__(8); + var util = __webpack_require__(14); - this.body = { - dom: this.dom, - domProps: this.props, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this) + /** + * @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 }, - hiddenDates: [], - util: { - 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) + line: { + width: 0, + height: 0 } }; - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); + } + } - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); - //this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); + Item.call(this, data, conversion, options); + } - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + BoxItem.prototype = new Item(null, null, null); - // item set - this.linegraph = new LineGraph(this.body); - this.components.push(this.linegraph); + /** + * 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; + }; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + /** + * Repaint the item + */ + BoxItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - this.on('tap', function (event) { - me.emit('click', me.getEventProperties(event)); - }); - this.on('doubletap', function (event) { - me.emit('doubleClick', me.getEventProperties(event)); - }); - this.dom.root.oncontextmenu = function (event) { - me.emit('contextmenu', me.getEventProperties(event)); - }; + // create main box + dom.box = document.createElement('DIV'); - // apply options - if (options) { - this.setOptions(options); + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'vis-item-content'; + dom.box.appendChild(dom.content); + + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'vis-line'; + + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'vis-dot'; + + // attach this item as attribute + dom.box['timeline-item'] = this; + + this.dirty = true; } - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); + // 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; - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); + // 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 editable = (this.options.editable.updateTime || this.options.editable.updateGroup || this.editable === true) && this.editable !== false; + + // update class + var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : '') + (editable ? ' vis-editable' : ' vis-readonly'); + dom.box.className = 'vis-item vis-box' + className; + dom.line.className = 'vis-item vis-line' + className; + dom.dot.className = 'vis-item vis-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; } - } - // Extend the functionality from Core - Graph2d.prototype = new Core(); + this._repaintDeleteButton(dom.box); + }; - Graph2d.prototype.setOptions = function (options) { - // validate options - var errorFound = Validator.validate(options, allOptions); - if (errorFound === true) { - console.log('%cErrors have been found in the supplied options object.', printStyle); + /** + * 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(); } + }; - Core.prototype.setOptions.call(this, options); + /** + * 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.displayed = false; + } }; /** - * Set items - * @param {vis.DataSet | Array | null} items + * Reposition the item horizontally + * @Override */ - Graph2d.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; + BoxItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); + var align = this.options.align; + var left; - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; + // calculate left position of the box + if (align == 'right') { + this.left = start - this.width; + } else if (align == 'left') { + this.left = start; } else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: 'Date', - end: 'Date' - } - }); + // default or 'center' + this.left = start - this.width / 2; + } + + // reposition box + this.dom.box.style.left = this.left + 'px'; + + // reposition line + this.dom.line.style.left = start - this.props.line.width / 2 + 'px'; + + // reposition dot + this.dom.dot.style.left = start - this.props.dot.width / 2 + 'px'; + }; + + /** + * Reposition the item vertically + * @Override + */ + BoxItem.prototype.repositionY = function () { + var orientation = this.options.orientation.item; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; + + if (orientation == 'top') { + box.style.top = (this.top || 0) + 'px'; + + 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; + + box.style.top = (this.parent.height - this.top - this.height || 0) + 'px'; + line.style.top = itemSetHeight - lineHeight + 'px'; + line.style.bottom = '0'; + } + + dot.style.top = -this.props.dot.height / 2 + 'px'; + }; + + /** + * Return the width of the item left from its start date + * @return {number} + */ + BoxItem.prototype.getWidthLeft = function () { + return this.width / 2; + }; + + /** + * Return the width of the item right from its start date + * @return {number} + */ + BoxItem.prototype.getWidthRight = function () { + return this.width / 2; + }; + + module.exports = BoxItem; + +/***/ }, +/* 46 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var Hammer = __webpack_require__(10); + var Item = __webpack_require__(8); + var BackgroundGroup = __webpack_require__(44); + var RangeItem = __webpack_require__(42); + + /** + * @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 + } + }; + 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); + } + } + + Item.call(this, data, conversion, options); + } + + BackgroundItem.prototype = new Item(null, null, null); + + BackgroundItem.prototype.baseClassName = 'vis-item vis-background'; + BackgroundItem.prototype.stack = false; + + /** + * 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 + */ + 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() + + // frame box (to prevent the item contents from overflowing + dom.frame = document.createElement('div'); + dom.frame.className = 'vis-item-overflow'; + dom.box.appendChild(dom.frame); + + // contents box + dom.content = document.createElement('div'); + dom.content.className = 'vis-item-content'; + dom.frame.appendChild(dom.content); + + // Note: we do NOT attach this item as attribute to the DOM, + // such that background items cannot be selected + //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'); + } + background.appendChild(dom.box); } + this.displayed = true; - // set items - this.itemsData = newDataSet; - this.linegraph && this.linegraph.setItems(newDataSet); + // 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); - 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; + // update class + var className = (this.data.className ? ' ' + this.data.className : '') + (this.selected ? ' vis-selected' : ''); + dom.box.className = this.baseClassName + className; - this.setWindow(start, end, { animation: false }); - } else { - this.fit({ animation: false }); - } - } - }; + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; - /** - * Set groups - * @param {vis.DataSet | Array} groups - */ - Graph2d.prototype.setGroups = function (groups) { - // convert to type DataSet when needed - var newDataSet; - if (!groups) { - newDataSet = null; - } else if (groups instanceof DataSet || groups instanceof DataView) { - newDataSet = groups; - } else { - // turn an array into a dataset - newDataSet = new DataSet(groups); - } + // 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.groupsData = newDataSet; - this.linegraph.setGroups(newDataSet); + this.dirty = false; + } }; /** - * 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 + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - 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 { - return 'cannot find group:' + groupId; - } - }; + BackgroundItem.prototype.show = RangeItem.prototype.show; /** - * This checks if the visible option of the supplied group (by ID) is true or false. - * @param groupId - * @returns {*} + * Hide the item from the DOM (when visible) + * @return {Boolean} changed */ - 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; - } - }; + BackgroundItem.prototype.hide = RangeItem.prototype.hide; /** - * 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 + * Reposition the item horizontally + * @Override */ - Graph2d.prototype.getDataRange = 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; - } - } - } - } - - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; - }; + BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; /** - * Generate Timeline related information from an event - * @param {Event} event - * @return {Object} An object with related information, like on which area - * The event happened, whether clicked on an item, etc. + * Reposition the item vertically + * @Override */ - Graph2d.prototype.getEventProperties = function (event) { - var clientX = event.center ? event.center.x : event.clientX; - var clientY = event.center ? event.center.y : event.clientY; - var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); - var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); - var time = this._toTime(x); + BackgroundItem.prototype.repositionY = function (margin) { + var onTop = this.options.orientation.item === 'top'; + this.dom.content.style.top = onTop ? '' : '0'; + this.dom.content.style.bottom = onTop ? '0' : ''; + var height; - var customTime = CustomTime.customTimeFromTarget(event); + // special positioning for subgroups + if (this.data.subgroup !== undefined) { + // TODO: instead of calculating the top position of the subgroups here for every BackgroundItem, calculate the top of the subgroup once in Itemset - var element = util.getTarget(event); - var what = null; - if (util.hasParent(element, this.timeAxis.dom.foreground)) { - what = 'axis'; - } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { - what = 'axis'; - } else if (util.hasParent(element, this.linegraph.yAxisLeft.dom.frame)) { - what = 'data-axis'; - } else if (util.hasParent(element, this.linegraph.yAxisRight.dom.frame)) { - what = 'data-axis'; - } else if (util.hasParent(element, this.linegraph.legendLeft.dom.frame)) { - what = 'legend'; - } else if (util.hasParent(element, this.linegraph.legendRight.dom.frame)) { - what = 'legend'; - } else if (customTime != null) { - what = 'custom-time'; - } else if (util.hasParent(element, this.currentTime.bar)) { - what = 'current-time'; - } else if (util.hasParent(element, this.dom.center)) { - what = 'background'; - } + var itemSubgroup = this.data.subgroup; + var subgroups = this.parent.subgroups; + var subgroupIndex = subgroups[itemSubgroup].index; + // 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[itemSubgroup].height + margin.item.vertical; + height += subgroupIndex == 0 ? margin.axis - 0.5 * margin.item.vertical : 0; + var newTop = this.parent.top; + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroupIndex) { + newTop += subgroups[subgroup].height + margin.item.vertical; + } + } + } - var value = []; - var yAxisLeft = this.linegraph.yAxisLeft; - var yAxisRight = this.linegraph.yAxisRight; - if (!yAxisLeft.hidden) { - value.push(yAxisLeft.screenToValue(y)); + // the others will have to be offset downwards with this same distance. + newTop += subgroupIndex != 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; + var totalHeight = 0; + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true) { + var newHeight = subgroups[subgroup].height + margin.item.vertical; + totalHeight += newHeight; + if (subgroups[subgroup].index > subgroupIndex) { + newTop += newHeight; + } + } + } + } + height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; + this.dom.box.style.top = this.parent.height - totalHeight + newTop + 'px'; + this.dom.box.style.bottom = ''; + } } - if (!yAxisRight.hidden) { - value.push(yAxisRight.screenToValue(y)); + // and in the case of no subgroups: + else { + // we want backgrounds with groups to only show in groups. + if (this.parent instanceof BackgroundGroup) { + // if the item is not in a group: + height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); + this.dom.box.style.top = onTop ? '0' : ''; + this.dom.box.style.bottom = onTop ? '' : '0'; + } else { + 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 = ''; + } } - - return { - event: event, - what: what, - pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, - pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, - x: x, - y: y, - time: time, - value: value - }; - }; - - /** - * Load a configurator - * @return {Object} - * @private - */ - Graph2d.prototype._createConfigurator = function () { - return new Configurator(this, this.dom.container, configureOptions); + this.dom.box.style.height = height + 'px'; }; - module.exports = Graph2d; + module.exports = BackgroundItem; /***/ }, -/* 51 */ +/* 47 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(9); - var DOMutil = __webpack_require__(14); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - var Component = __webpack_require__(28); - var DataAxis = __webpack_require__(52); - var GraphGroup = __webpack_require__(54); - var Legend = __webpack_require__(58); - var BarFunctions = __webpack_require__(57); - var LineFunctions = __webpack_require__(55); - - var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + var util = __webpack_require__(14); + var Component = __webpack_require__(33); + var TimeStep = __webpack_require__(43); + var DateUtil = __webpack_require__(37); + var moment = __webpack_require__(15); /** - * This is the constructor of the LineGraph. It requires a Timeline body and options. - * - * @param body - * @param options - * @constructor + * A horizontal time axis + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body + * @param {Object} [options] See TimeAxis.setOptions for the available + * options. + * @constructor TimeAxis + * @extends Component */ - function LineGraph(body, options) { - this.id = util.randomUUID(); - this.body = body; - - this.defaultOptions = { - yAxisOrientation: 'left', - defaultGroup: 'default', - sort: true, - sampling: true, - stack: false, - graphHeight: '400px', - shaded: { - enabled: false, - orientation: 'bottom' // top, bottom - }, - style: 'line', // line, bar - barChart: { - width: 50, - sideBySide: false, - align: 'center' // left, center, right - }, - interpolation: { - 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, - alignZeros: true, - left: { - range: { min: undefined, max: undefined }, - format: function format(value) { - return value; - }, - title: { text: undefined, style: undefined } - }, - right: { - range: { min: undefined, max: undefined }, - format: function format(value) { - return value; - }, - title: { text: undefined, style: 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: {} + function TimeAxis(body, options) { + this.dom = { + foreground: null, + lines: [], + majorTexts: [], + minorTexts: [], + redundant: { + lines: [], + majorTexts: [], + minorTexts: [] } }; - - // 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; - this.updateSVGheight = false; - this.updateSVGheightOnResize = false; - - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - - // listeners for the DataSet of the items - this.itemListeners = { - 'add': function add(event, params, senderId) { - me._onAdd(params.items); - }, - 'update': function update(event, params, senderId) { - me._onUpdate(params.items); + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 }, - 'remove': function remove(event, params, senderId) { - me._onRemove(params.items); - } + lineTop: 0 }; - // listeners for the DataSet of the groups - this.groupListeners = { - 'add': function add(event, params, senderId) { - me._onAddGroups(params.items); - }, - 'update': function update(event, params, senderId) { - me._onUpdateGroups(params.items); - }, - 'remove': function remove(event, params, senderId) { - me._onRemoveGroups(params.items); - } + this.defaultOptions = { + orientation: { + axis: 'bottom' + }, // axis orientation: 'top' or 'bottom' + showMinorLabels: true, + showMajorLabels: true, + format: TimeStep.FORMAT, + timeAxis: null }; + this.options = util.extend({}, this.defaultOptions); - 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.COUNTER = 0; - this.body.emitter.on('rangechanged', function () { - me.lastStart = me.body.range.start; - me.svg.style.left = util.option.asSize(-me.props.width); - me.redraw.call(me, true); - }); + this.body = body; // create the HTML DOM this._create(); - this.framework = { svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups }; - this.body.emitter.emit('change'); - } - - LineGraph.prototype = new Component(); - - /** - * Create the HTML DOM for the ItemSet - */ - LineGraph.prototype._create = function () { - var frame = document.createElement('div'); - frame.className = 'vis-line-graph'; - 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.setOptions(options); + } - this.show(); - }; + TimeAxis.prototype = new Component(); /** - * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. - * @param {object} options + * Set options for the TimeAxis. + * Parameters will be merged in current options. + * @param {Object} options Available options: + * {string} [orientation.axis] + * {boolean} [showMinorLabels] + * {boolean} [showMajorLabels] */ - LineGraph.prototype.setOptions = function (options) { + TimeAxis.prototype.setOptions = function (options) { if (options) { - var fields = ['sampling', 'defaultGroup', 'stack', 'height', 'graphHeight', 'yAxisOrientation', 'style', 'barChart', 'dataAxis', 'sort', 'groups']; - if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) { - this.updateSVGheight = true; - this.updateSVGheightOnResize = true; - } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { - if (parseInt((options.graphHeight + '').replace('px', '')) < this.body.domProps.centerContainer.height) { - this.updateSVGheight = true; - } - } - util.selectiveDeepExtend(fields, this.options, options); - util.mergeOptions(this.options, options, 'interpolation'); - util.mergeOptions(this.options, options, 'drawPoints'); - util.mergeOptions(this.options, options, 'shaded'); - util.mergeOptions(this.options, options, 'legend'); - - if (options.interpolation) { - if (typeof options.interpolation == 'object') { - if (options.interpolation.parametrization) { - if (options.interpolation.parametrization == 'uniform') { - this.options.interpolation.alpha = 0; - } else if (options.interpolation.parametrization == 'chordal') { - this.options.interpolation.alpha = 1; - } else { - this.options.interpolation.parametrization = 'centripetal'; - this.options.interpolation.alpha = 0.5; - } - } - } - } + // copy all options that we know + util.selectiveExtend(['showMinorLabels', 'showMajorLabels', 'hiddenDates', 'timeAxis'], this.options, options); - if (this.yAxisLeft) { - if (options.dataAxis !== undefined) { - this.yAxisLeft.setOptions(this.options.dataAxis); - this.yAxisRight.setOptions(this.options.dataAxis); - } - } + // deep copy the format options + util.selectiveDeepExtend(['format'], this.options, options); - if (this.legendLeft) { - if (options.legend !== undefined) { - this.legendLeft.setOptions(this.options.legend); - this.legendRight.setOptions(this.options.legend); + if ('orientation' in options) { + if (typeof options.orientation === 'string') { + this.options.orientation.axis = options.orientation; + } else if (typeof options.orientation === 'object' && 'axis' in options.orientation) { + this.options.orientation.axis = options.orientation.axis; } } - if (this.groups.hasOwnProperty(UNGROUPED)) { - this.groups[UNGROUPED].setOptions(options); - } - } - - // this is used to redraw the graph if the visibility of the groups is changed. - if (this.dom.frame) { - this.redraw(true); + // apply locale to moment.js + // TODO: not so nice, this is applied globally to moment.js + if ('locale' in options) { + if (typeof moment.locale === 'function') { + // moment.js 2.8.1+ + moment.locale(options.locale); + } else { + moment.lang(options.locale); + } + } } }; /** - * Hide the component from the DOM + * Create the HTML DOM for the TimeAxis */ - LineGraph.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + TimeAxis.prototype._create = function () { + this.dom.foreground = document.createElement('div'); + this.dom.background = document.createElement('div'); + + this.dom.foreground.className = 'vis-time-axis vis-foreground'; + this.dom.background.className = 'vis-time-axis vis-background'; }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * Destroy the TimeAxis */ - LineGraph.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); + TimeAxis.prototype.destroy = function () { + // remove from DOM + if (this.dom.foreground.parentNode) { + this.dom.foreground.parentNode.removeChild(this.dom.foreground); + } + if (this.dom.background.parentNode) { + this.dom.background.parentNode.removeChild(this.dom.background); } + + this.body = null; }; /** - * Set items - * @param {vis.DataSet | null} items + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - LineGraph.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; + TimeAxis.prototype.redraw = function () { + var props = this.props; + var foreground = this.dom.foreground; + var background = this.dom.background; - // replace the dataset - if (!items) { - this.itemsData = null; - } else if (items instanceof DataSet || items instanceof DataView) { - this.itemsData = items; - } else { - throw new TypeError('Data must be an instance of DataSet or DataView'); - } + // determine the correct parent DOM element (depending on option orientation) + var parent = this.options.orientation.axis == 'top' ? this.body.dom.top : this.body.dom.bottom; + var parentChanged = foreground.parentNode !== parent; - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); - }); + // calculate character width and height + this._calculateCharSize(); - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + // TODO: recalculate sizes only needed when parent is resized or options is changed + var showMinorLabels = this.options.showMinorLabels && this.options.orientation.axis !== 'none'; + var showMajorLabels = this.options.showMajorLabels && this.options.orientation.axis !== 'none'; - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); - }); + // determine the width and height of the elemens for the axis + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + props.height = props.minorLabelHeight + props.majorLabelHeight; + props.width = foreground.offsetWidth; - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - (this.options.orientation.axis == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height); + props.minorLineWidth = 1; // TODO: really calculate width + props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight; + props.majorLineWidth = 1; // TODO: really calculate width + + // take foreground and background offline while updating (is almost twice as fast) + var foregroundNextSibling = foreground.nextSibling; + var backgroundNextSibling = background.nextSibling; + foreground.parentNode && foreground.parentNode.removeChild(foreground); + background.parentNode && background.parentNode.removeChild(background); + + foreground.style.height = this.props.height + 'px'; + + this._repaintLabels(); + + // put DOM online again (at the same place) + if (foregroundNextSibling) { + parent.insertBefore(foreground, foregroundNextSibling); + } else { + parent.appendChild(foreground); } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); + if (backgroundNextSibling) { + this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling); + } else { + this.body.dom.backgroundVertical.appendChild(background); + } + + return this._isResized() || parentChanged; }; /** - * Set groups - * @param {vis.DataSet} groups + * Repaint major and minor text labels and vertical grid lines + * @private */ - LineGraph.prototype.setGroups = function (groups) { - var me = this; - var ids; + TimeAxis.prototype._repaintLabels = function () { + var orientation = this.options.orientation.axis; - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.off(event, callback); - }); + // calculate range and step (step such that we have space for 7 characters per label) + var start = util.convert(this.body.range.start, 'Number'); + var end = util.convert(this.body.range.end, 'Number'); + var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf(); + var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this.body.range, timeLabelsize); + minimumStep -= this.body.util.toTime(0).valueOf(); - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw + var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates); + if (this.options.format) { + step.setFormat(this.options.format); } + if (this.options.timeAxis) { + step.setScale(this.options.timeAxis); + } + this.step = step; - // 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'); + // Move all DOM elements to a "redundant" list, where they + // can be picked for re-use, and clear the lists with lines and texts. + // At the end of the function _repaintLabels, left over elements will be cleaned up + var dom = this.dom; + dom.redundant.lines = dom.lines; + dom.redundant.majorTexts = dom.majorTexts; + dom.redundant.minorTexts = dom.minorTexts; + dom.lines = []; + dom.majorTexts = []; + dom.minorTexts = []; + + var cur; + var x = 0; + var isMajor; + var xPrev = 0; + var width = 0; + var prevLine; + var xFirstMajorLabel = undefined; + var max = 0; + var className; + + step.first(); + while (step.hasNext() && max < 1000) { + max++; + + cur = step.getCurrent(); + isMajor = step.isMajor(); + className = step.getClassName(); + + xPrev = x; + x = this.body.util.toScreen(cur); + width = x - xPrev; + if (prevLine) { + prevLine.style.width = width + 'px'; + } + + if (this.options.showMinorLabels) { + this._repaintMinorText(x, step.getLabelMinor(), orientation, className); + } + + if (isMajor && this.options.showMajorLabels) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this._repaintMajorText(x, step.getLabelMajor(), orientation, className); + } + prevLine = this._repaintMajorLine(x, orientation, className); + } else { + prevLine = this._repaintMinorLine(x, orientation, className); + } + + step.next(); } - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + // create a major label on the left when needed + if (this.options.showMajorLabels) { + var leftTime = this.body.util.toTime(0), + leftText = step.getLabelMajor(leftTime), + widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText, orientation, className); + } } - this._onUpdate(); + + // Cleanup leftover DOM elements from the redundant list + util.forEach(this.dom.redundant, function (arr) { + while (arr.length) { + var elem = arr.pop(); + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + }); }; /** - * Update the data - * @param [ids] + * Create a minor label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className * @private */ - LineGraph.prototype._onUpdate = function (ids) { - this._updateUngrouped(); - this._updateAllGroupData(); - //this._updateGraph(); - this.redraw(true); - }; - 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]); + TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); + + if (!label) { + // create new label + var content = document.createTextNode(''); + label = document.createElement('div'); + label.appendChild(content); + this.dom.foreground.appendChild(label); } + this.dom.minorTexts.push(label); - //this._updateGraph(); - this.redraw(true); - }; - LineGraph.prototype._onAddGroups = function (groupIds) { - this._onUpdateGroups(groupIds); + label.childNodes[0].nodeValue = text; + + label.style.top = orientation == 'top' ? this.props.majorLabelHeight + 'px' : '0'; + label.style.left = x + 'px'; + label.className = 'vis-text vis-minor ' + className; + //label.title = title; // TODO: this is a heavy operation }; /** - * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph - * @param {Array} groupIds + * Create a Major label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className * @private */ - 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]]; - } + TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); + + if (!label) { + // create label + var content = document.createTextNode(text); + label = document.createElement('div'); + label.appendChild(content); + this.dom.foreground.appendChild(label); } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); + this.dom.majorTexts.push(label); + + label.childNodes[0].nodeValue = text; + label.className = 'vis-text vis-major ' + className; + //label.title = title; // TODO: this is a heavy operation + + label.style.top = orientation == 'top' ? '0' : this.props.minorLabelHeight + 'px'; + label.style.left = x + 'px'; }; /** - * update a group object with the group dataset entree - * - * @param group - * @param groupId + * Create a minor line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className + * @return {Element} Returns the created line * @private */ - LineGraph.prototype._updateGroup = function (group, groupId) { - if (!this.groups.hasOwnProperty(groupId)) { - this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles); - if (this.groups[groupId].options.yAxisOrientation == 'right') { - this.yAxisRight.addGroup(groupId, this.groups[groupId]); - this.legendRight.addGroup(groupId, this.groups[groupId]); - } else { - this.yAxisLeft.addGroup(groupId, this.groups[groupId]); - this.legendLeft.addGroup(groupId, this.groups[groupId]); - } + TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) { + // reuse redundant line + var line = this.dom.redundant.lines.shift(); + if (!line) { + // create vertical line + line = document.createElement('div'); + this.dom.background.appendChild(line); + } + this.dom.lines.push(line); + + var props = this.props; + if (orientation == 'top') { + line.style.top = props.majorLabelHeight + 'px'; } 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]); - } + line.style.top = this.body.domProps.top.height + 'px'; } - this.legendLeft.redraw(); - this.legendRight.redraw(); + line.style.height = props.minorLineHeight + 'px'; + line.style.left = x - props.minorLineWidth / 2 + 'px'; + + line.className = 'vis-grid vis-vertical vis-minor ' + className; + + return line; }; /** - * this updates all groups, it is used when there is an update the the itemset. - * + * Create a Major line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className + * @return {Element} Returns the created line * @private */ - 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]; - if (groupsContent[item.group] === undefined) { - throw new Error('Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups.'); - } - 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]); - } - } + TimeAxis.prototype._repaintMajorLine = function (x, orientation, className) { + // reuse redundant line + var line = this.dom.redundant.lines.shift(); + if (!line) { + // create vertical line + line = document.createElement('div'); + this.dom.background.appendChild(line); } - }; - - /** - * Create or delete the group holding all ungrouped items. This group is used when - * there are no groups specified. This anonymous group is called 'graph'. - * @protected - */ - LineGraph.prototype._updateUngrouped = function () { - if (this.itemsData && 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; - } - } - } + this.dom.lines.push(line); - 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); - } + var props = this.props; + if (orientation == 'top') { + line.style.top = '0'; } else { - delete this.groups[UNGROUPED]; - this.legendLeft.removeGroup(UNGROUPED); - this.legendRight.removeGroup(UNGROUPED); - this.yAxisLeft.removeGroup(UNGROUPED); - this.yAxisRight.removeGroup(UNGROUPED); + line.style.top = this.body.domProps.top.height + 'px'; } + line.style.left = x - props.majorLineWidth / 2 + 'px'; + line.style.height = props.majorLineHeight + 'px'; - this.legendLeft.redraw(); - this.legendRight.redraw(); + line.className = 'vis-grid vis-vertical vis-major ' + className; + + return line; }; /** - * Redraw the component, mandatory function - * @return {boolean} Returns true if the component is resized + * 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.redraw = function (forceGraphUpdate) { - var resized = false; + TimeAxis.prototype._calculateCharSize = function () { + // Note: We calculate char size with every redraw. Size may change, for + // example when any of the timelines parents had display:none for example. - // calculate actual size and position - this.props.width = this.dom.frame.offsetWidth; - this.props.height = this.body.domProps.centerContainer.height - this.body.domProps.border.top - this.body.domProps.border.bottom; + // determine the char width and height on the minor axis + if (!this.dom.measureCharMinor) { + this.dom.measureCharMinor = document.createElement('DIV'); + this.dom.measureCharMinor.className = 'vis-text vis-minor vis-measure'; + this.dom.measureCharMinor.style.position = 'absolute'; - // update the graph if there is no lastWidth or with, used for the initial draw - if (this.lastWidth === undefined && this.props.width) { - forceGraphUpdate = true; + this.dom.measureCharMinor.appendChild(document.createTextNode('0')); + this.dom.foreground.appendChild(this.dom.measureCharMinor); + } + this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; + this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; + + // determine the char width and height on the major axis + if (!this.dom.measureCharMajor) { + this.dom.measureCharMajor = document.createElement('DIV'); + this.dom.measureCharMajor.className = 'vis-text vis-major vis-measure'; + this.dom.measureCharMajor.style.position = 'absolute'; + + this.dom.measureCharMajor.appendChild(document.createTextNode('0')); + this.dom.foreground.appendChild(this.dom.measureCharMajor); } + this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; + this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; + }; + + module.exports = TimeAxis; + +/***/ }, +/* 48 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var keycharm = __webpack_require__(49); + var Emitter = __webpack_require__(25); + var Hammer = __webpack_require__(10); + var util = __webpack_require__(14); + + /** + * 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; - // check if this component is resized - resized = this._isResized() || resized; + this.dom = { + container: container + }; - // 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.lastVisibleInterval = visibleInterval; + this.dom.overlay = document.createElement('div'); + this.dom.overlay.className = 'vis-overlay'; - // 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.props.width); - this.svg.style.left = util.option.asSize(-this.props.width); + this.dom.container.appendChild(this.dom.overlay); - // if the height of the graph is set as proportional, change the height of the svg - if ((this.options.height + '').indexOf('%') != -1 || this.updateSVGheightOnResize == true) { - this.updateSVGheight = true; - } - } + this.hammer = Hammer(this.dom.overlay); + this.hammer.on('tap', this._onTapOverlay.bind(this)); - // update the height of the graph on each redraw of the graph. - if (this.updateSVGheight == true) { - if (this.options.graphHeight != this.props.height + 'px') { - this.options.graphHeight = this.props.height + 'px'; - this.svg.style.height = this.props.height + 'px'; - } - this.updateSVGheight = false; - } else { - this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px'; - } + // block all touch events (except tap) + var me = this; + var events = ['tap', 'doubletap', 'press', 'pinch', 'pan', 'panstart', 'panmove', 'panend']; + events.forEach(function (event) { + me.hammer.on(event, function (event) { + event.stopPropagation(); + }); + }); - // zoomed is here to ensure that animations are shown correctly. - if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) { - resized = this._updateGraph() || resized; - } 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.props.width != 0) { - var rangePerPixelInv = this.props.width / range; - var xOffset = offset * rangePerPixelInv; - this.svg.style.left = -this.props.width - xOffset + 'px'; + // attach a click event to the window, in order to deactivate when clicking outside the timeline + if (document && document.body) { + this.onClick = function (event) { + if (!_hasParent(event.target, container)) { + me.deactivate(); } - } + }; + document.body.addEventListener('click', this.onClick); } - this.legendLeft.redraw(); - this.legendRight.redraw(); - return resized; - }; - - /** - * Update and redraw the graph. - * - */ - LineGraph.prototype._updateGraph = function () { - // reset the svg elements - DOMutil.prepareElements(this.svgElements); - if (this.props.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 only loads the data we require based on the timewindow - this._getRelevantData(groupIds, groupsData, minDate, maxDate); + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } + this.keycharm = keycharm(); - // apply sampling, if disabled, it will pass through this function. - this._applySampling(groupIds, groupsData); + // keycharm listener only bounded when active) + this.escListener = this.deactivate.bind(this); + } - // we transform the X coordinates to detect collisions - for (i = 0; i < groupIds.length; i++) { - preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); - } + // turn into an event emitter + Emitter(Activator.prototype); - // now all needed data has been collected we start the processing. - this._getYRanges(groupIds, preprocessedGroupData, groupRanges); + // The currently active activator + Activator.current = null; - // 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); - var MAX_CYCLES = 5; - if (changeCalled == true && this.COUNTER < MAX_CYCLES) { - DOMutil.cleanupElements(this.svgElements); - this.abortedGraphUpdate = true; - this.COUNTER++; - this.body.emitter.emit('change'); - return true; - } else { - if (this.COUNTER > MAX_CYCLES) { - console.log('WARNING: there may be an infinite loop in the _updateGraph emitter cycle.'); - } - this.COUNTER = 0; - this.abortedGraphUpdate = false; + /** + * Destroy the activator. Cleans up all created DOM and event listeners + */ + Activator.prototype.destroy = function () { + this.deactivate(); - // 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); - } + // remove dom + this.dom.overlay.parentNode.removeChild(this.dom.overlay); - // draw the groups - for (i = 0; i < groupIds.length; i++) { - group = this.groups[groupIds[i]]; - if (group.options.style != 'bar') { - // bar needs to be drawn enmasse - group.draw(processedGroupData[groupIds[i]], group, this.framework); - } - } - BarFunctions.draw(groupIds, processedGroupData, this.framework); - } - } + // remove global event listener + if (this.onClick) { + document.body.removeEventListener('click', this.onClick); } - // cleanup unused svg elements - DOMutil.cleanupElements(this.svgElements); - return false; + // cleanup hammer instances + this.hammer.destroy(); + this.hammer = null; + // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) }; /** - * 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. - * - * @param {array} groupIds - * @param {object} groupsData - * @param {date} minDate - * @param {date} maxDate - * @private + * Activate the element + * Overlay is hidden, element is decorated with a blue shadow border */ - LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { - 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.binarySearchValue(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); - } - } - } - } - } + 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 + this.keycharm.bind('esc', this.escListener); }; /** - * - * @param groupIds - * @param groupsData - * @private + * Deactivate the element + * Overlay is displayed on top of the element */ - 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))); + Activator.prototype.deactivate = function () { + this.active = false; + this.dom.overlay.style.display = ''; + util.removeClassName(this.dom.container, 'vis-active'); + this.keycharm.unbind('esc', this.escListener); - var sampledData = []; - for (var j = 0; j < amountOfPoints; j += increment) { - sampledData.push(dataContainer[j]); - } - groupsData[groupIds[i]] = sampledData; - } - } - } - } + this.emit('change'); + this.emit('deactivate'); }; /** - * - * - * @param {array} groupIds - * @param {object} groupsData - * @param {object} groupRanges | this is being filled here + * Handle a tap event: activate the container + * @param event * @private */ - LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { - var groupData, group, i; - var combinedDataLeft = []; - var combinedDataRight = []; - var options; - if (groupIds.length > 0) { - for (i = 0; i < groupIds.length; i++) { - groupData = groupsData[groupIds[i]]; - options = this.groups[groupIds[i]].options; - if (groupData.length > 0) { - group = this.groups[groupIds[i]]; - // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. - if (options.stack === true && options.style === 'bar') { - if (options.yAxisOrientation === 'left') { - combinedDataLeft = combinedDataLeft.concat(group.getData(groupData)); - } else { - combinedDataRight = combinedDataRight.concat(group.getData(groupData)); - } - } else { - groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); - } - } - } - - // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. - BarFunctions.getStackedYRange(combinedDataLeft, groupRanges, groupIds, '__barStackLeft', 'left'); - BarFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__barStackRight', 'right'); - // if line graphs are stacked, their range need to be handled differently and accumulated over all groups. - //LineFunctions.getStackedYRange(combinedDataLeft , groupRanges, groupIds, '__lineStackLeft' , 'left' ); - //LineFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__lineStackRight', 'right'); - } + Activator.prototype._onTapOverlay = function (event) { + // activate the container + this.activate(); + event.stopPropagation(); }; /** - * 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 + * 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 */ - LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) { - var resized = false; - var yAxisLeftUsed = false; - var yAxisRightUsed = false; - var minLeft = 1000000000, - minRight = 1000000000, - maxLeft = -1000000000, - maxRight = -1000000000, - minVal, - maxVal; - // if groups are present - if (groupIds.length > 0) { - // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop. - for (var i = 0; i < groupIds.length; i++) { - var group = this.groups[groupIds[i]]; - if (group && group.options.yAxisOrientation != 'right') { - yAxisLeftUsed = true; - minLeft = 1000000000; - maxLeft = -1000000000; - } else if (group && group.options.yAxisOrientation) { - yAxisRightUsed = true; - minRight = 1000000000; - maxRight = -1000000000; - } + function _hasParent(element, parent) { + while (element) { + if (element === parent) { + return true; } + element = element.parentNode; + } + return false; + } - // if there are items: - 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; + module.exports = Activator; - if (groupRanges[groupIds[i]].yAxisOrientation != 'right') { - 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; - } - } - } - } +/***/ }, +/* 49 */ +/***/ function(module, exports, __webpack_require__) { - if (yAxisLeftUsed == true) { - this.yAxisLeft.setRange(minLeft, maxLeft); - } - if (yAxisRightUsed == true) { - this.yAxisRight.setRange(minRight, maxRight); - } - } - resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized; - resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized; + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; + /** + * Created by Alex on 11/6/2014. + */ - if (yAxisRightUsed == true && yAxisLeftUsed == true) { - this.yAxisLeft.drawIcons = true; - this.yAxisRight.drawIcons = true; + // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 + // if the module has no dependencies, the above pattern can be simplified to + (function (root, factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); } else { - this.yAxisLeft.drawIcons = false; - this.yAxisRight.drawIcons = false; + // Browser globals (root is window) + root.keycharm = factory(); } - this.yAxisRight.master = !yAxisLeftUsed; - if (this.yAxisRight.master == false) { - if (yAxisRightUsed == true) { - this.yAxisLeft.lineOffset = this.yAxisRight.width; - } else { - this.yAxisLeft.lineOffset = 0; - } + }(this, function () { - resized = this.yAxisLeft.redraw() || resized; - this.yAxisRight.stepPixels = this.yAxisLeft.stepPixels; - this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing; - this.yAxisRight.amountOfSteps = this.yAxisLeft.amountOfSteps; - resized = this.yAxisRight.redraw() || resized; - } else { - resized = this.yAxisRight.redraw() || resized; - } + function keycharm(options) { + var preventDefault = options && options.preventDefault || false; + + var container = options && options.container || window; + + var _exportFunctions = {}; + var _bound = {keydown:{}, keyup:{}}; + var _keys = {}; + var i; + + // a - z + for (i = 97; i <= 122; i++) {_keys[String.fromCharCode(i)] = {code:65 + (i - 97), shift: false};} + // A - Z + for (i = 65; i <= 90; i++) {_keys[String.fromCharCode(i)] = {code:i, shift: true};} + // 0 - 9 + for (i = 0; i <= 9; i++) {_keys['' + i] = {code:48 + i, shift: false};} + // F1 - F12 + for (i = 1; i <= 12; i++) {_keys['F' + i] = {code:111 + i, shift: false};} + // num0 - num9 + for (i = 0; i <= 9; i++) {_keys['num' + i] = {code:96 + i, shift: false};} + + // numpad misc + _keys['num*'] = {code:106, shift: false}; + _keys['num+'] = {code:107, shift: false}; + _keys['num-'] = {code:109, shift: false}; + _keys['num/'] = {code:111, shift: false}; + _keys['num.'] = {code:110, shift: false}; + // arrows + _keys['left'] = {code:37, shift: false}; + _keys['up'] = {code:38, shift: false}; + _keys['right'] = {code:39, shift: false}; + _keys['down'] = {code:40, shift: false}; + // extra keys + _keys['space'] = {code:32, shift: false}; + _keys['enter'] = {code:13, shift: false}; + _keys['shift'] = {code:16, shift: undefined}; + _keys['esc'] = {code:27, shift: false}; + _keys['backspace'] = {code:8, shift: false}; + _keys['tab'] = {code:9, shift: false}; + _keys['ctrl'] = {code:17, shift: false}; + _keys['alt'] = {code:18, shift: false}; + _keys['delete'] = {code:46, shift: false}; + _keys['pageup'] = {code:33, shift: false}; + _keys['pagedown'] = {code:34, shift: false}; + // symbols + _keys['='] = {code:187, shift: false}; + _keys['-'] = {code:189, shift: false}; + _keys[']'] = {code:221, shift: false}; + _keys['['] = {code:219, shift: false}; + + + + var down = function(event) {handleEvent(event,'keydown');}; + var up = function(event) {handleEvent(event,'keyup');}; + + // handle the actualy bound key with the event + var handleEvent = function(event,type) { + if (_bound[type][event.keyCode] !== undefined) { + var bound = _bound[type][event.keyCode]; + for (var i = 0; i < bound.length; i++) { + if (bound[i].shift === undefined) { + bound[i].fn(event); + } + else if (bound[i].shift == true && event.shiftKey == true) { + bound[i].fn(event); + } + else if (bound[i].shift == false && event.shiftKey == false) { + bound[i].fn(event); + } + } + + if (preventDefault == true) { + event.preventDefault(); + } + } + }; + + // bind a key to a callback + _exportFunctions.bind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (_bound[type][_keys[key].code] === undefined) { + _bound[type][_keys[key].code] = []; + } + _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); + }; - // clean the accumulated lists - var tempGroups = ['__barStackLeft', '__barStackRight', '__lineStackLeft', '__lineStackRight']; - for (var i = 0; i < tempGroups.length; i++) { - if (groupIds.indexOf(tempGroups[i]) != -1) { - groupIds.splice(groupIds.indexOf(tempGroups[i]), 1); - } - } - return resized; - }; + // bind all keys to a call back (demo purposes) + _exportFunctions.bindAll = function(callback, type) { + if (type === undefined) { + type = 'keydown'; + } + for (var key in _keys) { + if (_keys.hasOwnProperty(key)) { + _exportFunctions.bind(key,callback,type); + } + } + }; - /** - * 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.hidden == false) { - axis.hide(); - changed = true; - } - } else { - if (!axis.dom.frame.parentNode && axis.hidden == true) { - axis.show(); - changed = true; - } - } - return changed; - }; + // get the key label from an event + _exportFunctions.getKey = function(event) { + for (var key in _keys) { + if (_keys.hasOwnProperty(key)) { + if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { + return key; + } + else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { + return key; + } + else if (event.keyCode == _keys[key].code && key == 'shift') { + return key; + } + } + } + return "unknown key, currently not supported"; + }; - /** - * 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; + // unbind either a specific callback from a key or all of them (by leaving callback undefined) + _exportFunctions.unbind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (callback !== undefined) { + var newBindings = []; + var bound = _bound[type][_keys[key].code]; + if (bound !== undefined) { + for (var i = 0; i < bound.length; i++) { + if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { + newBindings.push(_bound[type][_keys[key].code][i]); + } + } + } + _bound[type][_keys[key].code] = newBindings; + } + else { + _bound[type][_keys[key].code] = []; + } + }; - for (var i = 0; i < datapoints.length; i++) { - xValue = toScreen(datapoints[i].x) + this.props.width; - yValue = datapoints[i].y; - extractedData.push({ x: xValue, y: yValue }); - } + // reset all bound variables. + _exportFunctions.reset = function() { + _bound = {keydown:{}, keyup:{}}; + }; - return extractedData; - }; + // unbind all listeners and reset all variables. + _exportFunctions.destroy = function() { + _bound = {keydown:{}, keyup:{}}; + container.removeEventListener('keydown', down, true); + container.removeEventListener('keyup', up, true); + }; - /** - * 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 - * @param group - * @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; - } + // create listeners. + container.addEventListener('keydown',down,true); + container.addEventListener('keyup',up,true); - for (var i = 0; i < datapoints.length; i++) { - var labelValue = datapoints[i].label ? datapoints[i].label : null; - xValue = toScreen(datapoints[i].x) + this.props.width; - yValue = Math.round(axis.convertValue(datapoints[i].y)); - extractedData.push({ x: xValue, y: yValue, label: labelValue }); + // return the public functions. + return _exportFunctions; } - group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); + return keycharm; + })); + - return extractedData; - }; - module.exports = LineGraph; /***/ }, -/* 52 */ +/* 50 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(9); - var DOMutil = __webpack_require__(14); - var Component = __webpack_require__(28); - var DataStep = __webpack_require__(53); + var Hammer = __webpack_require__(10); + var util = __webpack_require__(14); + var Component = __webpack_require__(33); + var moment = __webpack_require__(15); + var locales = __webpack_require__(34); /** - * A horizontal time axis - * @param {Object} [options] See DataAxis.setOptions for the available - * options. - * @constructor DataAxis + * A custom time bar + * @param {{range: Range, dom: Object}} body + * @param {Object} [options] Available parameters: + * {number | string} id + * {string} locales + * {string} locale + * @constructor CustomTime * @extends Component - * @param body */ - function DataAxis(body, options, svg, linegraphOptions) { - this.id = util.randomUUID(); + + function CustomTime(body, options) { this.body = body; + // default options 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, - alignZeros: true, - left: { - range: { min: undefined, max: undefined }, - format: function format(value) { - return value; - }, - title: { text: undefined, style: undefined } - }, - right: { - range: { min: undefined, max: undefined }, - format: function format(value) { - return value; - }, - title: { text: undefined, style: undefined } - } - }; - - this.linegraphOptions = linegraphOptions; - this.linegraphSVG = svg; - this.props = {}; - this.DOMelements = { // dynamic elements - lines: {}, - labels: {}, - title: {} + locales: locales, + locale: 'en', + id: undefined }; - - this.dom = {}; - - this.range = { start: 0, end: 0 }; - this.options = util.extend({}, this.defaultOptions); - this.conversionFactor = 1; - - this.setOptions(options); - this.width = Number(('' + this.options.width).replace('px', '')); - this.minWidth = this.width; - this.height = this.linegraphSVG.offsetHeight; - this.hidden = false; - this.stepPixels = 25; - this.zeroCrossing = -1; - this.amountOfSteps = -1; + if (options && options.time) { + this.customTime = options.time; + } else { + this.customTime = new Date(); + } - this.lineOffset = 0; - this.master = true; - this.svgElements = {}; - this.iconsRemoved = false; + this.eventParams = {}; // stores state parameters while dragging the bar - this.groups = {}; - this.amountOfGroups = 0; + this.setOptions(options); - // create the HTML DOM + // create the DOM this._create(); - - var me = this; - this.body.emitter.on('verticalDrag', function () { - me.dom.lineContainer.style.top = me.body.domProps.scrollTop + 'px'; - }); } - DataAxis.prototype = new Component(); - - DataAxis.prototype.addGroup = function (label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; - } - this.amountOfGroups += 1; - }; - - DataAxis.prototype.updateGroup = function (label, graphOptions) { - this.groups[label] = graphOptions; - }; + CustomTime.prototype = new Component(); - DataAxis.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; + /** + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {number | string} id + * {string} locales + * {string} locale + */ + CustomTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['locale', 'locales', 'id'], this.options, options); } }; - 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', 'left', 'right', 'alignZeros']; - util.selectiveExtend(fields, this.options, options); + /** + * Create the DOM for the custom time + * @private + */ + CustomTime.prototype._create = function () { + var bar = document.createElement('div'); + bar['custom-time'] = this; + bar.className = 'vis-custom-time ' + (this.options.id || ''); + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + this.bar = bar; - this.minWidth = Number(('' + this.options.width).replace('px', '')); + var drag = document.createElement('div'); + drag.style.position = 'relative'; + drag.style.top = '0px'; + drag.style.left = '-10px'; + drag.style.height = '100%'; + drag.style.width = '20px'; + bar.appendChild(drag); - if (redraw === true && this.dom.frame) { - this.hide(); - this.show(); - } - } + // attach event listeners + this.hammer = new Hammer(drag); + this.hammer.on('panstart', this._onDragStart.bind(this)); + this.hammer.on('panmove', this._onDrag.bind(this)); + this.hammer.on('panend', this._onDragEnd.bind(this)); + this.hammer.get('pan').set({ threshold: 5, direction: 30 }); // 30 is ALL_DIRECTIONS in hammer. + // TODO: cleanup + //this.hammer.on('pan', function (event) { + // event.preventDefault(); + //}); }; /** - * Create the HTML DOM for the DataAxis + * Destroy the CustomTime bar */ - 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; + CustomTime.prototype.destroy = function () { + this.hide(); - this.dom.lineContainer = document.createElement('div'); - this.dom.lineContainer.style.width = '100%'; - this.dom.lineContainer.style.height = this.height; - this.dom.lineContainer.style.position = 'relative'; + this.hammer.destroy(); + this.hammer = null; - // 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); + this.body = null; }; - DataAxis.prototype._redrawGroupIcons = function () { - DOMutil.prepareElements(this.svgElements); - - var x; - var iconWidth = this.options.iconWidth; - var iconHeight = 15; - var iconOffset = 4; - var y = iconOffset + 0.5 * iconHeight; - - if (this.options.orientation === 'left') { - x = iconOffset; - } else { - x = this.width - iconWidth - iconOffset; + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + CustomTime.prototype.redraw = function () { + var parent = this.body.dom.backgroundVertical; + if (this.bar.parentNode != parent) { + // attach to the dom + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + parent.appendChild(this.bar); } - var groupArray = Object.keys(this.groups); - groupArray.sort(function (a, b) { - return a < b ? -1 : 1; - }); + var x = this.body.util.toScreen(this.customTime); - for (var i = 0; i < groupArray.length; i++) { - var groupId = groupArray[i]; - 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; + var locale = this.options.locales[this.options.locale]; + if (!locale) { + if (!this.warned) { + console.log('WARNING: options.locales[\'' + this.options.locale + '\'] not found. See http://visjs.org/docs/timeline.html#Localization'); + this.warned = true; } + locale = this.options.locales['en']; // fall back on english when not available } + var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); + title = title.charAt(0).toUpperCase() + title.substring(1); - DOMutil.cleanupElements(this.svgElements); - this.iconsRemoved = false; - }; + this.bar.style.left = x + 'px'; + this.bar.title = title; - DataAxis.prototype._cleanupIcons = function () { - if (this.iconsRemoved === false) { - DOMutil.prepareElements(this.svgElements); - DOMutil.cleanupElements(this.svgElements); - this.iconsRemoved = true; - } + return false; }; /** - * Create the HTML DOM for the DataAxis + * Remove the CustomTime from the DOM */ - DataAxis.prototype.show = function () { - this.hidden = false; - if (!this.dom.frame.parentNode) { - if (this.options.orientation === 'left') { - this.body.dom.left.appendChild(this.dom.frame); - } else { - this.body.dom.right.appendChild(this.dom.frame); - } - } - - if (!this.dom.lineContainer.parentNode) { - this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); + CustomTime.prototype.hide = function () { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); } }; /** - * Create the HTML DOM for the DataAxis + * Set custom time. + * @param {Date | number | string} time */ - DataAxis.prototype.hide = function () { - this.hidden = true; - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } - - if (this.dom.lineContainer.parentNode) { - this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); - } + CustomTime.prototype.setCustomTime = function (time) { + this.customTime = util.convert(time, 'Date'); + this.redraw(); }; /** - * Set a range (start and end) - * @param end - * @param start - * @param end + * Retrieve the current custom time. + * @return {Date} customTime */ - DataAxis.prototype.setRange = function (start, end) { - if (this.master === false && this.options.alignZeros === true && this.zeroCrossing != -1) { - if (start > 0) { - start = 0; - } - } - this.range.start = start; - this.range.end = end; + CustomTime.prototype.getCustomTime = function () { + return new Date(this.customTime.valueOf()); }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Start moving horizontally + * @param {Event} event + * @private */ - DataAxis.prototype.redraw = function () { - var resized = false; - var activeGroups = 0; - - // Make sure the line container adheres to the vertical scrolling. - this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px'; - - 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... - this.dom.lineContainer.style.height = this.height + 'px'; - this.width = this.options.visible === true ? Number(('' + this.options.width).replace('px', '')) : 0; - - var props = this.props; - var frame = this.dom.frame; - - // update classname - frame.className = 'vis-data-axis'; - - // calculate character width and height - this._calculateCharSize(); - - var orientation = this.options.orientation; - var showMinorLabels = this.options.showMinorLabels; - var showMajorLabels = this.options.showMajorLabels; - - // determine the width and height of the elements for the axis - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - - props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset; - props.minorLineHeight = 1; - props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset; - props.majorLineHeight = 1; - - // take frame offline while updating (is almost twice as fast) - if (orientation === 'left') { - frame.style.top = '0'; - frame.style.left = '0'; - frame.style.bottom = ''; - frame.style.width = this.width + 'px'; - frame.style.height = this.height + 'px'; - this.props.width = this.body.domProps.left.width; - this.props.height = this.body.domProps.left.height; - } 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'; - this.props.width = this.body.domProps.right.width; - this.props.height = this.body.domProps.right.height; - } + CustomTime.prototype._onDragStart = function (event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; - resized = this._redrawLabels(); - resized = this._isResized() || resized; + event.stopPropagation(); + }; - if (this.options.icons === true) { - this._redrawGroupIcons(); - } else { - this._cleanupIcons(); - } + /** + * Perform moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; - this._redrawTitle(orientation); - } - return resized; + var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; + var time = this.body.util.toTime(x); + + this.setCustomTime(time); + + // fire a timechange event + this.body.emitter.emit('timechange', { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); + + event.stopPropagation(); }; /** - * Repaint major and minor text labels and vertical grid lines + * Stop moving operating. + * @param {Event} event * @private */ - DataAxis.prototype._redrawLabels = function () { - var resized = false; - DOMutil.prepareElements(this.DOMelements.lines); - DOMutil.prepareElements(this.DOMelements.labels); - var orientation = this.options['orientation']; + CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; - // get the range for the slaved axis - var step; - if (this.master === false) { - var stepSize, rangeStart, rangeEnd, minimumStep; - if (this.zeroCrossing !== -1 && this.options.alignZeros === true) { - if (this.range.end > 0) { - stepSize = this.range.end / this.zeroCrossing; // size of one step - rangeStart = this.range.end - this.amountOfSteps * stepSize; - rangeEnd = this.range.end; - } else { - // all of the range (including start) has to be done before the zero crossing. - stepSize = -1 * this.range.start / (this.amountOfSteps - this.zeroCrossing); // absolute size of a step - rangeStart = this.range.start; - rangeEnd = this.range.start + stepSize * this.amountOfSteps; - } - } else { - rangeStart = this.range.start; - rangeEnd = this.range.end; - } - minimumStep = this.stepPixels; - } else { - // calculate range and step (step such that we have space for 7 characters per label) - minimumStep = this.props.majorCharHeight; - rangeStart = this.range.start; - rangeEnd = this.range.end; - } + // fire a timechanged event + this.body.emitter.emit('timechanged', { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - this.step = step = new DataStep(rangeStart, rangeEnd, minimumStep, this.dom.frame.offsetHeight, this.options[this.options.orientation].range, this.options[this.options.orientation].format, this.master === false && this.options.alignZeros // does the step have to align zeros? only if not master and the options is on - ); + event.stopPropagation(); + }; - // the slave axis needs to use the same horizontal lines as the master axis. - if (this.master === true) { - this.stepPixels = this.dom.frame.offsetHeight / step.marginRange * step.step; - this.amountOfSteps = Math.ceil(this.dom.frame.offsetHeight / this.stepPixels); - } else { - // align with zero - if (this.options.alignZeros === true && this.zeroCrossing !== -1) { - // distance is the amount of steps away from the zero crossing we are. - var distance = (step.current - this.zeroCrossing * step.step) / step.step; - this.step.shift(distance); + /** + * Find a custom time from an event target: + * searches for the attribute 'custom-time' in the event target's element tree + * @param {Event} event + * @return {CustomTime | null} customTime + */ + CustomTime.customTimeFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('custom-time')) { + return target['custom-time']; } + target = target.parentNode; } - // value at the bottom of the SVG - this.valueAtBottom = step.marginEnd; - - this.maxLabelSize = 0; - var y = 0; // init value - var stepIndex = 0; // init value - var isMajor = false; // init value - while (stepIndex < this.amountOfSteps) { - y = Math.round(stepIndex * this.stepPixels); - isMajor = step.isMajor(); - - if (stepIndex > 0 && stepIndex !== this.amountOfSteps) { - if (this.options['showMinorLabels'] && isMajor === false || this.master === false && this.options['showMinorLabels'] === true) { - this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight); - } + return null; + }; - 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, 'vis-y-axis vis-major', this.props.majorCharHeight); - } - this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth); - } else { - this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth); - } - } + module.exports = CustomTime; - // get zero crossing - if (this.master === true && step.current === 0) { - this.zeroCrossing = stepIndex; - } +/***/ }, +/* 51 */ +/***/ function(module, exports, __webpack_require__) { - step.next(); - stepIndex += 1; - } + 'use strict'; - // get zero crossing if it's the last step - if (this.master === true && step.current === 0) { - this.zeroCrossing = stepIndex; - } + Object.defineProperty(exports, '__esModule', { + value: true + }); - this.conversionFactor = this.stepPixels / step.step; + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - // Note that title is rotated, so we're using the height, not width! - var titleWidth = 0; - if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) { - titleWidth = this.props.titleCharHeight; - } - var offset = this.options.icons === true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15; + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - // this will resize the yAxis to accommodate 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(); - resized = 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(); - resized = true; - } else { - DOMutil.cleanupElements(this.DOMelements.lines); - DOMutil.cleanupElements(this.DOMelements.labels); - resized = false; - } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - return resized; - }; + var _ColorPicker = __webpack_require__(52); - DataAxis.prototype.convertValue = function (value) { - var invertedValue = this.valueAtBottom - value; - var convertedValue = invertedValue * this.conversionFactor; - return convertedValue; - }; + var _ColorPicker2 = _interopRequireDefault(_ColorPicker); - DataAxis.prototype.screenToValue = function (x) { - return this.valueAtBottom - x / this.conversionFactor; - }; + var util = __webpack_require__(14); /** - * Create a label for the axis at position x - * @private - * @param y - * @param text - * @param orientation - * @param className - * @param characterHeight + * The way this works is for all properties of this.possible options, you can supply the property name in any form to list the options. + * Boolean options are recognised as Boolean + * Number options should be written as array: [default value, min value, max value, stepsize] + * Colors should be written as array: ['color', '#ffffff'] + * Strings with should be written as array: [option1, option2, option3, ..] + * + * The options are matched with their counterparts in each of the modules and the values used in the configuration are + * + * @param parentModule | the location where parentModule.setOptions() can be called + * @param defaultContainer | the default container of the module + * @param configureOptions | the fully configured and predefined options set found in allOptions.js + * @param pixelRatio | canvas pixel ratio */ - 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'; + var Configurator = (function () { + function Configurator(parentModule, defaultContainer, configureOptions) { + var pixelRatio = arguments[3] === undefined ? 1 : arguments[3]; - text += ''; + _classCallCheck(this, Configurator); - var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); - if (this.maxLabelSize < text.length * largestWidth) { - this.maxLabelSize = text.length * largestWidth; + this.parent = parentModule; + this.changedOptions = []; + this.container = defaultContainer; + this.allowCreation = false; + + this.options = {}; + this.defaultOptions = { + enabled: false, + filter: true, + container: undefined, + showButton: true + }; + util.extend(this.options, this.defaultOptions); + + this.configureOptions = configureOptions; + this.moduleOptions = {}; + this.domElements = []; + this.colorPicker = new _ColorPicker2['default'](pixelRatio); + this.wrapper = undefined; } - }; - /** - * 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 = ''; + _createClass(Configurator, [{ + key: 'setOptions', - if (orientation === 'left') { - line.style.left = this.width - offset + 'px'; - } else { - line.style.right = this.width - offset + 'px'; + /** + * refresh all options. + * Because all modules parse their options by themselves, we just use their options. We copy them here. + * + * @param options + */ + value: function setOptions(options) { + if (options !== undefined) { + var enabled = true; + if (typeof options === 'string') { + this.options.filter = options; + } else if (options instanceof Array) { + this.options.filter = options.join(); + } else if (typeof options === 'object') { + if (options.container !== undefined) { + this.options.container = options.container; + } + if (options.filter !== undefined) { + this.options.filter = options.filter; + } + if (options.showButton !== undefined) { + this.options.showButton = options.showButton; + } + if (options.enabled !== undefined) { + enabled = options.enabled; + } + } else if (typeof options === 'boolean') { + this.options.filter = true; + enabled = options; + } else if (typeof options === 'function') { + this.options.filter = options; + enabled = true; + } + if (this.options.filter === false) { + enabled = false; + } + + this.options.enabled = enabled; + } + this._clean(); + } + }, { + key: 'setModuleOptions', + value: function setModuleOptions(moduleOptions) { + this.moduleOptions = moduleOptions; + if (this.options.enabled === true) { + this._clean(); + if (this.options.container !== undefined) { + this.container = this.options.container; + } + this._create(); + } } + }, { + key: '_create', - line.style.width = width + 'px'; - line.style.top = y + 'px'; - } - }; + /** + * Create all DOM elements + * @private + */ + value: function _create() { + var _this = this; - /** - * Create a title for the axis - * @private - * @param orientation - */ - DataAxis.prototype._redrawTitle = function (orientation) { - DOMutil.prepareElements(this.DOMelements.title); + this._clean(); + this.changedOptions = []; - // Check if the title is defined for this axes - if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) { - var title = DOMutil.getDOMElement('div', this.DOMelements.title, this.dom.frame); - title.className = 'vis-y-axis vis-title vis-' + orientation; - title.innerHTML = this.options[orientation].title.text; + var filter = this.options.filter; + var counter = 0; + var show = false; + for (var option in this.configureOptions) { + if (this.configureOptions.hasOwnProperty(option)) { + this.allowCreation = false; + show = false; + if (typeof filter === 'function') { + show = filter(option, []); + show = show || this._handleObject(this.configureOptions[option], [option], true); + } else if (filter === true || filter.indexOf(option) !== -1) { + show = true; + } - // Add style - if provided - if (this.options[orientation].title.style !== undefined) { - util.addCssText(title, this.options[orientation].title.style); + if (show !== false) { + this.allowCreation = true; + + // linebreak between categories + if (counter > 0) { + this._makeItem([]); + } + // a header for the category + this._makeHeader(option); + + // get the suboptions + this._handleObject(this.configureOptions[option], [option]); + } + counter++; + } + } + + if (this.options.showButton === true) { + (function () { + var generateButton = document.createElement('div'); + generateButton.className = 'vis-network-configuration button'; + generateButton.innerHTML = 'generate options'; + generateButton.onclick = function () { + _this._printOptions(); + }; + generateButton.onmouseover = function () { + generateButton.className = 'vis-network-configuration button hover'; + }; + generateButton.onmouseout = function () { + generateButton.className = 'vis-network-configuration button'; + }; + + _this.optionsContainer = document.createElement('div'); + _this.optionsContainer.className = 'vis-network-configuration vis-option-container'; + + _this.domElements.push(_this.optionsContainer); + _this.domElements.push(generateButton); + })(); + } + + this._push(); + this.colorPicker.insertTo(this.container); } + }, { + key: '_push', - if (orientation === 'left') { - title.style.left = this.props.titleCharHeight + 'px'; - } else { - title.style.right = this.props.titleCharHeight + 'px'; + /** + * draw all DOM elements on the screen + * @private + */ + value: function _push() { + this.wrapper = document.createElement('div'); + this.wrapper.className = 'vis-network-configuration-wrapper'; + this.container.appendChild(this.wrapper); + for (var i = 0; i < this.domElements.length; i++) { + this.wrapper.appendChild(this.domElements[i]); + } } + }, { + key: '_clean', - title.style.width = this.height + 'px'; - } + /** + * delete all DOM elements + * @private + */ + value: function _clean() { + for (var i = 0; i < this.domElements.length; i++) { + this.wrapper.removeChild(this.domElements[i]); + } - // we need to clean up in case we did not use all elements. - DOMutil.cleanupElements(this.DOMelements.title); - }; + if (this.wrapper !== undefined) { + this.container.removeChild(this.wrapper); + this.wrapper = undefined; + } + this.domElements = []; + } + }, { + key: '_getValue', - /** - * Determine the size of text on the axis (both major and minor axis). - * The size is calculated only once and then cached in this.props. - * @private - */ - DataAxis.prototype._calculateCharSize = function () { - // determine the char width and height on the minor axis - if (!('minorCharHeight' in this.props)) { - var textMinor = document.createTextNode('0'); - var measureCharMinor = document.createElement('div'); - measureCharMinor.className = 'vis-y-axis vis-minor vis-measure'; - measureCharMinor.appendChild(textMinor); - this.dom.frame.appendChild(measureCharMinor); + /** + * get the value from the actualOptions if it exists + * @param {array} path | where to look for the actual option + * @returns {*} + * @private + */ + value: function _getValue(path) { + var base = this.moduleOptions; + for (var i = 0; i < path.length; i++) { + if (base[path[i]] !== undefined) { + base = base[path[i]]; + } else { + base = undefined; + break; + } + } + return base; + } + }, { + key: '_makeItem', - this.props.minorCharHeight = measureCharMinor.clientHeight; - this.props.minorCharWidth = measureCharMinor.clientWidth; + /** + * all option elements are wrapped in an item + * @param path + * @param domElements + * @private + */ + value: function _makeItem(path) { + var _this2 = this; - this.dom.frame.removeChild(measureCharMinor); - } + for (var _len = arguments.length, domElements = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + domElements[_key - 1] = arguments[_key]; + } - if (!('majorCharHeight' in this.props)) { - var textMajor = document.createTextNode('0'); - var measureCharMajor = document.createElement('div'); - measureCharMajor.className = 'vis-y-axis vis-major vis-measure'; - measureCharMajor.appendChild(textMajor); - this.dom.frame.appendChild(measureCharMajor); + if (this.allowCreation === true) { + (function () { + var item = document.createElement('div'); + item.className = 'vis-network-configuration item s' + path.length; + domElements.forEach(function (element) { + item.appendChild(element); + }); + _this2.domElements.push(item); + })(); + } + } + }, { + key: '_makeHeader', - this.props.majorCharHeight = measureCharMajor.clientHeight; - this.props.majorCharWidth = measureCharMajor.clientWidth; + /** + * header for major subjects + * @param name + * @private + */ + value: function _makeHeader(name) { + var div = document.createElement('div'); + div.className = 'vis-network-configuration header'; + div.innerHTML = name; + this._makeItem([], div); + } + }, { + key: '_makeLabel', - this.dom.frame.removeChild(measureCharMajor); - } + /** + * make a label, if it is an object label, it gets different styling. + * @param name + * @param path + * @param objectLabel + * @returns {HTMLElement} + * @private + */ + value: function _makeLabel(name, path) { + var objectLabel = arguments[2] === undefined ? false : arguments[2]; - if (!('titleCharHeight' in this.props)) { - var textTitle = document.createTextNode('0'); - var measureCharTitle = document.createElement('div'); - measureCharTitle.className = 'vis-y-axis vis-title vis-measure'; - measureCharTitle.appendChild(textTitle); - this.dom.frame.appendChild(measureCharTitle); + var div = document.createElement('div'); + div.className = 'vis-network-configuration label s' + path.length; + if (objectLabel === true) { + div.innerHTML = '' + name + ':'; + } else { + div.innerHTML = name + ':'; + } + return div; + } + }, { + key: '_makeDropdown', - this.props.titleCharHeight = measureCharTitle.clientHeight; - this.props.titleCharWidth = measureCharTitle.clientWidth; + /** + * make a dropdown list for multiple possible string optoins + * @param arr + * @param value + * @param path + * @private + */ + value: function _makeDropdown(arr, value, path) { + var select = document.createElement('select'); + select.className = 'vis-network-configuration select'; + var selectedValue = 0; + if (value !== undefined) { + if (arr.indexOf(value) !== -1) { + selectedValue = arr.indexOf(value); + } + } - this.dom.frame.removeChild(measureCharTitle); - } - }; + for (var i = 0; i < arr.length; i++) { + var option = document.createElement('option'); + option.value = arr[i]; + if (i === selectedValue) { + option.selected = 'selected'; + } + option.innerHTML = arr[i]; + select.appendChild(option); + } - module.exports = DataAxis; + var me = this; + select.onchange = function () { + me._update(this.value, path); + }; -/***/ }, -/* 53 */ -/***/ function(module, exports, __webpack_require__) { + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, select); + } + }, { + key: '_makeRange', - /** - * @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 - */ - 'use strict'; + /** + * make a range object for numeric options + * @param arr + * @param value + * @param path + * @private + */ + value: function _makeRange(arr, value, path) { + var defaultValue = arr[0]; + var min = arr[1]; + var max = arr[2]; + var step = arr[3]; + var range = document.createElement('input'); + range.className = 'vis-network-configuration range'; + try { + range.type = 'range'; // not supported on IE9 + range.min = min; + range.max = max; + } catch (err) {} + range.step = step; - function DataStep(start, end, minimumStep, containerHeight, customRange, formattingFunction, alignZeros) { - // variables - this.current = 0; + if (value !== undefined) { + if (value < 0 && value * 2 < min) { + range.min = value * 2; + } else if (value * 0.1 < min) { + range.min = value / 10; + } + if (value * 2 > max && max !== 1) { + range.max = value * 2; + } + range.value = value; + } else { + range.value = defaultValue; + } - this.autoScale = true; - this.stepIndex = 0; - this.step = 1; - this.scale = 1; - this.formattingFunction = formattingFunction; + var input = document.createElement('input'); + input.className = 'vis-network-configuration rangeinput'; + input.value = range.value; - this.marginStart; - this.marginEnd; - this.deadSpace = 0; + var me = this; + range.onchange = function () { + input.value = this.value;me._update(Number(this.value), path); + }; + range.oninput = function () { + input.value = this.value; + }; - this.majorSteps = [1, 2, 5, 10]; - this.minorSteps = [0.25, 0.5, 1, 2]; + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, range, input); + } + }, { + key: '_makeCheckbox', - this.alignZeros = alignZeros; + /** + * make a checkbox for boolean options. + * @param defaultValue + * @param value + * @param path + * @private + */ + value: function _makeCheckbox(defaultValue, value, path) { + var checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'vis-network-configuration checkbox'; + checkbox.checked = defaultValue; + if (value !== undefined) { + checkbox.checked = value; + if (value !== defaultValue) { + if (typeof defaultValue === 'object') { + if (value !== defaultValue.enabled) { + this.changedOptions.push({ path: path, value: value }); + } + } else { + this.changedOptions.push({ path: path, value: value }); + } + } + } - this.setRange(start, end, minimumStep, containerHeight, customRange); - } + var me = this; + checkbox.onchange = function () { + me._update(this.checked, path); + }; - /** - * 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 = customRange.min === undefined ? this._start - 0.75 : this._start; - this._end = customRange.max === undefined ? this._end + 1 : this._end;; - } + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, checkbox); + } + }, { + key: '_makeTextInput', - if (this.autoScale === true) { - this.setMinimumStep(minimumStep, containerHeight); - } + /** + * make a text input field for string options. + * @param defaultValue + * @param value + * @param path + * @private + */ + value: function _makeTextInput(defaultValue, value, path) { + var checkbox = document.createElement('input'); + checkbox.type = 'text'; + checkbox.className = 'vis-network-configuration text'; + checkbox.value = value; + if (value !== defaultValue) { + this.changedOptions.push({ path: path, value: value }); + } - this.setFirst(customRange); - }; + var me = this; + checkbox.onchange = function () { + me._update(this.value, path); + }; - /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in pixels - */ - DataStep.prototype.setMinimumStep = function (minimumStep, containerHeight) { - // round to floor - var range = this._end - this._start; - var safeRange = range * 1.2; - var minimumStepValue = minimumStep * (safeRange / containerHeight); - var orderOfMagnitude = Math.round(Math.log(safeRange) / Math.LN10); + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, checkbox); + } + }, { + key: '_makeColorField', - var minorStepIdx = -1; - var magnitudefactor = Math.pow(10, orderOfMagnitude); + /** + * make a color field with a color picker for color fields + * @param arr + * @param value + * @param path + * @private + */ + value: function _makeColorField(arr, value, path) { + var _this3 = this; - var start = 0; - if (orderOfMagnitude < 0) { - start = orderOfMagnitude; - } + var defaultColor = arr[1]; + var div = document.createElement('div'); + value = value === undefined ? defaultColor : value; - 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 (value !== 'none') { + div.className = 'vis-network-configuration colorBlock'; + div.style.backgroundColor = value; + } else { + div.className = 'vis-network-configuration colorBlock none'; } - } - if (solutionFound === true) { - break; - } - } - this.stepIndex = minorStepIdx; - this.scale = magnitudefactor; - this.step = magnitudefactor * this.minorSteps[minorStepIdx]; - }; - /** - * 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 = {}; - } + value = value === undefined ? defaultColor : value; + div.onclick = function () { + _this3._showColorPicker(value, div, path); + }; - 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; + var label = this._makeLabel(path[path.length - 1], path); + this._makeItem(path, label, div); + } + }, { + key: '_showColorPicker', - this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; - this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; + /** + * used by the color buttons to call the color picker. + * @param event + * @param value + * @param div + * @param path + * @private + */ + value: function _showColorPicker(value, div, path) { + var _this4 = this; - // if we need to align the zero's we need to make sure that there is a zero to use. - if (this.alignZeros === true && (this.marginEnd - this.marginStart) % this.step != 0) { - this.marginEnd += this.marginEnd % this.step; - } + var rect = div.getBoundingClientRect(); + var bodyRect = document.body.getBoundingClientRect(); + var pickerX = rect.left + rect.width + 5; + var pickerY = rect.top - bodyRect.top + rect.height * 0.5; + this.colorPicker.show(pickerX, pickerY); + this.colorPicker.setColor(value); + this.colorPicker.setCallback(function (color) { + var colorString = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')'; + div.style.backgroundColor = colorString; + _this4._update(colorString, path); + }); + } + }, { + key: '_handleObject', - this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; - this.marginRange = this.marginEnd - this.marginStart; + /** + * parse an object and draw the correct items + * @param obj + * @param path + * @private + */ + value: function _handleObject(obj) { + var path = arguments[1] === undefined ? [] : arguments[1]; + var checkOnly = arguments[2] === undefined ? false : arguments[2]; - this.current = this.marginEnd; - }; + var show = false; + var filter = this.options.filter; + var visibleInSet = false; + for (var subObj in obj) { + if (obj.hasOwnProperty(subObj)) { + show = true; + var item = obj[subObj]; + var newPath = util.copyAndExtendArray(path, subObj); + if (typeof filter === 'function') { + show = filter(subObj, path); - 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; - } - }; + // if needed we must go deeper into the object. + if (show === false) { + if (!(item instanceof Array) && typeof item !== 'string' && typeof item !== 'boolean' && item instanceof Object) { + this.allowCreation = false; + show = this._handleObject(item, newPath, true); + this.allowCreation = checkOnly === false; + } + } + } - /** - * 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; - }; + if (show !== false) { + visibleInSet = true; + var value = this._getValue(newPath); - /** - * Do the next step - */ - DataStep.prototype.next = function () { - var prev = this.current; - this.current -= this.step; + if (item instanceof Array) { + this._handleArray(item, value, newPath); + } else if (typeof item === 'string') { + this._makeTextInput(item, value, newPath); + } else if (typeof item === 'boolean') { + this._makeCheckbox(item, value, newPath); + } else if (item instanceof Object) { + // collapse the physics options that are not enabled + var draw = true; + if (path.indexOf('physics') !== -1) { + if (this.moduleOptions.physics.solver !== subObj) { + draw = false; + } + } - // safety mechanism: if current time is still unchanged, move to the end - if (this.current === prev) { - this.current = this._end; - } - }; + if (draw === true) { + // initially collapse options with an disabled enabled option. + if (item.enabled !== undefined) { + var enabledPath = util.copyAndExtendArray(newPath, 'enabled'); + var enabledValue = this._getValue(enabledPath); + if (enabledValue === true) { + var label = this._makeLabel(subObj, newPath, true); + this._makeItem(newPath, label); + visibleInSet = this._handleObject(item, newPath) || visibleInSet; + } else { + this._makeCheckbox(item, enabledValue, newPath); + } + } else { + var label = this._makeLabel(subObj, newPath, true); + this._makeItem(newPath, label); + visibleInSet = this._handleObject(item, newPath) || visibleInSet; + } + } + } else { + console.error('dont know how to handle', item, subObj, newPath); + } + } + } + } + return visibleInSet; + } + }, { + key: '_handleArray', - /** - * Do the next step - */ - DataStep.prototype.previous = function () { - this.current += this.step; - this.marginEnd += this.step; - this.marginRange = this.marginEnd - this.marginStart; - }; + /** + * handle the array type of option + * @param optionName + * @param arr + * @param value + * @param path + * @private + */ + value: function _handleArray(arr, value, path) { + if (typeof arr[0] === 'string' && arr[0] === 'color') { + this._makeColorField(arr, value, path); + if (arr[1] !== value) { + this.changedOptions.push({ path: path, value: value }); + } + } else if (typeof arr[0] === 'string') { + this._makeDropdown(arr, value, path); + if (arr[0] !== value) { + this.changedOptions.push({ path: path, value: value }); + } + } else if (typeof arr[0] === 'number') { + this._makeRange(arr, value, path); + if (arr[0] !== value) { + this.changedOptions.push({ path: path, value: Number(value) }); + } + } + } + }, { + key: '_update', - /** - * Get the current datetime - * @return {String} current The current date - */ - DataStep.prototype.getCurrent = function () { - // prevent round-off errors when close to zero - var current = Math.abs(this.current) < this.step / 2 ? 0 : this.current; - var returnValue = current.toPrecision(5); - if (typeof this.formattingFunction === 'function') { - returnValue = this.formattingFunction(current); - } + /** + * called to update the network with the new settings. + * @param value + * @param path + * @private + */ + value: function _update(value, path) { + var options = this._constructOptions(value, path); + this.parent.setOptions(options); + } + }, { + key: '_constructOptions', + value: function _constructOptions(value, path) { + var optionsObj = arguments[2] === undefined ? {} : arguments[2]; - if (typeof returnValue === 'number') { - return '' + returnValue; - } else if (typeof returnValue === 'string') { - return returnValue; - } else { - return current.toPrecision(5); - } - }; + var pointer = optionsObj; - /** - * 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; - }; + // when dropdown boxes can be string or boolean, we typecast it into correct types + value = value === 'true' ? true : value; + value = value === 'false' ? false : value; - DataStep.prototype.shift = function (steps) { - if (steps < 0) { - for (var i = 0; i < -steps; i++) { - this.previous(); + for (var i = 0; i < path.length; i++) { + if (path[i] !== 'global') { + if (pointer[path[i]] === undefined) { + pointer[path[i]] = {}; + } + if (i !== path.length - 1) { + pointer = pointer[path[i]]; + } else { + pointer[path[i]] = value; + } + } + } + return optionsObj; } - } else if (steps > 0) { - for (var i = 0; i < steps; i++) { - this.next(); + }, { + key: '_printOptions', + value: function _printOptions() { + var options = {}; + for (var i = 0; i < this.changedOptions.length; i++) { + this._constructOptions(this.changedOptions[i].value, this.changedOptions[i].path, options); + } + this.optionsContainer.innerHTML = '
var options = ' + JSON.stringify(options, null, 2) + '
'; } - } - }; + }]); - module.exports = DataStep; + return Configurator; + })(); + + exports['default'] = Configurator; + module.exports = exports['default']; /***/ }, -/* 54 */ +/* 52 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(9); - var DOMutil = __webpack_require__(14); - var Line = __webpack_require__(55); - var Bar = __webpack_require__(57); - var Points = __webpack_require__(56); + Object.defineProperty(exports, '__esModule', { + value: true + }); - /** - * /** - * @param {object} group | the object of the group from the dataset - * @param {string} groupId | ID of the group - * @param {object} options | the default options - * @param {array} groupsUsingDefaultStyles | this array has one entree. - * It is passed as an array so it is passed by reference. - * It enumerates through the default styles - * @constructor - */ - function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { - this.id = groupId; - var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation']; - 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; - } + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - /** - * this loads a reference to all items in this group into this group. - * @param {array} items - */ - 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; - }); - } - // typecast all items to numbers. Takes around 10ms for 500.000 items - for (var i = 0; i < this.itemsData.length; i++) { - this.itemsData[i].y = Number(this.itemsData[i].y); - } - } else { - this.itemsData = []; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + var Hammer = __webpack_require__(10); + var hammerUtil = __webpack_require__(36); + var util = __webpack_require__(14); + + var ColorPicker = (function () { + function ColorPicker() { + var pixelRatio = arguments[0] === undefined ? 1 : arguments[0]; + + _classCallCheck(this, ColorPicker); + + this.pixelRatio = pixelRatio; + this.generated = false; + this.centerCoordinates = { x: 289 / 2, y: 289 / 2 }; + this.r = 289 * 0.49; + this.color = { r: 255, g: 255, b: 255, a: 1 }; + this.hueCircle = undefined; + this.initialColor = { r: 255, g: 255, b: 255, a: 1 }; + this.previousColor = undefined; + this.applied = false; + + // bound by + this.updateCallback = function () {}; + + // create all DOM elements + this._create(); } - }; - /** - * this is used for plotting barcharts, this way, we only have to calculate it once. - * @param pos - */ - GraphGroup.prototype.setZeroPosition = function (pos) { - this.zeroPosition = pos; - }; + _createClass(ColorPicker, [{ + key: 'insertTo', - /** - * set the options of the graph group over the default options. - * @param options - */ - GraphGroup.prototype.setOptions = function (options) { - if (options !== undefined) { - var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart']; - util.selectiveDeepExtend(fields, this.options, options); + /** + * this inserts the colorPicker into a div from the DOM + * @param container + */ + value: function insertTo(container) { + if (this.hammer !== undefined) { + this.hammer.destroy(); + this.hammer = undefined; + } + this.container = container; + this.container.appendChild(this.frame); + this._bindHammer(); - // if the group's drawPoints is a function delegate the callback to the onRender property - if (typeof options.drawPoints == 'function') { - options.drawPoints = { - onRender: options.drawPoints - }; + this._setSize(); } + }, { + key: 'setCallback', - util.mergeOptions(this.options, options, 'interpolation'); - util.mergeOptions(this.options, options, 'drawPoints'); - util.mergeOptions(this.options, options, 'shaded'); - - if (options.interpolation) { - if (typeof options.interpolation == 'object') { - if (options.interpolation.parametrization) { - if (options.interpolation.parametrization == 'uniform') { - this.options.interpolation.alpha = 0; - } else if (options.interpolation.parametrization == 'chordal') { - this.options.interpolation.alpha = 1; - } else { - this.options.interpolation.parametrization = 'centripetal'; - this.options.interpolation.alpha = 0.5; - } - } + /** + * the callback is executed on apply and save. Bind it to the application + * @param callback + */ + value: function setCallback(callback) { + if (typeof callback === 'function') { + this.updateCallback = callback; + } else { + throw new Error('Function attempted to set as colorPicker callback is not a function.'); } } - } + }, { + key: '_isColorString', + value: function _isColorString(color) { + var htmlColors = { black: '#000000', navy: '#000080', darkblue: '#00008B', mediumblue: '#0000CD', blue: '#0000FF', darkgreen: '#006400', green: '#008000', teal: '#008080', darkcyan: '#008B8B', deepskyblue: '#00BFFF', darkturquoise: '#00CED1', mediumspringgreen: '#00FA9A', lime: '#00FF00', springgreen: '#00FF7F', aqua: '#00FFFF', cyan: '#00FFFF', midnightblue: '#191970', dodgerblue: '#1E90FF', lightseagreen: '#20B2AA', forestgreen: '#228B22', seagreen: '#2E8B57', darkslategray: '#2F4F4F', limegreen: '#32CD32', mediumseagreen: '#3CB371', turquoise: '#40E0D0', royalblue: '#4169E1', steelblue: '#4682B4', darkslateblue: '#483D8B', mediumturquoise: '#48D1CC', indigo: '#4B0082', darkolivegreen: '#556B2F', cadetblue: '#5F9EA0', cornflowerblue: '#6495ED', mediumaquamarine: '#66CDAA', dimgray: '#696969', slateblue: '#6A5ACD', olivedrab: '#6B8E23', slategray: '#708090', lightslategray: '#778899', mediumslateblue: '#7B68EE', lawngreen: '#7CFC00', chartreuse: '#7FFF00', aquamarine: '#7FFFD4', maroon: '#800000', purple: '#800080', olive: '#808000', gray: '#808080', skyblue: '#87CEEB', lightskyblue: '#87CEFA', blueviolet: '#8A2BE2', darkred: '#8B0000', darkmagenta: '#8B008B', saddlebrown: '#8B4513', darkseagreen: '#8FBC8F', lightgreen: '#90EE90', mediumpurple: '#9370D8', darkviolet: '#9400D3', palegreen: '#98FB98', darkorchid: '#9932CC', yellowgreen: '#9ACD32', sienna: '#A0522D', brown: '#A52A2A', darkgray: '#A9A9A9', lightblue: '#ADD8E6', greenyellow: '#ADFF2F', paleturquoise: '#AFEEEE', lightsteelblue: '#B0C4DE', powderblue: '#B0E0E6', firebrick: '#B22222', darkgoldenrod: '#B8860B', mediumorchid: '#BA55D3', rosybrown: '#BC8F8F', darkkhaki: '#BDB76B', silver: '#C0C0C0', mediumvioletred: '#C71585', indianred: '#CD5C5C', peru: '#CD853F', chocolate: '#D2691E', tan: '#D2B48C', lightgrey: '#D3D3D3', palevioletred: '#D87093', thistle: '#D8BFD8', orchid: '#DA70D6', goldenrod: '#DAA520', crimson: '#DC143C', gainsboro: '#DCDCDC', plum: '#DDA0DD', burlywood: '#DEB887', lightcyan: '#E0FFFF', lavender: '#E6E6FA', darksalmon: '#E9967A', violet: '#EE82EE', palegoldenrod: '#EEE8AA', lightcoral: '#F08080', khaki: '#F0E68C', aliceblue: '#F0F8FF', honeydew: '#F0FFF0', azure: '#F0FFFF', sandybrown: '#F4A460', wheat: '#F5DEB3', beige: '#F5F5DC', whitesmoke: '#F5F5F5', mintcream: '#F5FFFA', ghostwhite: '#F8F8FF', salmon: '#FA8072', antiquewhite: '#FAEBD7', linen: '#FAF0E6', lightgoldenrodyellow: '#FAFAD2', oldlace: '#FDF5E6', red: '#FF0000', fuchsia: '#FF00FF', magenta: '#FF00FF', deeppink: '#FF1493', orangered: '#FF4500', tomato: '#FF6347', hotpink: '#FF69B4', coral: '#FF7F50', darkorange: '#FF8C00', lightsalmon: '#FFA07A', orange: '#FFA500', lightpink: '#FFB6C1', pink: '#FFC0CB', gold: '#FFD700', peachpuff: '#FFDAB9', navajowhite: '#FFDEAD', moccasin: '#FFE4B5', bisque: '#FFE4C4', mistyrose: '#FFE4E1', blanchedalmond: '#FFEBCD', papayawhip: '#FFEFD5', lavenderblush: '#FFF0F5', seashell: '#FFF5EE', cornsilk: '#FFF8DC', lemonchiffon: '#FFFACD', floralwhite: '#FFFAF0', snow: '#FFFAFA', yellow: '#FFFF00', lightyellow: '#FFFFE0', ivory: '#FFFFF0', white: '#FFFFFF' }; + if (typeof color === 'string') { + return htmlColors[color]; + } + } + }, { + key: 'setColor', - if (this.options.style == 'line') { - this.type = new Line(this.id, this.options); - } else if (this.options.style == 'bar') { - this.type = new Bar(this.id, this.options); - } else if (this.options.style == 'points') { - this.type = new Points(this.id, this.options); - } - }; + /** + * Set the color of the colorPicker + * Supported formats: + * 'red' --> HTML color string + * '#ffffff' --> hex string + * 'rbg(255,255,255)' --> rgb string + * 'rgba(255,255,255,1.0)' --> rgba string + * {r:255,g:255,b:255} --> rgb object + * {r:255,g:255,b:255,a:1.0} --> rgba object + * @param color + * @param setInitial + */ + value: function setColor(color) { + var setInitial = arguments[1] === undefined ? true : arguments[1]; - /** - * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph - * @param group - */ - GraphGroup.prototype.update = function (group) { - this.group = group; - this.content = group.content || 'graph'; - this.className = group.className || this.className || 'vis-graph-group' + this.groupsUsingDefaultStyles[0] % 10; - this.visible = group.visible === undefined ? true : group.visible; - this.style = group.style; - this.setOptions(group.options); - }; + if (color === 'none') { + return; + } - /** - * draw the icon for the legend. - * - * @param x - * @param y - * @param JSONcontainer - * @param SVGcontainer - * @param iconWidth - * @param iconHeight - */ - GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { - var fillHeight = iconHeight * 0.5; - var path, fillPath; + var rgba = undefined; - 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', 'vis-outline'); + // if a html color shorthand is used, convert to hex + var htmlColor = this._isColorString(color); + if (htmlColor !== undefined) { + color = htmlColor; + } - if (this.options.style == 'line') { - path = DOMutil.getSVGElement('path', JSONcontainer, SVGcontainer); - path.setAttributeNS(null, 'class', this.className); - if (this.style !== undefined) { - path.setAttributeNS(null, 'style', this.style); - } + // check format + if (util.isString(color) === true) { + if (util.isValidRGB(color) === true) { + var rgbaArray = color.substr(4).substr(0, color.length - 5).split(','); + rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: 1 }; + } else if (util.isValidRGBA(color) === true) { + var rgbaArray = color.substr(5).substr(0, color.length - 6).split(','); + rgba = { r: rgbaArray[0], g: rgbaArray[1], b: rgbaArray[2], a: rgbaArray[3] }; + } else if (util.isValidHex(color) === true) { + var rgbObj = util.hexToRGB(color); + rgba = { r: rgbObj.r, g: rgbObj.g, b: rgbObj.b, a: 1 }; + } + } else { + if (color instanceof Object) { + if (color.r !== undefined && color.g !== undefined && color.b !== undefined) { + var alpha = color.a !== undefined ? color.a : '1.0'; + rgba = { r: color.r, g: color.g, b: color.b, a: alpha }; + } + } + } - 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)); + // set color + if (rgba === undefined) { + throw new Error('Unknown color passed to the colorPicker. Supported are strings: rgb, hex, rgba. Object: rgb ({r:r,g:g,b:b,[a:a]}). Supplied: ' + JSON.stringify(color)); } else { - fillPath.setAttributeNS(null, 'd', 'M' + x + ',' + y + ' ' + 'L' + x + ',' + (y + fillHeight) + ' ' + 'L' + (x + iconWidth) + ',' + (y + fillHeight) + 'L' + (x + iconWidth) + ',' + y); + this._setColor(rgba, setInitial); } - fillPath.setAttributeNS(null, 'class', this.className + ' vis-icon-fill'); } + }, { + key: 'show', - if (this.options.drawPoints.enabled == true) { - var groupTemplate = { - style: this.options.drawPoints.style, - size: this.options.drawPoints.size, - className: this.className - }; - DOMutil.drawPoint(x + 0.5 * iconWidth, y, groupTemplate, JSONcontainer, SVGcontainer); + /** + * this shows the color picker at a location. The hue circle is constructed once and stored. + * @param x + * @param y + */ + value: function show(x, y) { + this.applied = false; + this.frame.style.display = 'block'; + this.frame.style.top = y + 'px'; + this.frame.style.left = x + 'px'; + this._generateHueCircle(); } - } else { - var barWidth = Math.round(0.3 * iconWidth); - var bar1Height = Math.round(0.4 * iconHeight); - var bar2Height = Math.round(0.75 * iconHeight); + }, { + key: '_hide', - var offset = Math.round((iconWidth - 2 * barWidth) / 3); + // ------------------------------------------ PRIVATE ----------------------------- // - DOMutil.drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style); - DOMutil.drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style); - } - }; + /** + * Hide the picker. Is called by the cancel button. + * Optional boolean to store the previous color for easy access later on. + * @param storePrevious + * @private + */ + value: function _hide() { + var storePrevious = arguments[0] === undefined ? true : arguments[0]; - /** - * return the legend entree for this group. - * - * @param iconWidth - * @param iconHeight - * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} - */ - 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 }; - }; + // store the previous color for next time; + if (storePrevious === true) { + this.previousColor = util.extend({}, this.color); + } - GraphGroup.prototype.getYRange = function (groupData) { - return this.type.getYRange(groupData); - }; + if (this.applied === true) { + this.updateCallback(this.initialColor); + } - GraphGroup.prototype.getData = function (groupData) { - return this.type.getData(groupData); - }; + this.frame.style.display = 'none'; + } + }, { + key: '_save', - GraphGroup.prototype.draw = function (dataset, group, framework) { - this.type.draw(dataset, group, framework); - }; + /** + * bound to the save button. Saves and hides. + * @private + */ + value: function _save() { + this.updateCallback(this.color); + this.applied = false; + this._hide(); + } + }, { + key: '_apply', - module.exports = GraphGroup; + /** + * Bound to apply button. Saves but does not close. Is undone by the cancel button. + * @private + */ + value: function _apply() { + this.applied = true; + this.updateCallback(this.color); + this._updatePicker(this.color); + } + }, { + key: '_loadLast', -/***/ }, -/* 55 */ -/***/ function(module, exports, __webpack_require__) { + /** + * load the color from the previous session. + * @private + */ + value: function _loadLast() { + if (this.previousColor !== undefined) { + this.setColor(this.previousColor, false); + } else { + alert('There is no last color to load...'); + } + } + }, { + key: '_setColor', - 'use strict'; + /** + * set the color, place the picker + * @param rgba + * @param setInitial + * @private + */ + value: function _setColor(rgba) { + var setInitial = arguments[1] === undefined ? true : arguments[1]; - var DOMutil = __webpack_require__(14); - var Points = __webpack_require__(56); + // store the initial color + if (setInitial === true) { + this.initialColor = util.extend({}, rgba); + } - function Line(groupId, options) { - this.groupId = groupId; - this.options = options; - } + this.color = rgba; + var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); - Line.prototype.getData = function (groupData) { - var combinedData = []; - for (var j = 0; j < groupData.length; j++) { - combinedData.push({ - x: groupData[j].x, - y: groupData[j].y, - groupId: this.groupId - }); - } - return combinedData; - }; + var angleConvert = 2 * Math.PI; + var radius = this.r * hsv.s; + var x = this.centerCoordinates.x + radius * Math.sin(angleConvert * hsv.h); + var y = this.centerCoordinates.y + radius * Math.cos(angleConvert * hsv.h); - Line.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + this.colorPickerSelector.style.left = x - 0.5 * this.colorPickerSelector.clientWidth + 'px'; + this.colorPickerSelector.style.top = y - 0.5 * this.colorPickerSelector.clientHeight + 'px'; - Line.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { - if (combinedData.length > 0) { - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x === b.x) { - return a.groupId < b.groupId ? -1 : 1; - } else { - return a.x - b.x; + this._updatePicker(rgba); + } + }, { + key: '_setOpacity', + + /** + * bound to opacity control + * @param value + * @private + */ + value: function _setOpacity(value) { + this.color.a = value / 100; + this._updatePicker(this.color); + } + }, { + key: '_setBrightness', + + /** + * bound to brightness control + * @param value + * @private + */ + value: function _setBrightness(value) { + var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); + hsv.v = value / 100; + var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); + rgba['a'] = this.color.a; + this.color = rgba; + this._updatePicker(); + } + }, { + key: '_updatePicker', + + /** + * update the colorpicker. A black circle overlays the hue circle to mimic the brightness decreasing. + * @param rgba + * @private + */ + value: function _updatePicker() { + var rgba = arguments[0] === undefined ? this.color : arguments[0]; + + var hsv = util.RGBToHSV(rgba.r, rgba.g, rgba.b); + var ctx = this.colorPickerCanvas.getContext('2d'); + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); } - }); - var intersections = {}; + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - Line._getDataIntersections(intersections, combinedData); - groupRanges[groupLabel] = Line._getStackedYRange(intersections, combinedData); - groupRanges[groupLabel].yAxisOrientation = orientation; - groupIds.push(groupLabel); - } - }; + // clear the canvas + var w = this.colorPickerCanvas.clientWidth; + var h = this.colorPickerCanvas.clientHeight; + ctx.clearRect(0, 0, w, h); - Line._getStackedYRange = 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 { - if (combinedData[i].y < 0) { - intersections[key].accumulatedNegative += combinedData[i].y; - } else { - intersections[key].accumulatedPositive += combinedData[i].y; - } - } - } - for (var xpos in intersections) { - if (intersections.hasOwnProperty(xpos)) { - yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; - yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; - yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; - yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; - } - } + ctx.putImageData(this.hueCircle, 0, 0); + ctx.fillStyle = 'rgba(0,0,0,' + (1 - hsv.v) + ')'; + ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); + ctx.fill(); - return { min: yMin, max: yMax }; - }; + this.brightnessRange.value = 100 * hsv.v; + this.opacityRange.value = 100 * rgba.a; - /** - * Fill the intersections object with counters of how many datapoints share the same x coordinates - * @param intersections - * @param combinedData - * @private - */ - Line._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)); + this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; + this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; } - if (coreDistance === 0) { - if (intersections[combinedData[i].x] === undefined) { - intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; - } - intersections[combinedData[i].x].amount += 1; + }, { + key: '_setSize', + + /** + * used by create to set the size of the canvas. + * @private + */ + value: function _setSize() { + this.colorPickerCanvas.style.width = '100%'; + this.colorPickerCanvas.style.height = '100%'; + + this.colorPickerCanvas.width = 289 * this.pixelRatio; + this.colorPickerCanvas.height = 289 * this.pixelRatio; } - } - }; + }, { + key: '_create', - /** - * draw a line graph - * - * @param dataset - * @param group - */ - Line.prototype.draw = function (dataset, group, framework) { - if (dataset != null) { - if (dataset.length > 0) { - var path, d; - var svgHeight = Number(framework.svg.style.height.replace('px', '')); - path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); - path.setAttributeNS(null, 'class', group.className); - if (group.style !== undefined) { - path.setAttributeNS(null, 'style', group.style); - } + /** + * create all dom elements + * TODO: cleanup, lots of similar dom elements + * @private + */ + value: function _create() { + this.frame = document.createElement('div'); + this.frame.className = 'vis-color-picker'; - // construct path from dataset - if (group.options.interpolation.enabled == true) { - d = Line._catmullRom(dataset, group); + this.colorPickerDiv = document.createElement('div'); + this.colorPickerSelector = document.createElement('div'); + this.colorPickerSelector.className = 'vis-selector'; + this.colorPickerDiv.appendChild(this.colorPickerSelector); + + this.colorPickerCanvas = document.createElement('canvas'); + this.colorPickerDiv.appendChild(this.colorPickerCanvas); + + if (!this.colorPickerCanvas.getContext) { + var noCanvas = document.createElement('DIV'); + noCanvas.style.color = 'red'; + noCanvas.style.fontWeight = 'bold'; + noCanvas.style.padding = '10px'; + noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; + this.colorPickerCanvas.appendChild(noCanvas); } else { - d = Line._linear(dataset); - } + var ctx = this.colorPickerCanvas.getContext('2d'); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - // append with points for fill and finalize the path - if (group.options.shaded.enabled == true) { - var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.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 + ' vis-fill'); - if (group.options.shaded.style !== undefined) { - fillPath.setAttributeNS(null, 'style', group.options.shaded.style); - } - fillPath.setAttributeNS(null, 'd', dFill); + this.colorPickerCanvas.getContext('2d').setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); } - // copy properties to path for drawing. - path.setAttributeNS(null, 'd', 'M' + d); - // draw points - if (group.options.drawPoints.enabled == true) { - Points.draw(dataset, group, framework); - } - } - } - }; + this.colorPickerDiv.className = 'vis-color'; - /** - * This uses an uniform parametrization of the interpolation algorithm: - * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al. - * @param data - * @returns {string} - * @private - */ - Line._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++) { + this.opacityDiv = document.createElement('div'); + this.opacityDiv.className = 'vis-opacity'; - p0 = i == 0 ? data[0] : data[i - 1]; - p1 = data[i]; - p2 = data[i + 1]; - p3 = i + 2 < length ? data[i + 2] : p2; + this.brightnessDiv = document.createElement('div'); + this.brightnessDiv.className = 'vis-brightness'; - // 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 + this.arrowDiv = document.createElement('div'); + this.arrowDiv.className = 'vis-arrow'; - // 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 }; + this.opacityRange = document.createElement('input'); + try { + this.opacityRange.type = 'range'; // Not supported on IE9 + this.opacityRange.min = '0'; + this.opacityRange.max = '100'; + } catch (err) {} + this.opacityRange.value = '100'; + this.opacityRange.className = 'vis-range'; - d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; - } + this.brightnessRange = document.createElement('input'); + try { + this.brightnessRange.type = 'range'; // Not supported on IE9 + this.brightnessRange.min = '0'; + this.brightnessRange.max = '100'; + } catch (err) {} + this.brightnessRange.value = '100'; + this.brightnessRange.className = 'vis-range'; - return d; - }; + this.opacityDiv.appendChild(this.opacityRange); + this.brightnessDiv.appendChild(this.brightnessRange); - /** - * 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 - * @param group - * @returns {string} - * @private - */ - Line._catmullRom = function (data, group) { - var alpha = group.options.interpolation.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++) { + var me = this; + this.opacityRange.onchange = function () { + me._setOpacity(this.value); + }; + this.opacityRange.oninput = function () { + me._setOpacity(this.value); + }; + this.brightnessRange.onchange = function () { + me._setBrightness(this.value); + }; + this.brightnessRange.oninput = function () { + me._setBrightness(this.value); + }; - p0 = i == 0 ? data[0] : data[i - 1]; - p1 = data[i]; - p2 = data[i + 1]; - p3 = i + 2 < length ? data[i + 2] : p2; + this.brightnessLabel = document.createElement('div'); + this.brightnessLabel.className = 'vis-label vis-brightness'; + this.brightnessLabel.innerHTML = 'brightness:'; - 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)); + this.opacityLabel = document.createElement('div'); + this.opacityLabel.className = 'vis-label vis-opacity'; + this.opacityLabel.innerHTML = 'opacity:'; - // Catmull-Rom to Cubic Bezier conversion matrix + this.newColorDiv = document.createElement('div'); + this.newColorDiv.className = 'vis-new-color'; + this.newColorDiv.innerHTML = 'new'; - // A = 2d1^2a + 3d1^a * d2^a + d3^2a - // B = 2d3^2a + 3d3^a * d2^a + d2^2a + this.initialColorDiv = document.createElement('div'); + this.initialColorDiv.className = 'vis-initial-color'; + this.initialColorDiv.innerHTML = 'initial'; - // [ 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 ] + this.cancelButton = document.createElement('div'); + this.cancelButton.className = 'vis-button vis-cancel'; + this.cancelButton.innerHTML = 'cancel'; + this.cancelButton.onclick = this._hide.bind(this, false); - 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); + this.applyButton = document.createElement('div'); + this.applyButton.className = 'vis-button vis-apply'; + this.applyButton.innerHTML = 'apply'; + this.applyButton.onclick = this._apply.bind(this); - 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; - } + this.saveButton = document.createElement('div'); + this.saveButton.className = 'vis-button vis-save'; + this.saveButton.innerHTML = 'save'; + this.saveButton.onclick = this._save.bind(this); - bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, - y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + this.loadButton = document.createElement('div'); + this.loadButton.className = 'vis-button vis-load'; + this.loadButton.innerHTML = 'load last'; + this.loadButton.onclick = this._loadLast.bind(this); - bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, - y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; + this.frame.appendChild(this.colorPickerDiv); + this.frame.appendChild(this.arrowDiv); + this.frame.appendChild(this.brightnessLabel); + this.frame.appendChild(this.brightnessDiv); + this.frame.appendChild(this.opacityLabel); + this.frame.appendChild(this.opacityDiv); + this.frame.appendChild(this.newColorDiv); + this.frame.appendChild(this.initialColorDiv); - 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 + ' '; + this.frame.appendChild(this.cancelButton); + this.frame.appendChild(this.applyButton); + this.frame.appendChild(this.saveButton); + this.frame.appendChild(this.loadButton); } + }, { + key: '_bindHammer', - return d; - } - }; + /** + * bind hammer to the color picker + * @private + */ + value: function _bindHammer() { + var _this = this; - /** - * this generates the SVG path for a linear drawing between datapoints. - * @param data - * @returns {string} - * @private - */ - Line._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; + this.drag = {}; + this.pinch = {}; + this.hammer = new Hammer(this.colorPickerCanvas); + this.hammer.get('pinch').set({ enable: true }); + + hammerUtil.onTouch(this.hammer, function (event) { + _this._moveSelector(event); + }); + this.hammer.on('tap', function (event) { + _this._moveSelector(event); + }); + this.hammer.on('panstart', function (event) { + _this._moveSelector(event); + }); + this.hammer.on('panmove', function (event) { + _this._moveSelector(event); + }); + this.hammer.on('panend', function (event) { + _this._moveSelector(event); + }); } - } - return d; - }; + }, { + key: '_generateHueCircle', - module.exports = Line; + /** + * generate the hue circle. This is relatively heavy (200ms) and is done only once on the first time it is shown. + * @private + */ + value: function _generateHueCircle() { + if (this.generated === false) { + var ctx = this.colorPickerCanvas.getContext('2d'); + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + } + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); -/***/ }, -/* 56 */ -/***/ function(module, exports, __webpack_require__) { + // clear the canvas + var w = this.colorPickerCanvas.clientWidth; + var h = this.colorPickerCanvas.clientHeight; + ctx.clearRect(0, 0, w, h); - 'use strict'; + // draw hue circle + var x = undefined, + y = undefined, + hue = undefined, + sat = undefined; + this.centerCoordinates = { x: w * 0.5, y: h * 0.5 }; + this.r = 0.49 * w; + var angleConvert = 2 * Math.PI / 360; + var hfac = 1 / 360; + var sfac = 1 / this.r; + var rgb = undefined; + for (hue = 0; hue < 360; hue++) { + for (sat = 0; sat < this.r; sat++) { + x = this.centerCoordinates.x + sat * Math.sin(angleConvert * hue); + y = this.centerCoordinates.y + sat * Math.cos(angleConvert * hue); + rgb = util.HSVToRGB(hue * hfac, sat * sfac, 1); + ctx.fillStyle = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')'; + ctx.fillRect(x - 0.5, y - 0.5, 2, 2); + } + } + ctx.strokeStyle = 'rgba(0,0,0,1)'; + ctx.circle(this.centerCoordinates.x, this.centerCoordinates.y, this.r); + ctx.stroke(); - var DOMutil = __webpack_require__(14); + this.hueCircle = ctx.getImageData(0, 0, w, h); + } + this.generated = true; + } + }, { + key: '_moveSelector', - function Points(groupId, options) { - this.groupId = groupId; - this.options = options; - } + /** + * move the selector. This is called by hammer functions. + * + * @param event + * @private + */ + value: function _moveSelector(event) { + var rect = this.colorPickerDiv.getBoundingClientRect(); + var left = event.center.x - rect.left; + var top = event.center.y - rect.top; - Points.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + var centerY = 0.5 * this.colorPickerDiv.clientHeight; + var centerX = 0.5 * this.colorPickerDiv.clientWidth; - Points.prototype.draw = function (dataset, group, framework, offset) { - Points.draw(dataset, group, framework, offset); - }; + var x = left - centerX; + var y = top - centerY; - /** - * draw the data points - * - * @param {Array} dataset - * @param {Object} JSONcontainer - * @param {Object} svg | SVG DOM element - * @param {GraphGroup} group - * @param {Number} [offset] - */ - Points.draw = function (dataset, group, framework, offset) { - offset = offset || 0; - var callback = getCallback(); + var angle = Math.atan2(x, y); + var radius = 0.98 * Math.min(Math.sqrt(x * x + y * y), centerX); - for (var i = 0; i < dataset.length; i++) { - if (!callback) { - // draw the point the simple way. - DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(), framework.svgElements, framework.svg, dataset[i].label); - } else { - var callbackResult = callback(dataset[i], group, framework); // result might be true, false or an object - if (callbackResult === true || typeof callbackResult === 'object') { - DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(callbackResult), framework.svgElements, framework.svg, dataset[i].label); - } - } - } + var newTop = Math.cos(angle) * radius + centerY; + var newLeft = Math.sin(angle) * radius + centerX; - function getGroupTemplate(callbackResult) { - callbackResult = typeof callbackResult === 'undefined' ? {} : callbackResult; - return { - style: callbackResult.style || group.options.drawPoints.style, - size: callbackResult.size || group.options.drawPoints.size, - className: callbackResult.className || group.className - }; - } + this.colorPickerSelector.style.top = newTop - 0.5 * this.colorPickerSelector.clientHeight + 'px'; + this.colorPickerSelector.style.left = newLeft - 0.5 * this.colorPickerSelector.clientWidth + 'px'; - function getCallback() { - var callback = undefined; - // check for the graph2d onRender - if (framework.options.drawPoints.onRender && typeof framework.options.drawPoints.onRender == 'function') { - callback = framework.options.drawPoints.onRender; - } + // set color + var h = angle / (2 * Math.PI); + h = h < 0 ? h + 1 : h; + var s = radius / this.r; + var hsv = util.RGBToHSV(this.color.r, this.color.g, this.color.b); + hsv.h = h; + hsv.s = s; + var rgba = util.HSVToRGB(hsv.h, hsv.s, hsv.v); + rgba['a'] = this.color.a; + this.color = rgba; - // override it with the group onRender if defined - if (group.group.options && group.group.options.drawPoints && group.group.options.drawPoints.onRender && typeof group.group.options.drawPoints.onRender == 'function') { - callback = group.group.options.drawPoints.onRender; + // update previews + this.initialColorDiv.style.backgroundColor = 'rgba(' + this.initialColor.r + ',' + this.initialColor.g + ',' + this.initialColor.b + ',' + this.initialColor.a + ')'; + this.newColorDiv.style.backgroundColor = 'rgba(' + this.color.r + ',' + this.color.g + ',' + this.color.b + ',' + this.color.a + ')'; } + }]); - return callback; - } - }; + return ColorPicker; + })(); - module.exports = Points; + exports['default'] = ColorPicker; + module.exports = exports['default']; /***/ }, -/* 57 */ +/* 53 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var DOMutil = __webpack_require__(14); - var Points = __webpack_require__(56); + Object.defineProperty(exports, '__esModule', { + value: true + }); - function Bargraph(groupId, options) { - this.groupId = groupId; - this.options = options; - } + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - Bargraph.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - Bargraph.prototype.getData = function (groupData) { - var combinedData = []; - for (var j = 0; j < groupData.length; j++) { - combinedData.push({ - x: groupData[j].x, - y: groupData[j].y, - groupId: this.groupId - }); - } - return combinedData; - }; + var util = __webpack_require__(14); + var errorFound = false; + var allOptions = undefined; + var printStyle = 'background: #FFeeee; color: #dd0000'; /** - * draw a bar graph - * - * @param groupIds - * @param processedGroupData + * Used to validate options. */ - Bargraph.draw = function (groupIds, processedGroupData, framework) { - var combinedData = []; - var intersections = {}; - var coreDistance; - var key, drawData; - var group; - var i, j; - var barPoints = 0; - // combine all barchart data - for (i = 0; i < groupIds.length; i++) { - group = framework.groups[groupIds[i]]; - if (group.options.style === 'bar') { - if (group.visible === true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.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], - label: processedGroupData[groupIds[i]][j].label - }); - barPoints += 1; - } - } - } + var Validator = (function () { + function Validator() { + _classCallCheck(this, Validator); } - if (barPoints === 0) { - return; - } + _createClass(Validator, null, [{ + key: 'validate', - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x === b.x) { - return a.groupId < b.groupId ? -1 : 1; - } else { - return a.x - b.x; + /** + * Main function to be called + * @param options + * @param subObject + * @returns {boolean} + */ + value: function validate(options, referenceOptions, subObject) { + errorFound = false; + allOptions = referenceOptions; + var usedOptions = referenceOptions; + if (subObject !== undefined) { + usedOptions = referenceOptions[subObject]; + } + Validator.parse(options, usedOptions, []); + return errorFound; } - }); - - // get intersections - Bargraph._getDataIntersections(intersections, combinedData); + }, { + key: 'parse', - // plot barchart - for (i = 0; i < combinedData.length; i++) { - group = framework.groups[combinedData[i].groupId]; - var minWidth = 0.1 * group.options.barChart.width; + /** + * Will traverse an object recursively and check every value + * @param options + * @param referenceOptions + * @param path + */ + value: function parse(options, referenceOptions, path) { + for (var option in options) { + if (options.hasOwnProperty(option)) { + Validator.check(option, options, referenceOptions, path); + } + } + } + }, { + key: 'check', - key = combinedData[i].x; - var heightOffset = 0; - if (intersections[key] === undefined) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - key); + /** + * Check every value. If the value is an object, call the parse function on that object. + * @param option + * @param options + * @param referenceOptions + * @param path + */ + value: function check(option, options, referenceOptions, path) { + if (referenceOptions[option] === undefined && referenceOptions.__any__ === undefined) { + Validator.getSuggestion(option, referenceOptions, path); + } else if (referenceOptions[option] === undefined && referenceOptions.__any__ !== undefined) { + // __any__ is a wildcard. Any value is accepted and will be further analysed by reference. + if (Validator.getType(options[option]) === 'object' && referenceOptions['__any__'].__type__ !== undefined) { + // if the any subgroup is not a predefined object int he configurator we do not look deeper into the object. + Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'].__type__, path); + } else { + Validator.checkFields(option, options, referenceOptions, '__any__', referenceOptions['__any__'], path); + } + } else { + // Since all options in the reference are objects, we can check whether they are supposed to be object to look for the __type__ field. + if (referenceOptions[option].__type__ !== undefined) { + // if this should be an object, we check if the correct type has been supplied to account for shorthand options. + Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option].__type__, path); + } else { + Validator.checkFields(option, options, referenceOptions, option, referenceOptions[option], path); + } } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); + } + }, { + key: 'checkFields', + + /** + * + * @param {String} option | the option property + * @param {Object} options | The supplied options object + * @param {Object} referenceOptions | The reference options containing all options and their allowed formats + * @param {String} referenceOption | Usually this is the same as option, except when handling an __any__ tag. + * @param {String} refOptionType | This is the type object from the reference options + * @param {Array} path | where in the object is the option + */ + value: function checkFields(option, options, referenceOptions, referenceOption, refOptionObj, path) { + var optionType = Validator.getType(options[option]); + var refOptionType = refOptionObj[optionType]; + if (refOptionType !== undefined) { + // if the type is correct, we check if it is supposed to be one of a few select values + if (Validator.getType(refOptionType) === 'array') { + if (refOptionType.indexOf(options[option]) === -1) { + console.log('%cInvalid option detected in "' + option + '".' + ' Allowed values are:' + Validator.print(refOptionType) + ' not "' + options[option] + '". ' + Validator.printLocation(path, option), printStyle); + errorFound = true; + } else if (optionType === 'object' && referenceOption !== '__any__') { + path = util.copyAndExtendArray(path, option); + Validator.parse(options[option], referenceOptions[referenceOption], path); + } + } else if (optionType === 'object' && referenceOption !== '__any__') { + path = util.copyAndExtendArray(path, option); + Validator.parse(options[option], referenceOptions[referenceOption], path); + } + } else if (refOptionObj['any'] === undefined) { + // type of the field is incorrect and the field cannot be any + console.log('%cInvalid type received for "' + option + '". Expected: ' + Validator.print(Object.keys(refOptionObj)) + '. Received [' + optionType + '] "' + options[option] + '"' + Validator.printLocation(path, option), printStyle); + errorFound = true; } - drawData = Bargraph._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); + } + }, { + key: 'getType', + value: function getType(object) { + var type = typeof object; + + if (type === 'object') { + if (object === null) { + return 'null'; + } + if (object instanceof Boolean) { + return 'boolean'; + } + if (object instanceof Number) { + return 'number'; + } + if (object instanceof String) { + return 'string'; + } + if (Array.isArray(object)) { + return 'array'; + } + if (object instanceof Date) { + return 'date'; + } + if (object.nodeType !== undefined) { + return 'dom'; + } + if (object._isAMomentObject === true) { + return 'moment'; + } + return 'object'; + } else if (type === 'number') { + return 'number'; + } else if (type === 'boolean') { + return 'boolean'; + } else if (type === 'string') { + return 'string'; + } else if (type === undefined) { + return 'undefined'; } - if (prevKey > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + return type; + } + }, { + key: 'getSuggestion', + value: function getSuggestion(option, options, path) { + var localSearch = Validator.findInOptions(option, options, path, false); + var globalSearch = Validator.findInOptions(option, allOptions, [], true); + + var localSearchThreshold = 8; + var globalSearchThreshold = 4; + + if (localSearch.indexMatch !== undefined) { + console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was incomplete? Did you mean: "' + localSearch.indexMatch + '"?\n\n', printStyle); + } else if (globalSearch.distance <= globalSearchThreshold && localSearch.distance > globalSearch.distance) { + console.log('%cUnknown option detected: "' + option + '" in ' + Validator.printLocation(localSearch.path, option, '') + 'Perhaps it was misplaced? Matching option found at: ' + Validator.printLocation(globalSearch.path, globalSearch.closestMatch, ''), printStyle); + } else if (localSearch.distance <= localSearchThreshold) { + console.log('%cUnknown option detected: "' + option + '". Did you mean "' + localSearch.closestMatch + '"?' + Validator.printLocation(localSearch.path, option), printStyle); + } else { + console.log('%cUnknown option detected: "' + option + '". Did you mean one of these: ' + Validator.print(Object.keys(options)) + Validator.printLocation(path, option), printStyle); } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - intersections[key].resolved += 1; - if (group.options.stack === true) { - if (combinedData[i].y < group.zeroPosition) { - heightOffset = intersections[key].accumulatedNegative; - intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].y; + errorFound = true; + } + }, { + key: 'findInOptions', + + /** + * traverse the options in search for a match. + * @param option + * @param options + * @param path + * @param recursive + * @returns {{closestMatch: string, path: Array, distance: number}} + */ + value: function findInOptions(option, options, path) { + var recursive = arguments[3] === undefined ? false : arguments[3]; + + var min = 1000000000; + var closestMatch = ''; + var closestMatchPath = []; + var lowerCaseOption = option.toLowerCase(); + var indexMatch = undefined; + for (var op in options) { + var distance = undefined; + if (options[op].__type__ !== undefined && recursive === true) { + var result = Validator.findInOptions(option, options[op], util.copyAndExtendArray(path, op)); + if (min > result.distance) { + closestMatch = result.closestMatch; + closestMatchPath = result.path; + min = result.distance; + indexMatch = result.indexMatch; + } } else { - heightOffset = intersections[key].accumulatedPositive; - intersections[key].accumulatedPositive += group.zeroPosition - combinedData[i].y; - } - } else if (group.options.barChart.sideBySide === true) { - 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; + if (op.toLowerCase().indexOf(lowerCaseOption) !== -1) { + indexMatch = op; + } + distance = Validator.levenshteinDistance(option, op); + if (min > distance) { + closestMatch = op; + closestMatchPath = util.copyArray(path); + min = distance; + } } } + return { closestMatch: closestMatch, path: closestMatchPath, distance: min, indexMatch: indexMatch }; } - DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style); - // draw points - if (group.options.drawPoints.enabled === true) { - Points.draw([combinedData[i]], group, framework, drawData.offset); - //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); - } - } - }; + }, { + key: 'printLocation', + value: function printLocation(path, option) { + var prefix = arguments[2] === undefined ? 'Problem value found at: \n' : arguments[2]; - /** - * Fill the intersections object with counters of how many datapoints share the same x coordinates - * @param intersections - * @param combinedData - * @private - */ - Bargraph._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); + var str = '\n\n' + prefix + 'options = {\n'; + for (var i = 0; i < path.length; i++) { + for (var j = 0; j < i + 1; j++) { + str += ' '; + } + str += path[i] + ': {\n'; + } + for (var j = 0; j < path.length + 1; j++) { + str += ' '; + } + str += option + '\n'; + for (var i = 0; i < path.length + 1; i++) { + for (var j = 0; j < path.length - i; j++) { + str += ' '; + } + str += '}\n'; + } + return str + '\n\n'; } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); + }, { + key: 'print', + value: function print(options) { + return JSON.stringify(options).replace(/(\")|(\[)|(\])|(,"__type__")/g, '').replace(/(\,)/g, ', '); } - if (coreDistance === 0) { - if (intersections[combinedData[i].x] === undefined) { - intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulatedPositive: 0, accumulatedNegative: 0 }; + }, { + key: 'levenshteinDistance', + + // Compute the edit distance between the two given strings + // http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#JavaScript + /* + Copyright (c) 2011 Andrei Mackenzie + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + value: function levenshteinDistance(a, b) { + if (a.length === 0) return b.length; + if (b.length === 0) return a.length; + + var matrix = []; + + // increment along the first column of each row + var i; + for (i = 0; i <= b.length; i++) { + matrix[i] = [i]; } - intersections[combinedData[i].x].amount += 1; + + // increment each column in the first row + var j; + for (j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (i = 1; i <= b.length; i++) { + for (j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) == a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution + Math.min(matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1)); // deletion + } + } + } + + return matrix[b.length][a.length]; } - } - }; + }]); + + return Validator; + })(); + + exports['default'] = Validator; + exports.printStyle = printStyle; + +/***/ }, +/* 54 */ +/***/ function(module, exports, __webpack_require__) { /** - * Get the width and offset for bargraphs based on the coredistance between datapoints + * This object contains all possible options. It will check if the types are correct, if required if the option is one + * of the allowed values. * - * @param coreDistance - * @param group - * @param minWidth - * @returns {{width: Number, offset: Number}} - * @private + * __any__ means that the name of the property does not matter. + * __type__ is a required field for all objects and contains the allowed types of all objects */ - Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { - var width, offset; - if (coreDistance < group.options.barChart.width && coreDistance > 0) { - width = coreDistance < minWidth ? minWidth : coreDistance; + 'use strict'; - 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; - } - } + Object.defineProperty(exports, '__esModule', { + value: true + }); + var string = 'string'; + var boolean = 'boolean'; + var number = 'number'; + var array = 'array'; + var date = 'date'; + var object = 'object'; // should only be in a __type__ property + var dom = 'dom'; + var moment = 'moment'; + var any = 'any'; + + var allOptions = { + configure: { + enabled: { boolean: boolean }, + filter: { boolean: boolean, 'function': 'function' }, + container: { dom: dom }, + __type__: { object: object, boolean: boolean, 'function': 'function' } + }, + + //globals : + align: { string: string }, + autoResize: { boolean: boolean }, + clickToUse: { boolean: boolean }, + dataAttributes: { string: string, array: array }, + editable: { + add: { boolean: boolean, 'undefined': 'undefined' }, + remove: { boolean: boolean, 'undefined': 'undefined' }, + updateGroup: { boolean: boolean, 'undefined': 'undefined' }, + updateTime: { boolean: boolean, 'undefined': 'undefined' }, + __type__: { boolean: boolean, object: object } + }, + end: { number: number, date: date, string: string, moment: moment }, + format: { + minorLabels: { + millisecond: { string: string, 'undefined': 'undefined' }, + second: { string: string, 'undefined': 'undefined' }, + minute: { string: string, 'undefined': 'undefined' }, + hour: { string: string, 'undefined': 'undefined' }, + weekday: { string: string, 'undefined': 'undefined' }, + day: { string: string, 'undefined': 'undefined' }, + month: { string: string, 'undefined': 'undefined' }, + year: { string: string, 'undefined': 'undefined' }, + __type__: { object: object } + }, + majorLabels: { + millisecond: { string: string, 'undefined': 'undefined' }, + second: { string: string, 'undefined': 'undefined' }, + minute: { string: string, 'undefined': 'undefined' }, + hour: { string: string, 'undefined': 'undefined' }, + weekday: { string: string, 'undefined': 'undefined' }, + day: { string: string, 'undefined': 'undefined' }, + month: { string: string, 'undefined': 'undefined' }, + year: { string: string, 'undefined': 'undefined' }, + __type__: { object: object } + }, + __type__: { object: object } + }, + groupOrder: { string: string, 'function': 'function' }, + height: { string: string, number: number }, + hiddenDates: { object: object, array: array }, + locale: { string: string }, + locales: { + __any__: { any: any }, + __type__: { object: object } + }, + margin: { + axis: { number: number }, + item: { + horizontal: { number: number, 'undefined': 'undefined' }, + vertical: { number: number, 'undefined': 'undefined' }, + __type__: { object: object, number: number } + }, + __type__: { object: object, number: number } + }, + max: { date: date, number: number, string: string, moment: moment }, + maxHeight: { number: number, string: string }, + min: { date: date, number: number, string: string, moment: moment }, + minHeight: { number: number, string: string }, + moveable: { boolean: boolean }, + multiselect: { boolean: boolean }, + onAdd: { 'function': 'function' }, + onUpdate: { 'function': 'function' }, + onMove: { 'function': 'function' }, + onMoving: { 'function': 'function' }, + onRemove: { 'function': 'function' }, + order: { 'function': 'function' }, + orientation: { + axis: { string: string, 'undefined': 'undefined' }, + item: { string: string, 'undefined': 'undefined' }, + __type__: { string: string, object: object } + }, + selectable: { boolean: boolean }, + showCurrentTime: { boolean: boolean }, + showMajorLabels: { boolean: boolean }, + showMinorLabels: { boolean: boolean }, + stack: { boolean: boolean }, + snap: { 'function': 'function', 'null': 'null' }, + start: { date: date, number: number, string: string, moment: moment }, + template: { 'function': 'function' }, + timeAxis: { + scale: { string: string, 'undefined': 'undefined' }, + step: { number: number, 'undefined': 'undefined' }, + __type__: { object: object } + }, + type: { string: string }, + width: { string: string, number: number }, + zoomable: { boolean: boolean }, + zoomMax: { number: number }, + zoomMin: { number: number }, - return { width: width, offset: offset }; + __type__: { object: object } }; - Bargraph.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { - if (combinedData.length > 0) { - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x === b.x) { - return a.groupId < b.groupId ? -1 : 1; - } else { - return a.x - b.x; + var configureOptions = { + global: { + align: ['center', 'left', 'right'], + autoResize: true, + clickToUse: false, + // dataAttributes: ['all'], // FIXME: can be 'all' or string[] + editable: { + add: false, + remove: false, + updateGroup: false, + updateTime: false + }, + end: '', + format: { + minorLabels: { + millisecond: 'SSS', + second: 's', + minute: 'HH:mm', + hour: 'HH:mm', + weekday: 'ddd D', + day: 'D', + month: 'MMM', + year: 'YYYY' + }, + majorLabels: { + millisecond: 'HH:mm:ss', + second: 'D MMMM HH:mm', + minute: 'ddd D MMMM', + hour: 'ddd D MMMM', + weekday: 'MMMM YYYY', + day: 'MMMM YYYY', + month: 'YYYY', + year: '' } - }); - var intersections = {}; - - Bargraph._getDataIntersections(intersections, combinedData); - groupRanges[groupLabel] = Bargraph._getStackedYRange(intersections, combinedData); - groupRanges[groupLabel].yAxisOrientation = orientation; - groupIds.push(groupLabel); - } - }; + }, - Bargraph._getStackedYRange = 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 { - if (combinedData[i].y < 0) { - intersections[key].accumulatedNegative += combinedData[i].y; - } else { - intersections[key].accumulatedPositive += combinedData[i].y; + //groupOrder: {string, 'function': 'function'}, + height: '', + //hiddenDates: {object, array}, + locale: '', + margin: { + axis: [20, 0, 100, 1], + item: { + horizontal: [10, 0, 100, 1], + vertical: [10, 0, 100, 1] } - } - } - for (var xpos in intersections) { - if (intersections.hasOwnProperty(xpos)) { - yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; - yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; - yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; - yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; - } + }, + max: '', + maxHeight: '', + min: '', + minHeight: '', + moveable: false, + multiselect: false, + //onAdd: {'function': 'function'}, + //onUpdate: {'function': 'function'}, + //onMove: {'function': 'function'}, + //onMoving: {'function': 'function'}, + //onRename: {'function': 'function'}, + //order: {'function': 'function'}, + orientation: { + axis: ['both', 'bottom', 'top'], + item: ['bottom', 'top'] + }, + selectable: true, + showCurrentTime: false, + showMajorLabels: true, + showMinorLabels: true, + stack: true, + //snap: {'function': 'function', nada}, + start: '', + //template: {'function': 'function'}, + //timeAxis: { + // scale: ['millisecond', 'second', 'minute', 'hour', 'weekday', 'day', 'month', 'year'], + // step: [1, 1, 10, 1] + //}, + type: ['box', 'point', 'range', 'background'], + width: '100%', + zoomable: true, + zoomMax: [315360000000000, 10, 315360000000000, 1], + zoomMin: [10, 10, 315360000000000, 1] } - - return { min: yMin, max: yMax }; }; - module.exports = Bargraph; + exports.allOptions = allOptions; + exports.configureOptions = configureOptions; /***/ }, -/* 58 */ +/* 55 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - var util = __webpack_require__(9); - var DOMutil = __webpack_require__(14); - var Component = __webpack_require__(28); + var Emitter = __webpack_require__(25); + var Hammer = __webpack_require__(10); + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + var Range = __webpack_require__(35); + var Core = __webpack_require__(38); + var TimeAxis = __webpack_require__(47); + var CurrentTime = __webpack_require__(32); + var CustomTime = __webpack_require__(50); + var LineGraph = __webpack_require__(56); + + var Configurator = __webpack_require__(51); + var Validator = __webpack_require__(53)['default']; + var printStyle = __webpack_require__(53).printStyle; + var allOptions = __webpack_require__(64).allOptions; + var configureOptions = __webpack_require__(64).configureOptions; /** - * Legend for Graph2d + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array} [items] + * @param {Object} [options] See Graph2d.setOptions for the available options. + * @constructor + * @extends Core */ - function Legend(body, options, side, linegraphOptions) { - this.body = body; + 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 = { - enabled: true, - icons: true, - iconSize: 20, - iconSpacing: 6, - left: { - visible: true, - position: 'top-left' // top/bottom - left,center,right + start: null, + end: null, + + autoResize: true, + + orientation: { + axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both' + item: 'bottom' // not relevant for Graph2d }, - right: { - visible: true, - position: 'top-left' // top/bottom - left,center,right + + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); + + // Create the DOM, props, and emitter + this._create(container); + + // all components listed here will be repainted automatically + this.components = []; + + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + hiddenDates: [], + util: { + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime: me._toGlobalTime.bind(me) } }; - this.side = side; - this.options = util.extend({}, this.defaultOptions); - this.linegraphOptions = linegraphOptions; - this.svgElements = {}; - this.dom = {}; - this.groups = {}; - this.amountOfGroups = 0; - this._create(); + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - this.setOptions(options); - } + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + //this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - Legend.prototype = new Component(); + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - Legend.prototype.clear = function () { - this.groups = {}; - this.amountOfGroups = 0; - }; + // item set + this.linegraph = new LineGraph(this.body); + this.components.push(this.linegraph); - Legend.prototype.addGroup = function (label, graphOptions) { + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; - } - this.amountOfGroups += 1; - }; + this.on('tap', function (event) { + me.emit('click', me.getEventProperties(event)); + }); + this.on('doubletap', function (event) { + me.emit('doubleClick', me.getEventProperties(event)); + }); + this.dom.root.oncontextmenu = function (event) { + me.emit('contextmenu', me.getEventProperties(event)); + }; - Legend.prototype.updateGroup = function (label, graphOptions) { - this.groups[label] = graphOptions; - }; + // apply options + if (options) { + this.setOptions(options); + } - Legend.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); } - }; - Legend.prototype._create = function () { - this.dom.frame = document.createElement('div'); - this.dom.frame.className = 'vis-legend'; - this.dom.frame.style.position = 'absolute'; - this.dom.frame.style.top = '10px'; - this.dom.frame.style.display = 'block'; + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } + } - this.dom.textArea = document.createElement('div'); - this.dom.textArea.className = 'vis-legend-text'; - this.dom.textArea.style.position = 'relative'; - this.dom.textArea.style.top = '0px'; + // Extend the functionality from Core + Graph2d.prototype = new Core(); - 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.svg.style.height = '100%'; + Graph2d.prototype.setOptions = function (options) { + // validate options + var errorFound = Validator.validate(options, allOptions); + if (errorFound === true) { + console.log('%cErrors have been found in the supplied options object.', printStyle); + } - this.dom.frame.appendChild(this.svg); - this.dom.frame.appendChild(this.dom.textArea); + Core.prototype.setOptions.call(this, options); }; /** - * Hide the component from the DOM + * Set items + * @param {vis.DataSet | Array | null} items */ - Legend.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); + Graph2d.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; + + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; + } else { + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: 'Date', + end: 'Date' + } + }); + } + + // set items + this.itemsData = newDataSet; + this.linegraph && this.linegraph.setItems(newDataSet); + + if (initialLoad) { + 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, { animation: false }); + } else { + this.fit({ animation: false }); + } } }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * Set groups + * @param {vis.DataSet | Array} groups */ - Legend.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); + 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); } - }; - Legend.prototype.setOptions = function (options) { - var fields = ['enabled', 'orientation', 'icons', 'left', 'right']; - util.selectiveDeepExtend(fields, this.options, options); + this.groupsData = newDataSet; + this.linegraph.setGroups(newDataSet); }; - Legend.prototype.redraw = function () { - var activeGroups = 0; - var groupArray = Object.keys(this.groups); - groupArray.sort(function (a, b) { - return a < b ? -1 : 1; - }); - - for (var i = 0; i < groupArray.length; i++) { - var groupId = groupArray[i]; - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - activeGroups++; - } + /** + * 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 + */ + Graph2d.prototype.getLegend = function (groupId, width, height) { + if (width === undefined) { + width = 15; } - - if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) { - this.hide(); + if (height === undefined) { + height = 15; + } + if (this.linegraph.groups[groupId] !== undefined) { + return this.linegraph.groups[groupId].getLegend(width, height); } 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 = ''; - } + return 'cannot find group:' + groupId; + } + }; - 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 { - var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height; - this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace('px', '')) + 'px'; - this.dom.frame.style.top = ''; - } + /** + * 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; + } + }; - 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(); - } + /** + * Get the data range of the item set. + * @returns {{min: Date, max: Date}} range A range with a start and end Date. + * When no minimum is found, min==null + * When no maximum is found, max==null + */ + Graph2d.prototype.getDataRange = function () { + var min = null; + var max = null; - var content = ''; - for (var i = 0; i < groupArray.length; i++) { - var groupId = groupArray[i]; - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - content += this.groups[groupId].content + '
'; + // 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; + } } } - this.dom.textArea.innerHTML = content; - this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + 'px'; } - }; - Legend.prototype.drawLegendIcons = function () { - if (this.dom.frame.parentNode) { - var groupArray = Object.keys(this.groups); - groupArray.sort(function (a, b) { - return a < b ? -1 : 1; - }); + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; + }; - 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; + /** + * Generate Timeline related information from an event + * @param {Event} event + * @return {Object} An object with related information, like on which area + * The event happened, whether clicked on an item, etc. + */ + Graph2d.prototype.getEventProperties = function (event) { + var clientX = event.center ? event.center.x : event.clientX; + var clientY = event.center ? event.center.y : event.clientY; + var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer); + var y = clientY - util.getAbsoluteTop(this.dom.centerContainer); + var time = this._toTime(x); - this.svg.style.width = iconWidth + 5 + iconOffset + 'px'; + var customTime = CustomTime.customTimeFromTarget(event); - for (var i = 0; i < groupArray.length; i++) { - var groupId = groupArray[i]; - 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; - } - } + var element = util.getTarget(event); + var what = null; + if (util.hasParent(element, this.timeAxis.dom.foreground)) { + what = 'axis'; + } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) { + what = 'axis'; + } else if (util.hasParent(element, this.linegraph.yAxisLeft.dom.frame)) { + what = 'data-axis'; + } else if (util.hasParent(element, this.linegraph.yAxisRight.dom.frame)) { + what = 'data-axis'; + } else if (util.hasParent(element, this.linegraph.legendLeft.dom.frame)) { + what = 'legend'; + } else if (util.hasParent(element, this.linegraph.legendRight.dom.frame)) { + what = 'legend'; + } else if (customTime != null) { + what = 'custom-time'; + } else if (util.hasParent(element, this.currentTime.bar)) { + what = 'current-time'; + } else if (util.hasParent(element, this.dom.center)) { + what = 'background'; + } - DOMutil.cleanupElements(this.svgElements); + var value = []; + var yAxisLeft = this.linegraph.yAxisLeft; + var yAxisRight = this.linegraph.yAxisRight; + if (!yAxisLeft.hidden) { + value.push(yAxisLeft.screenToValue(y)); + } + if (!yAxisRight.hidden) { + value.push(yAxisRight.screenToValue(y)); } + + return { + event: event, + what: what, + pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX, + pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY, + x: x, + y: y, + time: time, + value: value + }; }; - module.exports = Legend; + /** + * Load a configurator + * @return {Object} + * @private + */ + Graph2d.prototype._createConfigurator = function () { + return new Configurator(this, this.dom.container, configureOptions); + }; + + module.exports = Graph2d; /***/ }, -/* 59 */ +/* 56 */ /***/ function(module, exports, __webpack_require__) { - /** - * This object contains all possible options. It will check if the types are correct, if required if the option is one - * of the allowed values. - * - * __any__ means that the name of the property does not matter. - * __type__ is a required field for all objects and contains the allowed types of all objects - */ 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); - var string = 'string'; - var boolean = 'boolean'; - var number = 'number'; - var array = 'array'; - var date = 'date'; - var object = 'object'; // should only be in a __type__ property - var dom = 'dom'; - var moment = 'moment'; - var any = 'any'; - - var allOptions = { - configure: { - enabled: { boolean: boolean }, - filter: { boolean: boolean, 'function': 'function' }, - container: { dom: dom }, - __type__: { object: object, boolean: boolean, 'function': 'function' } - }, + var util = __webpack_require__(14); + var DOMutil = __webpack_require__(19); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + var Component = __webpack_require__(33); + var DataAxis = __webpack_require__(57); + var GraphGroup = __webpack_require__(59); + var Legend = __webpack_require__(63); + var BarFunctions = __webpack_require__(62); + var LineFunctions = __webpack_require__(60); - //globals : - yAxisOrientation: { string: ['left', 'right'] }, - defaultGroup: { string: string }, - sort: { boolean: boolean }, - sampling: { boolean: boolean }, - stack: { boolean: boolean }, - graphHeight: { string: string, number: number }, - shaded: { - enabled: { boolean: boolean }, - orientation: { string: ['bottom', 'top'] }, // top, bottom - __type__: { boolean: boolean, object: object } - }, - style: { string: ['line', 'bar', 'points'] }, // line, bar - barChart: { - width: { number: number }, - sideBySide: { boolean: boolean }, - align: { string: ['left', 'center', 'right'] }, - __type__: { object: object } - }, - interpolation: { - enabled: { boolean: boolean }, - parametrization: { string: ['centripetal', 'chordal', 'uniform'] }, // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) - alpha: { number: number }, - __type__: { object: object, boolean: boolean } - }, - drawPoints: { - enabled: { boolean: boolean }, - onRender: { 'function': 'function' }, - size: { number: number }, - style: { string: ['square', 'circle'] }, // square, circle - __type__: { object: object, boolean: boolean, 'function': 'function' } - }, - dataAxis: { - showMinorLabels: { boolean: boolean }, - showMajorLabels: { boolean: boolean }, - icons: { boolean: boolean }, - width: { string: string, number: number }, - visible: { boolean: boolean }, - alignZeros: { boolean: boolean }, - left: { - range: { min: { number: number }, max: { number: number }, __type__: { object: object } }, - format: { 'function': 'function' }, - title: { text: { string: string, number: number }, style: { string: string }, __type__: { object: object } }, - __type__: { object: object } - }, - right: { - range: { min: { number: number }, max: { number: number }, __type__: { object: object } }, - format: { 'function': 'function' }, - title: { text: { string: string, number: number }, style: { string: string }, __type__: { object: object } }, - __type__: { object: object } - }, - __type__: { object: object } - }, - legend: { - enabled: { boolean: boolean }, - icons: { boolean: boolean }, - left: { - visible: { boolean: boolean }, - position: { string: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] }, - __type__: { object: object } - }, - right: { - visible: { boolean: boolean }, - position: { string: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] }, - __type__: { object: object } - }, - __type__: { object: object, boolean: boolean } - }, - groups: { - visibility: { any: any }, - __type__: { object: object } - }, + var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items - autoResize: { boolean: boolean }, - clickToUse: { boolean: boolean }, - end: { number: number, date: date, string: string, moment: moment }, - format: { - minorLabels: { - millisecond: { string: string, 'undefined': 'undefined' }, - second: { string: string, 'undefined': 'undefined' }, - minute: { string: string, 'undefined': 'undefined' }, - hour: { string: string, 'undefined': 'undefined' }, - weekday: { string: string, 'undefined': 'undefined' }, - day: { string: string, 'undefined': 'undefined' }, - month: { string: string, 'undefined': 'undefined' }, - year: { string: string, 'undefined': 'undefined' }, - __type__: { object: object } - }, - majorLabels: { - millisecond: { string: string, 'undefined': 'undefined' }, - second: { string: string, 'undefined': 'undefined' }, - minute: { string: string, 'undefined': 'undefined' }, - hour: { string: string, 'undefined': 'undefined' }, - weekday: { string: string, 'undefined': 'undefined' }, - day: { string: string, 'undefined': 'undefined' }, - month: { string: string, 'undefined': 'undefined' }, - year: { string: string, 'undefined': 'undefined' }, - __type__: { object: object } - }, - __type__: { object: object } - }, - height: { string: string, number: number }, - hiddenDates: { object: object, array: array }, - locale: { string: string }, - locales: { - __any__: { any: any }, - __type__: { object: object } - }, - max: { date: date, number: number, string: string, moment: moment }, - maxHeight: { number: number, string: string }, - min: { date: date, number: number, string: string, moment: moment }, - minHeight: { number: number, string: string }, - moveable: { boolean: boolean }, - multiselect: { boolean: boolean }, - orientation: { string: string }, - showCurrentTime: { boolean: boolean }, - showMajorLabels: { boolean: boolean }, - showMinorLabels: { boolean: boolean }, - start: { date: date, number: number, string: string, moment: moment }, - timeAxis: { - scale: { string: string, 'undefined': 'undefined' }, - step: { number: number, 'undefined': 'undefined' }, - __type__: { object: object } - }, - width: { string: string, number: number }, - zoomable: { boolean: boolean }, - zoomMax: { number: number }, - zoomMin: { number: number }, - __type__: { object: object } - }; + /** + * 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; - var configureOptions = { - global: { - //yAxisOrientation: ['left','right'], // TDOO: enable as soon as Grahp2d doesn't crash when changing this on the fly + this.defaultOptions = { + yAxisOrientation: 'left', + defaultGroup: 'default', sort: true, sampling: true, stack: false, + graphHeight: '400px', shaded: { enabled: false, - orientation: ['top', 'bottom'] // top, bottom + orientation: 'bottom' // top, bottom }, - style: ['line', 'bar', 'points'], // line, bar + style: 'line', // line, bar barChart: { - width: [50, 5, 100, 5], + width: 50, sideBySide: false, - align: ['left', 'center', 'right'] // left, center, right + align: 'center' // left, center, right }, interpolation: { enabled: true, - parametrization: ['centripetal', 'chordal', 'uniform'] // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) + parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) + alpha: 0.5 }, drawPoints: { enabled: true, - size: [6, 2, 30, 1], - style: ['square', 'circle'] // square, circle + size: 6, + style: 'square' // square, circle }, dataAxis: { showMinorLabels: true, showMajorLabels: true, icons: false, - width: [40, 0, 200, 1], + width: '40px', visible: true, alignZeros: true, left: { - //range: {min:'undefined': 'undefined'ined,max:'undefined': 'undefined'ined}, - //format: function (value) {return value;}, - title: { text: '', style: '' } + range: { min: undefined, max: undefined }, + format: function format(value) { + return value; + }, + title: { text: undefined, style: undefined } }, right: { - //range: {min:'undefined': 'undefined'ined,max:'undefined': 'undefined'ined}, - //format: function (value) {return value;}, - title: { text: '', style: '' } + range: { min: undefined, max: undefined }, + format: function format(value) { + return value; + }, + title: { text: undefined, style: undefined } } }, legend: { @@ -26911,2647 +26207,3798 @@ return /******/ (function(modules) { // webpackBootstrap icons: true, left: { visible: true, - position: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] // top/bottom - left,right + position: 'top-left' // top/bottom - left,right }, right: { visible: true, - position: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] // top/bottom - left,right + position: 'top-right' // top/bottom - left,right } }, + groups: { + visibility: {} + } + }; - autoResize: true, - clickToUse: false, - end: '', - format: { - minorLabels: { - millisecond: 'SSS', - second: 's', - minute: 'HH:mm', - hour: 'HH:mm', - weekday: 'ddd D', - day: 'D', - month: 'MMM', - year: 'YYYY' - }, - majorLabels: { - millisecond: 'HH:mm:ss', - second: 'D MMMM HH:mm', - minute: 'ddd D MMMM', - hour: 'ddd D MMMM', - weekday: 'MMMM YYYY', - day: 'MMMM YYYY', - month: 'YYYY', - year: '' - } - }, + // 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; + this.updateSVGheight = false; + this.updateSVGheightOnResize = false; - height: '', - locale: '', - max: '', - maxHeight: '', - min: '', - minHeight: '', - moveable: true, - orientation: ['both', 'bottom', 'top'], - showCurrentTime: false, - showMajorLabels: true, - showMinorLabels: true, - start: '', - width: '100%', - zoomable: true, - zoomMax: [315360000000000, 10, 315360000000000, 1], - zoomMin: [10, 10, 315360000000000, 1] - } - }; + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - exports.allOptions = allOptions; - exports.configureOptions = configureOptions; + // listeners for the DataSet of the items + this.itemListeners = { + 'add': function add(event, params, senderId) { + me._onAdd(params.items); + }, + 'update': function update(event, params, senderId) { + me._onUpdate(params.items); + }, + 'remove': function remove(event, params, senderId) { + me._onRemove(params.items); + } + }; -/***/ }, -/* 60 */ -/***/ function(module, exports, __webpack_require__) { + // listeners for the DataSet of the groups + this.groupListeners = { + 'add': function add(event, params, senderId) { + me._onAddGroups(params.items); + }, + 'update': function update(event, params, senderId) { + me._onUpdateGroups(params.items); + }, + 'remove': function remove(event, params, senderId) { + me._onRemoveGroups(params.items); + } + }; - // Load custom shapes into CanvasRenderingContext2D - 'use strict'; + 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 - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + this.svgElements = {}; + this.setOptions(options); + this.groupsUsingDefaultStyles = [0]; + this.COUNTER = 0; + this.body.emitter.on('rangechanged', function () { + me.lastStart = me.body.range.start; + me.svg.style.left = util.option.asSize(-me.props.width); + me.redraw.call(me, true); + }); - var _modulesGroups = __webpack_require__(61); + // create the HTML DOM + this._create(); + this.framework = { svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups }; + this.body.emitter.emit('change'); + } - var _modulesGroups2 = _interopRequireDefault(_modulesGroups); + LineGraph.prototype = new Component(); - var _modulesNodesHandler = __webpack_require__(62); + /** + * Create the HTML DOM for the ItemSet + */ + LineGraph.prototype._create = function () { + var frame = document.createElement('div'); + frame.className = 'vis-line-graph'; + this.dom.frame = frame; - var _modulesNodesHandler2 = _interopRequireDefault(_modulesNodesHandler); + // 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 _modulesEdgesHandler = __webpack_require__(82); + // data axis + this.options.dataAxis.orientation = 'left'; + this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - var _modulesEdgesHandler2 = _interopRequireDefault(_modulesEdgesHandler); + 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 _modulesPhysicsEngine = __webpack_require__(89); + // 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 _modulesPhysicsEngine2 = _interopRequireDefault(_modulesPhysicsEngine); + this.show(); + }; - var _modulesClustering = __webpack_require__(98); + /** + * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. + * @param {object} options + */ + LineGraph.prototype.setOptions = function (options) { + if (options) { + var fields = ['sampling', 'defaultGroup', 'stack', 'height', 'graphHeight', 'yAxisOrientation', 'style', 'barChart', 'dataAxis', 'sort', 'groups']; + if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) { + this.updateSVGheight = true; + this.updateSVGheightOnResize = true; + } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { + if (parseInt((options.graphHeight + '').replace('px', '')) < this.body.domProps.centerContainer.height) { + this.updateSVGheight = true; + } + } + util.selectiveDeepExtend(fields, this.options, options); + util.mergeOptions(this.options, options, 'interpolation'); + util.mergeOptions(this.options, options, 'drawPoints'); + util.mergeOptions(this.options, options, 'shaded'); + util.mergeOptions(this.options, options, 'legend'); - var _modulesClustering2 = _interopRequireDefault(_modulesClustering); + if (options.interpolation) { + if (typeof options.interpolation == 'object') { + if (options.interpolation.parametrization) { + if (options.interpolation.parametrization == 'uniform') { + this.options.interpolation.alpha = 0; + } else if (options.interpolation.parametrization == 'chordal') { + this.options.interpolation.alpha = 1; + } else { + this.options.interpolation.parametrization = 'centripetal'; + this.options.interpolation.alpha = 0.5; + } + } + } + } - var _modulesCanvasRenderer = __webpack_require__(100); + if (this.yAxisLeft) { + if (options.dataAxis !== undefined) { + this.yAxisLeft.setOptions(this.options.dataAxis); + this.yAxisRight.setOptions(this.options.dataAxis); + } + } - var _modulesCanvasRenderer2 = _interopRequireDefault(_modulesCanvasRenderer); + if (this.legendLeft) { + if (options.legend !== undefined) { + this.legendLeft.setOptions(this.options.legend); + this.legendRight.setOptions(this.options.legend); + } + } - var _modulesCanvas = __webpack_require__(101); + if (this.groups.hasOwnProperty(UNGROUPED)) { + this.groups[UNGROUPED].setOptions(options); + } + } - var _modulesCanvas2 = _interopRequireDefault(_modulesCanvas); + // this is used to redraw the graph if the visibility of the groups is changed. + if (this.dom.frame) { + this.redraw(true); + } + }; - var _modulesView = __webpack_require__(102); + /** + * 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); + } + }; - var _modulesView2 = _interopRequireDefault(_modulesView); + /** + * 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 _modulesInteractionHandler = __webpack_require__(103); + /** + * Set items + * @param {vis.DataSet | null} items + */ + LineGraph.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; - var _modulesInteractionHandler2 = _interopRequireDefault(_modulesInteractionHandler); + // replace the dataset + if (!items) { + this.itemsData = null; + } else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } else { + throw new TypeError('Data must be an instance of DataSet or DataView'); + } - var _modulesSelectionHandler = __webpack_require__(3); + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - var _modulesSelectionHandler2 = _interopRequireDefault(_modulesSelectionHandler); + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } - var _modulesLayoutEngine = __webpack_require__(106); + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + } + this._updateUngrouped(); + //this._updateGraph(); + this.redraw(true); + }; - var _modulesManipulationSystem = __webpack_require__(107); + /** + * Set groups + * @param {vis.DataSet} groups + */ + LineGraph.prototype.setGroups = function (groups) { + var me = this; + var ids; - var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.off(event, callback); + }); - var _sharedConfigurator = __webpack_require__(46); + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - var _sharedConfigurator2 = _interopRequireDefault(_sharedConfigurator); + // replace the dataset + if (!groups) { + this.groupsData = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + this.groupsData = groups; + } else { + throw new TypeError('Data must be an instance of DataSet or DataView'); + } - var _sharedValidator = __webpack_require__(48); + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); - var _sharedValidator2 = _interopRequireDefault(_sharedValidator); + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } + this._onUpdate(); + }; - var _optionsJs = __webpack_require__(108); + /** + * Update the data + * @param [ids] + * @private + */ + LineGraph.prototype._onUpdate = function (ids) { + this._updateUngrouped(); + this._updateAllGroupData(); + //this._updateGraph(); + this.redraw(true); + }; + 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]); + } - __webpack_require__(109); + //this._updateGraph(); + this.redraw(true); + }; + LineGraph.prototype._onAddGroups = function (groupIds) { + this._onUpdateGroups(groupIds); + }; - var Emitter = __webpack_require__(20); - var Hammer = __webpack_require__(5); - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - var dotparser = __webpack_require__(110); - var gephiParser = __webpack_require__(111); - var Images = __webpack_require__(112); - var Activator = __webpack_require__(43); - var locales = __webpack_require__(113); + /** + * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph + * @param {Array} groupIds + * @private + */ + 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(true); + }; /** - * @constructor Network - * Create a network visualization, displaying nodes and edges. + * update a group object with the group dataset entree * - * @param {Element} container The DOM element in which the Network will - * be created. Normally a div element. - * @param {Object} data An object containing parameters - * {Array} nodes - * {Array} edges - * @param {Object} options Options + * @param group + * @param groupId + * @private */ - function Network(container, data, options) { - var _this = this; - - if (!(this instanceof Network)) { - throw new SyntaxError('Constructor must be called with the new operator'); + LineGraph.prototype._updateGroup = function (group, groupId) { + if (!this.groups.hasOwnProperty(groupId)) { + this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles); + if (this.groups[groupId].options.yAxisOrientation == 'right') { + this.yAxisRight.addGroup(groupId, this.groups[groupId]); + this.legendRight.addGroup(groupId, this.groups[groupId]); + } else { + this.yAxisLeft.addGroup(groupId, this.groups[groupId]); + this.legendLeft.addGroup(groupId, this.groups[groupId]); + } + } else { + this.groups[groupId].update(group); + if (this.groups[groupId].options.yAxisOrientation == 'right') { + this.yAxisRight.updateGroup(groupId, this.groups[groupId]); + this.legendRight.updateGroup(groupId, this.groups[groupId]); + } else { + this.yAxisLeft.updateGroup(groupId, this.groups[groupId]); + this.legendLeft.updateGroup(groupId, this.groups[groupId]); + } } + this.legendLeft.redraw(); + this.legendRight.redraw(); + }; - // set constant values - this.options = {}; - this.defaultOptions = { - locale: 'en', - locales: locales, - clickToUse: false - }; - util.extend(this.options, this.defaultOptions); - - // containers for nodes and edges - this.body = { - container: container, - nodes: {}, - nodeIndices: [], - edges: {}, - edgeIndices: [], - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this), - once: this.once.bind(this) - }, - eventListeners: { - onTap: function onTap() {}, - onTouch: function onTouch() {}, - onDoubleTap: function onDoubleTap() {}, - onHold: function onHold() {}, - onDragStart: function onDragStart() {}, - onDrag: function onDrag() {}, - onDragEnd: function onDragEnd() {}, - onMouseWheel: function onMouseWheel() {}, - onPinch: function onPinch() {}, - onMouseMove: function onMouseMove() {}, - onRelease: function onRelease() {}, - onContext: function onContext() {} - }, - data: { - nodes: null, // A DataSet or DataView - edges: null // A DataSet or DataView - }, - functions: { - createNode: function createNode() {}, - createEdge: function createEdge() {}, - getPointer: function getPointer() {} - }, - view: { - scale: 1, - translation: { x: 0, y: 0 } + /** + * this updates all groups, it is used when there is an update the the itemset. + * + * @private + */ + LineGraph.prototype._updateAllGroupData = function () { + if (this.itemsData != null) { + var groupsContent = {}; + var groupId; + for (groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + groupsContent[groupId] = []; + } } - }; - - // bind the event listeners - this.bindEventListeners(); - - // setting up all modules - this.images = new Images(function () { - return _this.body.emitter.emit('_requestRedraw'); - }); // object with images - this.groups = new _modulesGroups2['default'](); // object with groups - this.canvas = new _modulesCanvas2['default'](this.body); // DOM handler - this.selectionHandler = new _modulesSelectionHandler2['default'](this.body, this.canvas); // Selection handler - this.interactionHandler = new _modulesInteractionHandler2['default'](this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key - this.view = new _modulesView2['default'](this.body, this.canvas); // camera handler, does animations and zooms - this.renderer = new _modulesCanvasRenderer2['default'](this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into - this.physics = new _modulesPhysicsEngine2['default'](this.body); // physics engine, does all the simulations - this.layoutEngine = new _modulesLayoutEngine2['default'](this.body); // layout engine for inital layout and hierarchical layout - this.clustering = new _modulesClustering2['default'](this.body); // clustering api - this.manipulation = new _modulesManipulationSystem2['default'](this.body, this.canvas, this.selectionHandler); // data manipulation system - - this.nodesHandler = new _modulesNodesHandler2['default'](this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options - this.edgesHandler = new _modulesEdgesHandler2['default'](this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options - - // create the DOM elements - this.canvas._create(); + for (var itemId in this.itemsData._data) { + if (this.itemsData._data.hasOwnProperty(itemId)) { + var item = this.itemsData._data[itemId]; + if (groupsContent[item.group] === undefined) { + throw new Error('Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups.'); + } + 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]); + } + } + } + }; - // apply options - this.setOptions(options); + /** + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. This anonymous group is called 'graph'. + * @protected + */ + LineGraph.prototype._updateUngrouped = function () { + if (this.itemsData && 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; + } + } + } - // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data); - } + 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); + } + } else { + delete this.groups[UNGROUPED]; + this.legendLeft.removeGroup(UNGROUPED); + this.legendRight.removeGroup(UNGROUPED); + this.yAxisLeft.removeGroup(UNGROUPED); + this.yAxisRight.removeGroup(UNGROUPED); + } - // Extend Network with an Emitter mixin - Emitter(Network.prototype); + this.legendLeft.redraw(); + this.legendRight.redraw(); + }; /** - * Set options - * @param {Object} options + * Redraw the component, mandatory function + * @return {boolean} Returns true if the component is resized */ - Network.prototype.setOptions = function (options) { - var _this2 = this; - - if (options !== undefined) { + LineGraph.prototype.redraw = function (forceGraphUpdate) { + var resized = false; - var errorFound = _sharedValidator2['default'].validate(options, _optionsJs.allOptions); - if (errorFound === true) { - console.log('%cErrors have been found in the supplied options object.', _sharedValidator.printStyle); - } + // calculate actual size and position + this.props.width = this.dom.frame.offsetWidth; + this.props.height = this.body.domProps.centerContainer.height - this.body.domProps.border.top - this.body.domProps.border.bottom; - // copy the global fields over - var fields = ['locale', 'locales', 'clickToUse']; - util.selectiveDeepExtend(fields, this.options, options); + // update the graph if there is no lastWidth or with, used for the initial draw + if (this.lastWidth === undefined && this.props.width) { + forceGraphUpdate = true; + } - // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. - options = this.layoutEngine.setOptions(options.layout, options); + // check if this component is resized + resized = this._isResized() || resized; - this.canvas.setOptions(options); // options for canvas are in globals + // 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.lastVisibleInterval = visibleInterval; - // pass the options to the modules - this.groups.setOptions(options.groups); - this.nodesHandler.setOptions(options.nodes); - this.edgesHandler.setOptions(options.edges); - this.physics.setOptions(options.physics); - this.manipulation.setOptions(options.manipulation, options, this.options); // manipulation uses the locales in the globals + // 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.props.width); + this.svg.style.left = util.option.asSize(-this.props.width); - this.interactionHandler.setOptions(options.interaction); - this.renderer.setOptions(options.interaction); // options for rendering are in interaction - this.selectionHandler.setOptions(options.interaction); // options for selection are in interaction + // if the height of the graph is set as proportional, change the height of the svg + if ((this.options.height + '').indexOf('%') != -1 || this.updateSVGheightOnResize == true) { + this.updateSVGheight = true; + } + } - // reload the settings of the nodes to apply changes in groups that are not referenced by pointer. - if (options.groups !== undefined) { - this.body.emitter.emit('refreshNodes'); + // update the height of the graph on each redraw of the graph. + if (this.updateSVGheight == true) { + if (this.options.graphHeight != this.props.height + 'px') { + this.options.graphHeight = this.props.height + 'px'; + this.svg.style.height = this.props.height + 'px'; } - // these two do not have options at the moment, here for completeness - //this.view.setOptions(options.view); - //this.clustering.setOptions(options.clustering); + this.updateSVGheight = false; + } else { + this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px'; + } - if ('configure' in options) { - if (!this.configurator) { - this.configurator = new _sharedConfigurator2['default'](this, this.body.container, _optionsJs.configureOptions, this.canvas.pixelRatio); + // zoomed is here to ensure that animations are shown correctly. + if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) { + resized = this._updateGraph() || resized; + } 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.props.width != 0) { + var rangePerPixelInv = this.props.width / range; + var xOffset = offset * rangePerPixelInv; + this.svg.style.left = -this.props.width - xOffset + 'px'; } + } + } - this.configurator.setOptions(options.configure); + this.legendLeft.redraw(); + this.legendRight.redraw(); + return resized; + }; + + /** + * Update and redraw the graph. + * + */ + LineGraph.prototype._updateGraph = function () { + // reset the svg elements + DOMutil.prepareElements(this.svgElements); + if (this.props.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 only loads the data we require based on the timewindow + this._getRelevantData(groupIds, groupsData, minDate, maxDate); - // if the configuration system is enabled, copy all options and put them into the config system - if (this.configurator && this.configurator.options.enabled === true) { - var networkOptions = { nodes: {}, edges: {}, layout: {}, interaction: {}, manipulation: {}, physics: {}, global: {} }; - util.deepExtend(networkOptions.nodes, this.nodesHandler.options); - util.deepExtend(networkOptions.edges, this.edgesHandler.options); - util.deepExtend(networkOptions.layout, this.layoutEngine.options); - // load the selectionHandler and render default options in to the interaction group - util.deepExtend(networkOptions.interaction, this.selectionHandler.options); - util.deepExtend(networkOptions.interaction, this.renderer.options); + // apply sampling, if disabled, it will pass through this function. + this._applySampling(groupIds, groupsData); - util.deepExtend(networkOptions.interaction, this.interactionHandler.options); - util.deepExtend(networkOptions.manipulation, this.manipulation.options); - util.deepExtend(networkOptions.physics, this.physics.options); + // we transform the X coordinates to detect collisions + for (i = 0; i < groupIds.length; i++) { + preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); + } - // load globals into the global object - util.deepExtend(networkOptions.global, this.canvas.options); - util.deepExtend(networkOptions.global, this.options); + // now all needed data has been collected we start the processing. + this._getYRanges(groupIds, preprocessedGroupData, groupRanges); - this.configurator.setModuleOptions(networkOptions); - } + // 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); + var MAX_CYCLES = 5; + if (changeCalled == true && this.COUNTER < MAX_CYCLES) { + DOMutil.cleanupElements(this.svgElements); + this.abortedGraphUpdate = true; + this.COUNTER++; + this.body.emitter.emit('change'); + return true; + } else { + if (this.COUNTER > MAX_CYCLES) { + console.log('WARNING: there may be an infinite loop in the _updateGraph emitter cycle.'); + } + this.COUNTER = 0; + this.abortedGraphUpdate = false; - // handle network global options - if (options.clickToUse !== undefined) { - if (options.clickToUse === true) { - if (this.activator === undefined) { - this.activator = new Activator(this.canvas.frame); - this.activator.on('change', function () { - _this2.body.emitter.emit('activate'); - }); + // 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); } - } else { - if (this.activator !== undefined) { - this.activator.destroy(); - delete this.activator; + + // draw the groups + for (i = 0; i < groupIds.length; i++) { + group = this.groups[groupIds[i]]; + if (group.options.style != 'bar') { + // bar needs to be drawn enmasse + group.draw(processedGroupData[groupIds[i]], group, this.framework); + } } - this.body.emitter.emit('activate'); + BarFunctions.draw(groupIds, processedGroupData, this.framework); } - } else { - this.body.emitter.emit('activate'); } - - this.canvas.setSize(); - // start the physics simulation. Can be safely called multiple times. - this.body.emitter.emit('startSimulation'); } + + // cleanup unused svg elements + DOMutil.cleanupElements(this.svgElements); + return false; }; /** - * Update the this.body.nodeIndices with the most recent node index list + * 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. + * + * @param {array} groupIds + * @param {object} groupsData + * @param {date} minDate + * @param {date} maxDate * @private */ - Network.prototype._updateVisibleIndices = function () { - var nodes = this.body.nodes; - var edges = this.body.edges; - this.body.nodeIndices = []; - this.body.edgeIndices = []; - - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].options.hidden === false) { - this.body.nodeIndices.push(nodeId); - } - } - } - - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].options.hidden === false) { - this.body.edgeIndices.push(edgeId); + LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { + 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.binarySearchValue(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); + } + } + } } } } }; /** - * Bind all events - */ - Network.prototype.bindEventListeners = function () { - var _this3 = this; - - // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. - this.body.emitter.on('_dataChanged', function () { - // update shortcut lists - _this3._updateVisibleIndices(); - _this3.physics.updatePhysicsData(); - _this3.body.emitter.emit('_requestRedraw'); - // call the dataUpdated event because the only difference between the two is the updating of the indices - _this3.body.emitter.emit('_dataUpdated'); - }); - - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on('_dataUpdated', function () { - // update values - _this3._updateValueRange(_this3.body.nodes); - _this3._updateValueRange(_this3.body.edges); - // start simulation (can be called safely, even if already running) - _this3.body.emitter.emit('startSimulation'); - _this3.body.emitter.emit('_requestRedraw'); - }); - }; - - /** - * Set nodes and edges, and optionally options as well. * - * @param {Object} data Object containing parameters: - * {Array | DataSet | DataView} [nodes] Array with nodes - * {Array | DataSet | DataView} [edges] Array with edges - * {String} [dot] String containing data in DOT format - * {String} [gephi] String containing data in gephi JSON format - * {Options} [options] Object with options + * @param groupIds + * @param groupsData + * @private */ - Network.prototype.setData = function (data) { - // reset the physics engine. - this.body.emitter.emit('resetPhysics'); - this.body.emitter.emit('_resetData'); - - // unselect all to ensure no selections from old data are carried over. - this.selectionHandler.unselectAll(); + 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; - if (data && data.dot && (data.nodes || data.edges)) { - throw new SyntaxError('Data must contain either parameter "dot" or ' + ' parameter pair "nodes" and "edges", but not both.'); - } + // 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))); - // set options - this.setOptions(data && data.options); - // set all data - if (data && data.dot) { - console.log('The dot property has been depricated. Please use the static convertDot method to convert DOT into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertDot(dotString);'); - // parse DOT file - var dotData = dotparser.DOTToGraph(data.dot); - this.setData(dotData); - return; - } else if (data && data.gephi) { - // parse DOT file - console.log('The gephi property has been depricated. Please use the static convertGephi method to convert gephi into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertGephi(gephiJson);'); - var gephiData = gephiParser.parseGephi(data.gephi); - this.setData(gephiData); - return; - } else { - this.nodesHandler.setData(data && data.nodes, true); - this.edgesHandler.setData(data && data.edges, true); + var sampledData = []; + for (var j = 0; j < amountOfPoints; j += increment) { + sampledData.push(dataContainer[j]); + } + groupsData[groupIds[i]] = sampledData; + } + } + } } - - // emit change in data - this.body.emitter.emit('_dataChanged'); - - // find a stable position or start animating to a stable position - this.body.emitter.emit('initPhysics'); }; /** - * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. - * var network = new vis.Network(..); - * network.destroy(); - * network = null; + * + * + * @param {array} groupIds + * @param {object} groupsData + * @param {object} groupRanges | this is being filled here + * @private */ - Network.prototype.destroy = function () { - this.body.emitter.emit('destroy'); - // clear events - this.body.emitter.off(); - this.off(); - - // delete modules - delete this.groups; - delete this.canvas; - delete this.selectionHandler; - delete this.interactionHandler; - delete this.view; - delete this.renderer; - delete this.physics; - delete this.layoutEngine; - delete this.clustering; - delete this.manipulation; - delete this.nodesHandler; - delete this.edgesHandler; - delete this.configurator; - delete this.images; + LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { + var groupData, group, i; + var combinedDataLeft = []; + var combinedDataRight = []; + var options; + if (groupIds.length > 0) { + for (i = 0; i < groupIds.length; i++) { + groupData = groupsData[groupIds[i]]; + options = this.groups[groupIds[i]].options; + if (groupData.length > 0) { + group = this.groups[groupIds[i]]; + // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. + if (options.stack === true && options.style === 'bar') { + if (options.yAxisOrientation === 'left') { + combinedDataLeft = combinedDataLeft.concat(group.getData(groupData)); + } else { + combinedDataRight = combinedDataRight.concat(group.getData(groupData)); + } + } else { + groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); + } + } + } - for (var nodeId in this.body.nodes) { - delete this.body.nodes[nodeId]; - } - for (var edgeId in this.body.edges) { - delete this.body.edges[edgeId]; + // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. + BarFunctions.getStackedYRange(combinedDataLeft, groupRanges, groupIds, '__barStackLeft', 'left'); + BarFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__barStackRight', 'right'); + // if line graphs are stacked, their range need to be handled differently and accumulated over all groups. + //LineFunctions.getStackedYRange(combinedDataLeft , groupRanges, groupIds, '__lineStackLeft' , 'left' ); + //LineFunctions.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__lineStackRight', 'right'); } - - // remove the container and everything inside it recursively - util.recursiveDOMDelete(this.body.container); }; /** - * Update the values of all object in the given array according to the current - * value range of the objects in the array. - * @param {Object} obj An object containing a set of Edges or Nodes - * The objects must have a method getValue() and - * setValueRange(min, max). + * 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 */ - Network.prototype._updateValueRange = function (obj) { - var id; - - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; - var valueTotal = 0; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = valueMin === undefined ? value : Math.min(value, valueMin); - valueMax = valueMax === undefined ? value : Math.max(value, valueMax); - valueTotal += value; + LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) { + var resized = false; + var yAxisLeftUsed = false; + var yAxisRightUsed = false; + var minLeft = 1000000000, + minRight = 1000000000, + maxLeft = -1000000000, + maxRight = -1000000000, + minVal, + maxVal; + // if groups are present + if (groupIds.length > 0) { + // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop. + for (var i = 0; i < groupIds.length; i++) { + var group = this.groups[groupIds[i]]; + if (group && group.options.yAxisOrientation != 'right') { + yAxisLeftUsed = true; + minLeft = 1000000000; + maxLeft = -1000000000; + } else if (group && group.options.yAxisOrientation) { + yAxisRightUsed = true; + minRight = 1000000000; + maxRight = -1000000000; } } - } - // adjust the range of all objects - if (valueMin !== undefined && valueMax !== undefined) { - for (id in obj) { - if (obj.hasOwnProperty(id)) { - obj[id].setValueRange(valueMin, valueMax, valueTotal); + // if there are items: + 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 (groupRanges[groupIds[i]].yAxisOrientation != 'right') { + 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; + } + } } } - } - }; - /** - * Returns true when the Network is active. - * @returns {boolean} - */ - Network.prototype.isActive = function () { - return !this.activator || this.activator.active; - }; + if (yAxisLeftUsed == true) { + this.yAxisLeft.setRange(minLeft, maxLeft); + } + if (yAxisRightUsed == true) { + this.yAxisRight.setRange(minRight, maxRight); + } + } + resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized; + resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized; - Network.prototype.setSize = function () { - return this.canvas.setSize.apply(this.canvas, arguments); - }; - Network.prototype.canvasToDOM = function () { - return this.canvas.canvasToDOM.apply(this.canvas, arguments); - }; - Network.prototype.DOMtoCanvas = function () { - return this.canvas.DOMtoCanvas(this.canvas, arguments); - }; - Network.prototype.findNode = function () { - return this.clustering.findNode.apply(this.clustering, arguments); - }; - Network.prototype.isCluster = function () { - return this.clustering.isCluster.apply(this.clustering, arguments); - }; - Network.prototype.openCluster = function () { - return this.clustering.openCluster.apply(this.clustering, arguments); - }; - Network.prototype.cluster = function () { - return this.clustering.cluster.apply(this.clustering, arguments); - }; - Network.prototype.getNodesInCluster = function () { - return this.clustering.getNodesInCluster.apply(this.clustering, arguments); - }; - Network.prototype.clusterByConnection = function () { - return this.clustering.clusterByConnection.apply(this.clustering, arguments); - }; - Network.prototype.clusterByHubsize = function () { - return this.clustering.clusterByHubsize.apply(this.clustering, arguments); - }; - Network.prototype.clusterOutliers = function () { - return this.clustering.clusterOutliers.apply(this.clustering, arguments); - }; - Network.prototype.getSeed = function () { - return this.layoutEngine.getSeed.apply(this.layoutEngine, arguments); - }; - Network.prototype.enableEditMode = function () { - return this.manipulation.enableEditMode.apply(this.manipulation, arguments); - }; - Network.prototype.disableEditMode = function () { - return this.manipulation.disableEditMode.apply(this.manipulation, arguments); - }; - Network.prototype.addNodeMode = function () { - return this.manipulation.addNodeMode.apply(this.manipulation, arguments); - }; - Network.prototype.editNode = function () { - return this.manipulation.editNode.apply(this.manipulation, arguments); - }; - Network.prototype.editNodeMode = function () { - console.log('Depricated: Please use editNode instead of editNodeMode.');return this.manipulation.editNode.apply(this.manipulation, arguments); - }; - Network.prototype.addEdgeMode = function () { - return this.manipulation.addEdgeMode.apply(this.manipulation, arguments); - }; - Network.prototype.editEdgeMode = function () { - return this.manipulation.editEdgeMode.apply(this.manipulation, arguments); - }; - Network.prototype.deleteSelected = function () { - return this.manipulation.deleteSelected.apply(this.manipulation, arguments); - }; - Network.prototype.getPositions = function () { - return this.nodesHandler.getPositions.apply(this.nodesHandler, arguments); - }; - Network.prototype.storePositions = function () { - return this.nodesHandler.storePositions.apply(this.nodesHandler, arguments); - }; - Network.prototype.getBoundingBox = function () { - return this.nodesHandler.getBoundingBox.apply(this.nodesHandler, arguments); - }; - Network.prototype.getConnectedNodes = function (objectId) { - if (this.body.nodes[objectId] !== undefined) { - return this.nodesHandler.getConnectedNodes.apply(this.nodesHandler, arguments); + if (yAxisRightUsed == true && yAxisLeftUsed == true) { + this.yAxisLeft.drawIcons = true; + this.yAxisRight.drawIcons = true; } else { - return this.edgesHandler.getConnectedNodes.apply(this.edgesHandler, arguments); + this.yAxisLeft.drawIcons = false; + this.yAxisRight.drawIcons = false; } - }; - Network.prototype.getConnectedEdges = function () { - return this.nodesHandler.getConnectedEdges.apply(this.nodesHandler, arguments); - }; - Network.prototype.startSimulation = function () { - return this.physics.startSimulation.apply(this.physics, arguments); - }; - Network.prototype.stopSimulation = function () { - return this.physics.stopSimulation.apply(this.physics, arguments); - }; - Network.prototype.stabilize = function () { - return this.physics.stabilize.apply(this.physics, arguments); - }; - Network.prototype.getSelection = function () { - return this.selectionHandler.getSelection.apply(this.selectionHandler, arguments); - }; - Network.prototype.getSelectedNodes = function () { - return this.selectionHandler.getSelectedNodes.apply(this.selectionHandler, arguments); - }; - Network.prototype.getSelectedEdges = function () { - return this.selectionHandler.getSelectedEdges.apply(this.selectionHandler, arguments); - }; - Network.prototype.getNodeAt = function () { - var node = this.selectionHandler.getNodeAt.apply(this.selectionHandler, arguments); - if (node !== undefined && node.id !== undefined) { - return node.id; + this.yAxisRight.master = !yAxisLeftUsed; + if (this.yAxisRight.master == false) { + if (yAxisRightUsed == true) { + this.yAxisLeft.lineOffset = this.yAxisRight.width; + } else { + this.yAxisLeft.lineOffset = 0; + } + + resized = this.yAxisLeft.redraw() || resized; + this.yAxisRight.stepPixels = this.yAxisLeft.stepPixels; + this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing; + this.yAxisRight.amountOfSteps = this.yAxisLeft.amountOfSteps; + resized = this.yAxisRight.redraw() || resized; + } else { + resized = this.yAxisRight.redraw() || resized; } - return node; - }; - Network.prototype.getEdgeAt = function () { - var edge = this.selectionHandler.getEdgeAt.apply(this.selectionHandler, arguments); - if (edge !== undefined && edge.id !== undefined) { - return edge.id; + + // clean the accumulated lists + var tempGroups = ['__barStackLeft', '__barStackRight', '__lineStackLeft', '__lineStackRight']; + for (var i = 0; i < tempGroups.length; i++) { + if (groupIds.indexOf(tempGroups[i]) != -1) { + groupIds.splice(groupIds.indexOf(tempGroups[i]), 1); + } } - return edge; - }; - Network.prototype.selectNodes = function () { - return this.selectionHandler.selectNodes.apply(this.selectionHandler, arguments); - }; - Network.prototype.selectEdges = function () { - return this.selectionHandler.selectEdges.apply(this.selectionHandler, arguments); - }; - Network.prototype.unselectAll = function () { - return this.selectionHandler.unselectAll.apply(this.selectionHandler, arguments); - }; - Network.prototype.redraw = function () { - return this.renderer.redraw.apply(this.renderer, arguments); - }; - Network.prototype.getScale = function () { - return this.view.getScale.apply(this.view, arguments); - }; - Network.prototype.getViewPosition = function () { - return this.view.getViewPosition.apply(this.view, arguments); - }; - Network.prototype.fit = function () { - return this.view.fit.apply(this.view, arguments); - }; - Network.prototype.moveTo = function () { - return this.view.moveTo.apply(this.view, arguments); - }; - Network.prototype.focus = function () { - return this.view.focus.apply(this.view, arguments); + + return resized; }; - Network.prototype.releaseNode = function () { - return this.view.releaseNode.apply(this.view, arguments); + + /** + * 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.hidden == false) { + axis.hide(); + changed = true; + } + } else { + if (!axis.dom.frame.parentNode && axis.hidden == true) { + axis.show(); + changed = true; + } + } + return changed; }; - module.exports = Network; - -/***/ }, -/* 61 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - Object.defineProperty(exports, "__esModule", { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + /** + * 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; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + for (var i = 0; i < datapoints.length; i++) { + xValue = toScreen(datapoints[i].x) + this.props.width; + yValue = datapoints[i].y; + extractedData.push({ x: xValue, y: yValue }); + } - var util = __webpack_require__(9); + return extractedData; + }; /** - * @class Groups - * This class can store groups and options specific for groups. + * 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 + * @param group + * @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; + } - var Groups = (function () { - function Groups() { - _classCallCheck(this, Groups); + for (var i = 0; i < datapoints.length; i++) { + var labelValue = datapoints[i].label ? datapoints[i].label : null; + xValue = toScreen(datapoints[i].x) + this.props.width; + yValue = Math.round(axis.convertValue(datapoints[i].y)); + extractedData.push({ x: xValue, y: yValue, label: labelValue }); + } - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; + group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); - this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue - { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow - { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red - { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green - { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta - { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple - { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange - { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue - { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink - { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + return extractedData; + }; - { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red + module.exports = LineGraph; - { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange - { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue - { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green - { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta - { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple - { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue - { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink - { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint +/***/ }, +/* 57 */ +/***/ function(module, exports, __webpack_require__) { - { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } } // 20:bright red - ]; + 'use strict'; - this.options = {}; - this.defaultOptions = { - useDefaultGroups: true - }; - util.extend(this.options, this.defaultOptions); - } + var util = __webpack_require__(14); + var DOMutil = __webpack_require__(19); + var Component = __webpack_require__(33); + var DataStep = __webpack_require__(58); - _createClass(Groups, [{ - key: "setOptions", - value: function setOptions(options) { - var optionFields = ["useDefaultGroups"]; + /** + * 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; - if (options !== undefined) { - for (var groupName in options) { - if (options.hasOwnProperty(groupName)) { - if (optionFields.indexOf(groupName) === -1) { - var group = options[groupName]; - this.add(groupName, group); - } - } - } - } + 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, + alignZeros: true, + left: { + range: { min: undefined, max: undefined }, + format: function format(value) { + return value; + }, + title: { text: undefined, style: undefined } + }, + right: { + range: { min: undefined, max: undefined }, + format: function format(value) { + return value; + }, + title: { text: undefined, style: undefined } } - }, { - key: "clear", + }; - /** - * Clear all groups - */ - value: function clear() { - this.groups = {}; - this.groupsArray = []; - } - }, { - key: "get", + this.linegraphOptions = linegraphOptions; + this.linegraphSVG = svg; + this.props = {}; + this.DOMelements = { // dynamic elements + lines: {}, + labels: {}, + title: {} + }; - /** - * get group options of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group options - */ - value: function get(groupname) { - var group = this.groups[groupname]; - if (group === undefined) { - if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; - } else { - // create new group - var index = this.defaultIndex % this.defaultGroups.length; - this.defaultIndex++; - group = {}; - group.color = this.defaultGroups[index]; - this.groups[groupname] = group; - } - } + this.dom = {}; - return group; - } - }, { - key: "add", + this.range = { start: 0, end: 0 }; - /** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object - */ - value: function add(groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; - } - }]); + this.options = util.extend({}, this.defaultOptions); + this.conversionFactor = 1; - return Groups; - })(); + this.setOptions(options); + this.width = Number(('' + this.options.width).replace('px', '')); + this.minWidth = this.width; + this.height = this.linegraphSVG.offsetHeight; + this.hidden = false; - exports["default"] = Groups; - module.exports = exports["default"]; + this.stepPixels = 25; + this.zeroCrossing = -1; + this.amountOfSteps = -1; -/***/ }, -/* 62 */ -/***/ function(module, exports, __webpack_require__) { + this.lineOffset = 0; + this.master = true; + this.svgElements = {}; + this.iconsRemoved = false; - 'use strict'; + this.groups = {}; + this.amountOfGroups = 0; - Object.defineProperty(exports, '__esModule', { - value: true - }); + // create the HTML DOM + this._create(); - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var me = this; + this.body.emitter.on('verticalDrag', function () { + me.dom.lineContainer.style.top = me.body.domProps.scrollTop + 'px'; + }); + } - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + DataAxis.prototype = new Component(); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + DataAxis.prototype.addGroup = function (label, graphOptions) { + if (!this.groups.hasOwnProperty(label)) { + this.groups[label] = graphOptions; + } + this.amountOfGroups += 1; + }; - var _componentsNode = __webpack_require__(63); + DataAxis.prototype.updateGroup = function (label, graphOptions) { + this.groups[label] = graphOptions; + }; - var _componentsNode2 = _interopRequireDefault(_componentsNode); + DataAxis.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; + } + }; - var _componentsSharedLabel = __webpack_require__(64); + 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', 'left', 'right', 'alignZeros']; + util.selectiveExtend(fields, this.options, options); - var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); + this.minWidth = Number(('' + this.options.width).replace('px', '')); - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); + if (redraw === true && this.dom.frame) { + this.hide(); + this.show(); + } + } + }; - var NodesHandler = (function () { - function NodesHandler(body, images, groups, layoutEngine) { - var _this = this; + /** + * Create the HTML DOM for the DataAxis + */ + DataAxis.prototype._create = function () { + this.dom.frame = document.createElement('div'); + this.dom.frame.style.width = this.options.width; + this.dom.frame.style.height = this.height; - _classCallCheck(this, NodesHandler); + this.dom.lineContainer = document.createElement('div'); + this.dom.lineContainer.style.width = '100%'; + this.dom.lineContainer.style.height = this.height; + this.dom.lineContainer.style.position = 'relative'; - this.body = body; - this.images = images; - this.groups = groups; - this.layoutEngine = layoutEngine; + // 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); + }; - // create the node API in the body container - this.body.functions.createNode = this.create.bind(this); + DataAxis.prototype._redrawGroupIcons = function () { + DOMutil.prepareElements(this.svgElements); - this.nodesListeners = { - add: function add(event, params) { - _this.add(params.items); - }, - update: function update(event, params) { - _this.update(params.items, params.data); - }, - remove: function remove(event, params) { - _this.remove(params.items); - } - }; + var x; + var iconWidth = this.options.iconWidth; + var iconHeight = 15; + var iconOffset = 4; + var y = iconOffset + 0.5 * iconHeight; - this.options = {}; - this.defaultOptions = { - borderWidth: 1, - borderWidthSelected: 2, - brokenImage: undefined, - color: { - border: '#2B7CE9', - background: '#97C2FC', - highlight: { - border: '#2B7CE9', - background: '#D2E5FF' - }, - hover: { - border: '#2B7CE9', - background: '#D2E5FF' - } - }, - fixed: { - x: false, - y: false - }, - font: { - color: '#343434', - size: 14, // px - face: 'arial', - background: 'none', - strokeWidth: 0, // px - strokeColor: '#ffffff', - align: 'horizontal' - }, - group: undefined, - hidden: false, - icon: { - face: 'FontAwesome', //'FontAwesome', - code: undefined, //'\uf007', - size: 50, //50, - color: '#2B7CE9' //'#aa00ff' - }, - image: undefined, // --> URL - label: undefined, - level: undefined, - mass: 1, - physics: true, - scaling: { - min: 10, - max: 30, - label: { - enabled: false, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 5 - }, - customScalingFunction: function customScalingFunction(min, max, total, value) { - if (max === min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - shadow: { - enabled: false, - size: 10, - x: 5, - y: 5 - }, - shape: 'ellipse', - size: 25, - title: undefined, - value: undefined, - x: undefined, - y: undefined - }; - util.extend(this.options, this.defaultOptions); + if (this.options.orientation === 'left') { + x = iconOffset; + } else { + x = this.width - iconWidth - iconOffset; + } - this.bindEventListeners(); + var groupArray = Object.keys(this.groups); + groupArray.sort(function (a, b) { + return a < b ? -1 : 1; + }); + + for (var i = 0; i < groupArray.length; i++) { + var groupId = groupArray[i]; + 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; + } } - _createClass(NodesHandler, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this2 = this; + DOMutil.cleanupElements(this.svgElements); + this.iconsRemoved = false; + }; - // refresh the nodes. Used when reverting from hierarchical layout - this.body.emitter.on('refreshNodes', this.refresh.bind(this)); - this.body.emitter.on('refresh', this.refresh.bind(this)); - this.body.emitter.on('destroy', function () { - delete _this2.body.functions.createNode; - delete _this2.nodesListeners.add; - delete _this2.nodesListeners.update; - delete _this2.nodesListeners.remove; - delete _this2.nodesListeners; - }); + DataAxis.prototype._cleanupIcons = function () { + if (this.iconsRemoved === false) { + DOMutil.prepareElements(this.svgElements); + DOMutil.cleanupElements(this.svgElements); + this.iconsRemoved = true; + } + }; + + /** + * Create the HTML DOM for the DataAxis + */ + DataAxis.prototype.show = function () { + this.hidden = false; + 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); } - }, { - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - _componentsNode2['default'].parseOptions(this.options, options); + } - // update the shape in all nodes - if (options.shape !== undefined) { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].updateShape(); - } - } - } + if (!this.dom.lineContainer.parentNode) { + this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); + } + }; + + /** + * Create the HTML DOM for the DataAxis + */ + DataAxis.prototype.hide = function () { + this.hidden = true; + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } + + if (this.dom.lineContainer.parentNode) { + this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); + } + }; + + /** + * Set a range (start and end) + * @param end + * @param start + * @param end + */ + DataAxis.prototype.setRange = function (start, end) { + if (this.master === false && this.options.alignZeros === true && this.zeroCrossing != -1) { + if (start > 0) { + start = 0; + } + } + this.range.start = start; + this.range.end = end; + }; - // update the shape size in all nodes - if (options.font !== undefined) { - _componentsSharedLabel2['default'].parseOptions(this.options.font, options); - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].updateLabelModule(); - this.body.nodes[nodeId]._reset(); - } - } - } + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + DataAxis.prototype.redraw = function () { + var resized = false; + var activeGroups = 0; - // update the shape size in all nodes - if (options.size !== undefined) { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId]._reset(); - } - } - } + // Make sure the line container adheres to the vertical scrolling. + this.dom.lineContainer.style.top = this.body.domProps.scrollTop + 'px'; - // update the state of the letiables if needed - if (options.hidden !== undefined || options.physics !== undefined) { - this.body.emitter.emit('_dataChanged'); - } + 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++; } } - }, { - key: 'setData', - - /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. - * @private - */ - value: function setData(nodes) { - var _this3 = this; + } + if (this.amountOfGroups === 0 || activeGroups === 0) { + this.hide(); + } else { + this.show(); + this.height = Number(this.linegraphSVG.style.height.replace('px', '')); - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + // svg offsetheight did not work in firefox and explorer... + this.dom.lineContainer.style.height = this.height + 'px'; + this.width = this.options.visible === true ? Number(('' + this.options.width).replace('px', '')) : 0; - var oldNodesData = this.body.data.nodes; + var props = this.props; + var frame = this.dom.frame; - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.body.data.nodes = nodes; - } else if (Array.isArray(nodes)) { - this.body.data.nodes = new DataSet(); - this.body.data.nodes.add(nodes); - } else if (!nodes) { - this.body.data.nodes = new DataSet(); - } else { - throw new TypeError('Array or DataSet expected'); - } + // update classname + frame.className = 'vis-data-axis'; - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.off(event, callback); - }); - } + // calculate character width and height + this._calculateCharSize(); - // remove drawn nodes - this.body.nodes = {}; + var orientation = this.options.orientation; + var showMinorLabels = this.options.showMinorLabels; + var showMajorLabels = this.options.showMajorLabels; - if (this.body.data.nodes) { - (function () { - // subscribe to new dataset - var me = _this3; - util.forEach(_this3.nodesListeners, function (callback, event) { - me.body.data.nodes.on(event, callback); - }); + // determine the width and height of the elements for the axis + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - // draw all new nodes - var ids = _this3.body.data.nodes.getIds(); - _this3.add(ids, true); - })(); - } + 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; - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } + // 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'; + this.props.width = this.body.domProps.left.width; + this.props.height = this.body.domProps.left.height; + } 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'; + this.props.width = this.body.domProps.right.width; + this.props.height = this.body.domProps.right.height; } - }, { - key: 'add', - /** - * Add nodes - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + resized = this._redrawLabels(); + resized = this._isResized() || resized; - var id = undefined; - var newNodes = []; - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - var properties = this.body.data.nodes.get(id); - var node = this.create(properties); - newNodes.push(node); - this.body.nodes[id] = node; // note: this may replace an existing node - } + if (this.options.icons === true) { + this._redrawGroupIcons(); + } else { + this._cleanupIcons(); + } - this.layoutEngine.positionInitially(newNodes); + this._redrawTitle(orientation); + } + return resized; + }; - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'update', + /** + * Repaint major and minor text labels and vertical grid lines + * @private + */ + DataAxis.prototype._redrawLabels = function () { + var resized = false; + DOMutil.prepareElements(this.DOMelements.lines); + DOMutil.prepareElements(this.DOMelements.labels); + var orientation = this.options['orientation']; - /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids, changedData) { - var nodes = this.body.nodes; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = changedData[i]; - if (node !== undefined) { - // update node - dataChanged = node.setOptions(data); - } else { - dataChanged = true; - // create node - node = this.create(data); - nodes[id] = node; - } - } - if (dataChanged === true) { - this.body.emitter.emit('_dataChanged'); + // get the range for the slaved axis + var step; + if (this.master === false) { + var stepSize, rangeStart, rangeEnd, minimumStep; + if (this.zeroCrossing !== -1 && this.options.alignZeros === true) { + if (this.range.end > 0) { + stepSize = this.range.end / this.zeroCrossing; // size of one step + rangeStart = this.range.end - this.amountOfSteps * stepSize; + rangeEnd = this.range.end; } else { - this.body.emitter.emit('_dataUpdated'); + // all of the range (including start) has to be done before the zero crossing. + stepSize = -1 * this.range.start / (this.amountOfSteps - this.zeroCrossing); // absolute size of a step + rangeStart = this.range.start; + rangeEnd = this.range.start + stepSize * this.amountOfSteps; } + } else { + rangeStart = this.range.start; + rangeEnd = this.range.end; } - }, { - key: 'remove', - - /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var nodes = this.body.nodes; + minimumStep = this.stepPixels; + } else { + // calculate range and step (step such that we have space for 7 characters per label) + minimumStep = this.props.majorCharHeight; + rangeStart = this.range.start; + rangeEnd = this.range.end; + } - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - delete nodes[id]; - } + this.step = step = new DataStep(rangeStart, rangeEnd, minimumStep, this.dom.frame.offsetHeight, this.options[this.options.orientation].range, this.options[this.options.orientation].format, this.master === false && this.options.alignZeros // does the step have to align zeros? only if not master and the options is on + ); - this.body.emitter.emit('_dataChanged'); + // the slave axis needs to use the same horizontal lines as the master axis. + if (this.master === true) { + this.stepPixels = this.dom.frame.offsetHeight / step.marginRange * step.step; + this.amountOfSteps = Math.ceil(this.dom.frame.offsetHeight / this.stepPixels); + } else { + // align with zero + if (this.options.alignZeros === true && this.zeroCrossing !== -1) { + // distance is the amount of steps away from the zero crossing we are. + var distance = (step.current - this.zeroCrossing * step.step) / step.step; + this.step.shift(distance); } - }, { - key: 'create', + } - /** - * create a node - * @param properties - * @param constructorClass - */ - value: function create(properties) { - var constructorClass = arguments[1] === undefined ? _componentsNode2['default'] : arguments[1]; + // value at the bottom of the SVG + this.valueAtBottom = step.marginEnd; - return new constructorClass(properties, this.body, this.images, this.groups, this.options); - } - }, { - key: 'refresh', - value: function refresh() { - var nodes = this.body.nodes; - for (var nodeId in nodes) { - var node = undefined; - if (nodes.hasOwnProperty(nodeId)) { - node = nodes[nodeId]; - } - var data = this.body.data.nodes._data[nodeId]; - if (node !== undefined && data !== undefined) { - node.setOptions({ fixed: false }); - node.setOptions(data); - } + this.maxLabelSize = 0; + var y = 0; // init value + var stepIndex = 0; // init value + var isMajor = false; // init value + while (stepIndex < this.amountOfSteps) { + y = Math.round(stepIndex * this.stepPixels); + isMajor = step.isMajor(); + + if (stepIndex > 0 && stepIndex !== this.amountOfSteps) { + if (this.options['showMinorLabels'] && isMajor === false || this.master === false && this.options['showMinorLabels'] === true) { + this._redrawLabel(y - 2, step.getCurrent(), orientation, 'vis-y-axis vis-minor', this.props.minorCharHeight); } - } - }, { - key: 'getPositions', - /** - * Returns the positions of the nodes. - * @param ids --> optional, can be array of nodeIds, can be string - * @returns {{}} - */ - value: function getPositions(ids) { - var dataArray = {}; - if (ids !== undefined) { - if (Array.isArray(ids) === true) { - for (var i = 0; i < ids.length; i++) { - if (this.body.nodes[ids[i]] !== undefined) { - var node = this.body.nodes[ids[i]]; - dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } else { - if (this.body.nodes[ids] !== undefined) { - var node = this.body.nodes[ids]; - dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; - } + 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, 'vis-y-axis vis-major', this.props.majorCharHeight); } + this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-major', this.options.majorLinesOffset, this.props.majorLineWidth); } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } + this._redrawLine(y, orientation, 'vis-grid vis-horizontal vis-minor', this.options.minorLinesOffset, this.props.minorLineWidth); } - return dataArray; } - }, { - key: 'storePositions', - - /** - * Load the XY positions of the nodes into the dataset. - */ - value: function storePositions() { - // todo: add support for clusters and hierarchical. - var dataArray = []; - var dataset = this.body.data.nodes.getDataSet(); - for (var nodeId in dataset._data) { - if (dataset._data.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) { - dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) }); - } - } - } - dataset.update(dataArray); + // get zero crossing + if (this.master === true && step.current === 0) { + this.zeroCrossing = stepIndex; } - }, { - key: 'getBoundingBox', - /** - * get the bounding box of a node. - * @param nodeId - * @returns {j|*} - */ - value: function getBoundingBox(nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].shape.boundingBox; - } + step.next(); + stepIndex += 1; + } + + // get zero crossing if it's the last step + if (this.master === true && step.current === 0) { + this.zeroCrossing = stepIndex; + } + + this.conversionFactor = this.stepPixels / step.step; + + // Note that title is rotated, so we're using the height, not width! + var titleWidth = 0; + if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) { + titleWidth = this.props.titleCharHeight; + } + var offset = this.options.icons === true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15; + + // this will resize the yAxis to accommodate 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(); + resized = 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(); + resized = true; + } else { + DOMutil.cleanupElements(this.DOMelements.lines); + DOMutil.cleanupElements(this.DOMelements.labels); + resized = false; + } + + return resized; + }; + + DataAxis.prototype.convertValue = function (value) { + var invertedValue = this.valueAtBottom - value; + var convertedValue = invertedValue * this.conversionFactor; + return convertedValue; + }; + + DataAxis.prototype.screenToValue = function (x) { + return this.valueAtBottom - x / this.conversionFactor; + }; + + /** + * Create a label for the axis at position x + * @private + * @param y + * @param text + * @param orientation + * @param className + * @param characterHeight + */ + DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) { + // reuse redundant label + var label = DOMutil.getDOMElement('div', this.DOMelements.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'; + + text += ''; + + var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); + if (this.maxLabelSize < text.length * largestWidth) { + this.maxLabelSize = text.length * largestWidth; + } + }; + + /** + * Create a minor line for the axis at position y + * @param y + * @param orientation + * @param className + * @param offset + * @param width + */ + DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) { + if (this.master === true) { + var line = DOMutil.getDOMElement('div', this.DOMelements.lines, this.dom.lineContainer); //this.dom.redundant.lines.shift(); + line.className = className; + line.innerHTML = ''; + + if (orientation === 'left') { + line.style.left = this.width - offset + 'px'; + } else { + line.style.right = this.width - offset + 'px'; } - }, { - key: 'getConnectedNodes', - /** - * Get the Ids of nodes connected to this node. - * @param nodeId - * @returns {Array} - */ - value: function getConnectedNodes(nodeId) { - var nodeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - var nodeObj = {}; // used to quickly check if node already exists - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - if (edge.toId == nodeId) { - // these are double equals since ids can be numeric or string - if (nodeObj[edge.fromId] === undefined) { - nodeList.push(edge.fromId); - nodeObj[edge.fromId] = true; - } - } else if (edge.fromId == nodeId) { - // these are double equals since ids can be numeric or string - if (nodeObj[edge.toId] === undefined) { - nodeList.push(edge.toId); - nodeObj[edge.toId] = true; - } - } - } - } - return nodeList; + line.style.width = width + 'px'; + line.style.top = y + 'px'; + } + }; + + /** + * Create a title for the axis + * @private + * @param orientation + */ + DataAxis.prototype._redrawTitle = function (orientation) { + DOMutil.prepareElements(this.DOMelements.title); + + // Check if the title is defined for this axes + if (this.options[orientation].title !== undefined && this.options[orientation].title.text !== undefined) { + var title = DOMutil.getDOMElement('div', this.DOMelements.title, this.dom.frame); + title.className = 'vis-y-axis vis-title vis-' + orientation; + title.innerHTML = this.options[orientation].title.text; + + // Add style - if provided + if (this.options[orientation].title.style !== undefined) { + util.addCssText(title, this.options[orientation].title.style); } - }, { - key: 'getConnectedEdges', - /** - * Get the ids of the edges connected to this node. - * @param nodeId - * @returns {*} - */ - value: function getConnectedEdges(nodeId) { - var edgeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - for (var i = 0; i < node.edges.length; i++) { - edgeList.push(node.edges[i].id); - } - } else { - console.log('NodeId provided for getConnectedEdges does not exist. Provided: ', nodeId); - } - return edgeList; + if (orientation === 'left') { + title.style.left = this.props.titleCharHeight + 'px'; + } else { + title.style.right = this.props.titleCharHeight + 'px'; } - }]); - return NodesHandler; - })(); + title.style.width = this.height + 'px'; + } - exports['default'] = NodesHandler; - module.exports = exports['default']; + // we need to clean up in case we did not use all elements. + DOMutil.cleanupElements(this.DOMelements.title); + }; + + /** + * Determine the size of text on the axis (both major and minor axis). + * The size is calculated only once and then cached in this.props. + * @private + */ + DataAxis.prototype._calculateCharSize = function () { + // determine the char width and height on the minor axis + if (!('minorCharHeight' in this.props)) { + var textMinor = document.createTextNode('0'); + var measureCharMinor = document.createElement('div'); + measureCharMinor.className = 'vis-y-axis vis-minor vis-measure'; + measureCharMinor.appendChild(textMinor); + this.dom.frame.appendChild(measureCharMinor); + + this.props.minorCharHeight = measureCharMinor.clientHeight; + this.props.minorCharWidth = measureCharMinor.clientWidth; + + this.dom.frame.removeChild(measureCharMinor); + } + + if (!('majorCharHeight' in this.props)) { + var textMajor = document.createTextNode('0'); + var measureCharMajor = document.createElement('div'); + measureCharMajor.className = 'vis-y-axis vis-major vis-measure'; + measureCharMajor.appendChild(textMajor); + this.dom.frame.appendChild(measureCharMajor); + + this.props.majorCharHeight = measureCharMajor.clientHeight; + this.props.majorCharWidth = measureCharMajor.clientWidth; + + this.dom.frame.removeChild(measureCharMajor); + } + + if (!('titleCharHeight' in this.props)) { + var textTitle = document.createTextNode('0'); + var measureCharTitle = document.createElement('div'); + measureCharTitle.className = 'vis-y-axis vis-title vis-measure'; + measureCharTitle.appendChild(textTitle); + this.dom.frame.appendChild(measureCharTitle); + + this.props.titleCharHeight = measureCharTitle.clientHeight; + this.props.titleCharWidth = measureCharTitle.clientWidth; + + this.dom.frame.removeChild(measureCharTitle); + } + }; + + module.exports = DataAxis; /***/ }, -/* 63 */ +/* 58 */ /***/ function(module, exports, __webpack_require__) { + /** + * @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 + */ 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); + function DataStep(start, end, minimumStep, containerHeight, customRange, formattingFunction, alignZeros) { + // variables + this.current = 0; - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + this.autoScale = true; + this.stepIndex = 0; + this.step = 1; + this.scale = 1; + this.formattingFunction = formattingFunction; - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + this.marginStart; + this.marginEnd; + this.deadSpace = 0; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; - var _sharedLabel = __webpack_require__(64); + this.alignZeros = alignZeros; - var _sharedLabel2 = _interopRequireDefault(_sharedLabel); + this.setRange(start, end, minimumStep, containerHeight, customRange); + } - var _nodesShapesBox = __webpack_require__(65); + /** + * 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 = customRange.min === undefined ? this._start - 0.75 : this._start; + this._end = customRange.max === undefined ? this._end + 1 : this._end;; + } - var _nodesShapesBox2 = _interopRequireDefault(_nodesShapesBox); + if (this.autoScale === true) { + this.setMinimumStep(minimumStep, containerHeight); + } - var _nodesShapesCircle = __webpack_require__(67); + this.setFirst(customRange); + }; - var _nodesShapesCircle2 = _interopRequireDefault(_nodesShapesCircle); + /** + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in pixels + */ + DataStep.prototype.setMinimumStep = function (minimumStep, containerHeight) { + // round to floor + var range = this._end - this._start; + var safeRange = range * 1.2; + var minimumStepValue = minimumStep * (safeRange / containerHeight); + var orderOfMagnitude = Math.round(Math.log(safeRange) / Math.LN10); + + var minorStepIdx = -1; + var magnitudefactor = Math.pow(10, orderOfMagnitude); - var _nodesShapesCircularImage = __webpack_require__(69); + var start = 0; + if (orderOfMagnitude < 0) { + start = orderOfMagnitude; + } - var _nodesShapesCircularImage2 = _interopRequireDefault(_nodesShapesCircularImage); + var solutionFound = false; + for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { + magnitudefactor = Math.pow(10, i); + for (var j = 0; j < this.minorSteps.length; j++) { + var stepSize = magnitudefactor * this.minorSteps[j]; + if (stepSize >= minimumStepValue) { + solutionFound = true; + minorStepIdx = j; + break; + } + } + if (solutionFound === true) { + break; + } + } + this.stepIndex = minorStepIdx; + this.scale = magnitudefactor; + this.step = magnitudefactor * this.minorSteps[minorStepIdx]; + }; - var _nodesShapesDatabase = __webpack_require__(70); + /** + * 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 _nodesShapesDatabase2 = _interopRequireDefault(_nodesShapesDatabase); + 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; - var _nodesShapesDiamond = __webpack_require__(71); + this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; + this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; - var _nodesShapesDiamond2 = _interopRequireDefault(_nodesShapesDiamond); + // if we need to align the zero's we need to make sure that there is a zero to use. + if (this.alignZeros === true && (this.marginEnd - this.marginStart) % this.step != 0) { + this.marginEnd += this.marginEnd % this.step; + } - var _nodesShapesDot = __webpack_require__(73); + this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; + this.marginRange = this.marginEnd - this.marginStart; - var _nodesShapesDot2 = _interopRequireDefault(_nodesShapesDot); + this.current = this.marginEnd; + }; - var _nodesShapesEllipse = __webpack_require__(74); + 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; + } + }; - var _nodesShapesEllipse2 = _interopRequireDefault(_nodesShapesEllipse); + /** + * 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; + }; - var _nodesShapesIcon = __webpack_require__(75); + /** + * Do the next step + */ + DataStep.prototype.next = function () { + var prev = this.current; + this.current -= this.step; - var _nodesShapesIcon2 = _interopRequireDefault(_nodesShapesIcon); + // safety mechanism: if current time is still unchanged, move to the end + if (this.current === prev) { + this.current = this._end; + } + }; - var _nodesShapesImage = __webpack_require__(76); + /** + * Do the next step + */ + DataStep.prototype.previous = function () { + this.current += this.step; + this.marginEnd += this.step; + this.marginRange = this.marginEnd - this.marginStart; + }; - var _nodesShapesImage2 = _interopRequireDefault(_nodesShapesImage); + /** + * Get the current datetime + * @return {String} current The current date + */ + DataStep.prototype.getCurrent = function () { + // prevent round-off errors when close to zero + var current = Math.abs(this.current) < this.step / 2 ? 0 : this.current; + var returnValue = current.toPrecision(5); + if (typeof this.formattingFunction === 'function') { + returnValue = this.formattingFunction(current); + } - var _nodesShapesSquare = __webpack_require__(77); + if (typeof returnValue === 'number') { + return '' + returnValue; + } else if (typeof returnValue === 'string') { + return returnValue; + } else { + return current.toPrecision(5); + } + }; - var _nodesShapesSquare2 = _interopRequireDefault(_nodesShapesSquare); + /** + * 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; + }; - var _nodesShapesStar = __webpack_require__(78); + DataStep.prototype.shift = function (steps) { + if (steps < 0) { + for (var i = 0; i < -steps; i++) { + this.previous(); + } + } else if (steps > 0) { + for (var i = 0; i < steps; i++) { + this.next(); + } + } + }; - var _nodesShapesStar2 = _interopRequireDefault(_nodesShapesStar); + module.exports = DataStep; - var _nodesShapesText = __webpack_require__(79); +/***/ }, +/* 59 */ +/***/ function(module, exports, __webpack_require__) { - var _nodesShapesText2 = _interopRequireDefault(_nodesShapesText); + 'use strict'; - var _nodesShapesTriangle = __webpack_require__(80); + var util = __webpack_require__(14); + var DOMutil = __webpack_require__(19); + var Line = __webpack_require__(60); + var Bar = __webpack_require__(62); + var Points = __webpack_require__(61); - var _nodesShapesTriangle2 = _interopRequireDefault(_nodesShapesTriangle); + /** + * /** + * @param {object} group | the object of the group from the dataset + * @param {string} groupId | ID of the group + * @param {object} options | the default options + * @param {array} groupsUsingDefaultStyles | this array has one entree. + * It is passed as an array so it is passed by reference. + * It enumerates through the default styles + * @constructor + */ + function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { + this.id = groupId; + var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart', 'drawPoints', 'shaded', 'interpolation']; + 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; + } - var _nodesShapesTriangleDown = __webpack_require__(81); + /** + * this loads a reference to all items in this group into this group. + * @param {array} items + */ + 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; + }); + } + // typecast all items to numbers. Takes around 10ms for 500.000 items + for (var i = 0; i < this.itemsData.length; i++) { + this.itemsData[i].y = Number(this.itemsData[i].y); + } + } else { + this.itemsData = []; + } + }; - var _nodesShapesTriangleDown2 = _interopRequireDefault(_nodesShapesTriangleDown); + /** + * this is used for plotting barcharts, this way, we only have to calculate it once. + * @param pos + */ + GraphGroup.prototype.setZeroPosition = function (pos) { + this.zeroPosition = pos; + }; - var _sharedValidator = __webpack_require__(48); + /** + * set the options of the graph group over the default options. + * @param options + */ + GraphGroup.prototype.setOptions = function (options) { + if (options !== undefined) { + var fields = ['sampling', 'style', 'sort', 'yAxisOrientation', 'barChart']; + util.selectiveDeepExtend(fields, this.options, options); - var _sharedValidator2 = _interopRequireDefault(_sharedValidator); + // if the group's drawPoints is a function delegate the callback to the onRender property + if (typeof options.drawPoints == 'function') { + options.drawPoints = { + onRender: options.drawPoints + }; + } + + util.mergeOptions(this.options, options, 'interpolation'); + util.mergeOptions(this.options, options, 'drawPoints'); + util.mergeOptions(this.options, options, 'shaded'); + + if (options.interpolation) { + if (typeof options.interpolation == 'object') { + if (options.interpolation.parametrization) { + if (options.interpolation.parametrization == 'uniform') { + this.options.interpolation.alpha = 0; + } else if (options.interpolation.parametrization == 'chordal') { + this.options.interpolation.alpha = 1; + } else { + this.options.interpolation.parametrization = 'centripetal'; + this.options.interpolation.alpha = 0.5; + } + } + } + } + } - var util = __webpack_require__(9); + if (this.options.style == 'line') { + this.type = new Line(this.id, this.options); + } else if (this.options.style == 'bar') { + this.type = new Bar(this.id, this.options); + } else if (this.options.style == 'points') { + this.type = new Points(this.id, this.options); + } + }; /** - * @class Node - * A node. A node can be connected to other nodes via one or multiple edges. - * @param {object} options An object containing options for the node. All - * options are optional, except for the id. - * {number} id Id of the node. Required - * {string} label Text label for the node - * {number} x Horizontal position of the node - * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square", "icon" - * {string} image An image url - * {string} title An title text, can be HTML - * {anytype} group A group name or number - * @param {Network.Images} imagelist A list with images. Only needed - * when the node has an image - * @param {Network.Groups} grouplist A list with groups. Needed for - * retrieving group options - * @param {Object} constants An object with default values for - * example for the color + * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph + * @param group + */ + GraphGroup.prototype.update = function (group) { + this.group = group; + this.content = group.content || 'graph'; + this.className = group.className || this.className || 'vis-graph-group' + this.groupsUsingDefaultStyles[0] % 10; + this.visible = group.visible === undefined ? true : group.visible; + this.style = group.style; + this.setOptions(group.options); + }; + + /** + * draw the icon for the legend. * + * @param x + * @param y + * @param JSONcontainer + * @param SVGcontainer + * @param iconWidth + * @param iconHeight */ + GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { + var fillHeight = iconHeight * 0.5; + var path, fillPath; - var Node = (function () { - function Node(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Node); + 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', 'vis-outline'); + + if (this.options.style == 'line') { + path = DOMutil.getSVGElement('path', JSONcontainer, SVGcontainer); + path.setAttributeNS(null, 'class', this.className); + if (this.style !== undefined) { + path.setAttributeNS(null, 'style', this.style); + } + + 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 + ' vis-icon-fill'); + } + + if (this.options.drawPoints.enabled == true) { + var groupTemplate = { + style: this.options.drawPoints.style, + size: this.options.drawPoints.size, + className: this.className + }; + DOMutil.drawPoint(x + 0.5 * iconWidth, y, groupTemplate, JSONcontainer, SVGcontainer); + } + } else { + var barWidth = Math.round(0.3 * iconWidth); + var bar1Height = Math.round(0.4 * iconHeight); + var bar2Height = Math.round(0.75 * iconHeight); + + var offset = Math.round((iconWidth - 2 * barWidth) / 3); + + DOMutil.drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style); + DOMutil.drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' vis-bar', JSONcontainer, SVGcontainer, this.style); + } + }; + + /** + * return the legend entree for this group. + * + * @param iconWidth + * @param iconHeight + * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} + */ + 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 }; + }; + + GraphGroup.prototype.getYRange = function (groupData) { + return this.type.getYRange(groupData); + }; + + GraphGroup.prototype.getData = function (groupData) { + return this.type.getData(groupData); + }; + + GraphGroup.prototype.draw = function (dataset, group, framework) { + this.type.draw(dataset, group, framework); + }; - this.options = util.bridgeObject(globalOptions); - this.body = body; + module.exports = GraphGroup; - this.edges = []; // all edges connected to this node +/***/ }, +/* 60 */ +/***/ function(module, exports, __webpack_require__) { - // set defaults for the options - this.id = undefined; - this.imagelist = imagelist; - this.grouplist = grouplist; + 'use strict'; - // state options - this.x = undefined; - this.y = undefined; - this.baseSize = this.options.size; - this.baseFontSize = this.options.font.size; - this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate - this.selected = false; - this.hover = false; + var DOMutil = __webpack_require__(19); + var Points = __webpack_require__(61); - this.labelModule = new _sharedLabel2['default'](this.body, this.options); - this.setOptions(options); + function Line(groupId, options) { + this.groupId = groupId; + this.options = options; + } + + Line.prototype.getData = function (groupData) { + var combinedData = []; + for (var j = 0; j < groupData.length; j++) { + combinedData.push({ + x: groupData[j].x, + y: groupData[j].y, + groupId: this.groupId + }); } + return combinedData; + }; - _createClass(Node, [{ - key: 'attachEdge', + Line.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + }; - /** - * Attach a edge to the node - * @param {Edge} edge - */ - value: function attachEdge(edge) { - if (this.edges.indexOf(edge) === -1) { - this.edges.push(edge); + Line.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { + if (combinedData.length > 0) { + // sort by time and by group + combinedData.sort(function (a, b) { + if (a.x === b.x) { + return a.groupId < b.groupId ? -1 : 1; + } else { + return a.x - b.x; } - } - }, { - key: 'detachEdge', + }); + var intersections = {}; - /** - * Detach a edge from the node - * @param {Edge} edge - */ - value: function detachEdge(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); + Line._getDataIntersections(intersections, combinedData); + groupRanges[groupLabel] = Line._getStackedYRange(intersections, combinedData); + groupRanges[groupLabel].yAxisOrientation = orientation; + groupIds.push(groupLabel); + } + }; + + Line._getStackedYRange = 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 { + if (combinedData[i].y < 0) { + intersections[key].accumulatedNegative += combinedData[i].y; + } else { + intersections[key].accumulatedPositive += combinedData[i].y; } } - }, { - key: 'togglePhysics', - - /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - this.options.physics = status; + } + for (var xpos in intersections) { + if (intersections.hasOwnProperty(xpos)) { + yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; + yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; + yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; + yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; } - }, { - key: 'setOptions', + } - /** - * Set or overwrite options for the node - * @param {Object} options an object with options - * @param {Object} constants and object with default, global options - */ - value: function setOptions(options) { - if (!options) { - return; - } - // basic options - if (options.id !== undefined) { - this.id = options.id; - } + return { min: yMin, max: yMax }; + }; - if (this.id === undefined) { - throw 'Node must have an id'; + /** + * Fill the intersections object with counters of how many datapoints share the same x coordinates + * @param intersections + * @param combinedData + * @private + */ + Line._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, accumulatedPositive: 0, accumulatedNegative: 0 }; } + intersections[combinedData[i].x].amount += 1; + } + } + }; - // set these options locally - if (options.x !== undefined) { - this.x = parseInt(options.x);this.predefinedPosition = true; - } - if (options.y !== undefined) { - this.y = parseInt(options.y);this.predefinedPosition = true; - } - if (options.size !== undefined) { - this.baseSize = options.size; - } - if (options.value !== undefined) { - options.value = parseFloat(options.value); + /** + * draw a line graph + * + * @param dataset + * @param group + */ + Line.prototype.draw = function (dataset, group, framework) { + if (dataset != null) { + if (dataset.length > 0) { + var path, d; + var svgHeight = Number(framework.svg.style.height.replace('px', '')); + path = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); + path.setAttributeNS(null, 'class', group.className); + if (group.style !== undefined) { + path.setAttributeNS(null, 'style', group.style); } - // copy group options - if (typeof options.group === 'number' || typeof options.group === 'string' && options.group != '') { - var groupObj = this.grouplist.get(options.group); - util.deepExtend(this.options, groupObj); - // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. - this.options.color = util.parseColor(this.options.color); + // construct path from dataset + if (group.options.interpolation.enabled == true) { + d = Line._catmullRom(dataset, group); + } else { + d = Line._linear(dataset); } - // this transforms all shorthands into fully defined options - Node.parseOptions(this.options, options, true); - - // load the images - if (this.options.image !== undefined) { - if (this.imagelist) { - this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id); + // append with points for fill and finalize the path + if (group.options.shaded.enabled == true) { + var fillPath = DOMutil.getSVGElement('path', framework.svgElements, framework.svg); + var dFill; + if (group.options.shaded.orientation == 'top') { + dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0; } else { - throw 'No imagelist provided'; + dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight; + } + fillPath.setAttributeNS(null, 'class', group.className + ' vis-fill'); + if (group.options.shaded.style !== undefined) { + fillPath.setAttributeNS(null, 'style', group.options.shaded.style); } + fillPath.setAttributeNS(null, 'd', dFill); } + // copy properties to path for drawing. + path.setAttributeNS(null, 'd', 'M' + d); - this.updateShape(); - this.updateLabelModule(); - - // reset the size of the node, this can be changed - this._reset(); - - if (options.hidden !== undefined || options.physics !== undefined) { - return true; - } - return false; - } - }, { - key: 'updateLabelModule', - value: function updateLabelModule() { - if (this.options.label === undefined || this.options.label === null) { - this.options.label = ''; - } - this.labelModule.setOptions(this.options, true); - if (this.labelModule.baseSize !== undefined) { - this.baseFontSize = this.labelModule.baseSize; - } - } - }, { - key: 'updateShape', - value: function updateShape() { - // choose draw method depending on the shape - switch (this.options.shape) { - case 'box': - this.shape = new _nodesShapesBox2['default'](this.options, this.body, this.labelModule); - break; - case 'circle': - this.shape = new _nodesShapesCircle2['default'](this.options, this.body, this.labelModule); - break; - case 'circularImage': - this.shape = new _nodesShapesCircularImage2['default'](this.options, this.body, this.labelModule, this.imageObj); - break; - case 'database': - this.shape = new _nodesShapesDatabase2['default'](this.options, this.body, this.labelModule); - break; - case 'diamond': - this.shape = new _nodesShapesDiamond2['default'](this.options, this.body, this.labelModule); - break; - case 'dot': - this.shape = new _nodesShapesDot2['default'](this.options, this.body, this.labelModule); - break; - case 'ellipse': - this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); - break; - case 'icon': - this.shape = new _nodesShapesIcon2['default'](this.options, this.body, this.labelModule); - break; - case 'image': - this.shape = new _nodesShapesImage2['default'](this.options, this.body, this.labelModule, this.imageObj); - break; - case 'square': - this.shape = new _nodesShapesSquare2['default'](this.options, this.body, this.labelModule); - break; - case 'star': - this.shape = new _nodesShapesStar2['default'](this.options, this.body, this.labelModule); - break; - case 'text': - this.shape = new _nodesShapesText2['default'](this.options, this.body, this.labelModule); - break; - case 'triangle': - this.shape = new _nodesShapesTriangle2['default'](this.options, this.body, this.labelModule); - break; - case 'triangleDown': - this.shape = new _nodesShapesTriangleDown2['default'](this.options, this.body, this.labelModule); - break; - default: - this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); - break; + // draw points + if (group.options.drawPoints.enabled == true) { + Points.draw(dataset, group, framework); } - this._reset(); } - }, { - key: 'select', + } + }; - /** - * select this node - */ - value: function select() { - this.selected = true; - this._reset(); - } - }, { - key: 'unselect', + /** + * This uses an uniform parametrization of the interpolation algorithm: + * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al. + * @param data + * @returns {string} + * @private + */ + Line._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++) { - /** - * unselect this node - */ - value: function unselect() { - this.selected = false; - this._reset(); - } - }, { - key: '_reset', + p0 = i == 0 ? data[0] : data[i - 1]; + p1 = data[i]; + p2 = data[i + 1]; + p3 = i + 2 < length ? data[i + 2] : p2; - /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private - */ - value: function _reset() { - this.shape.width = undefined; - this.shape.height = undefined; - } - }, { - key: 'getTitle', + // 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 - /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. - */ - value: function getTitle() { - return this.options.title; - } - }, { - key: 'distanceToBorder', + // 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 }; - /** - * Calculate the distance to the border of the Node - * @param {CanvasRenderingContext2D} ctx - * @param {Number} angle Angle in radians - * @returns {number} distance Distance to the border in pixels - */ - value: function distanceToBorder(ctx, angle) { - return this.shape.distanceToBorder(ctx, angle); - } - }, { - key: 'isFixed', + d += 'C' + bp1.x + ',' + bp1.y + ' ' + bp2.x + ',' + bp2.y + ' ' + p2.x + ',' + p2.y + ' '; + } - /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not - */ - value: function isFixed() { - return this.options.fixed.x && this.options.fixed.y; - } - }, { - key: 'isSelected', + return d; + }; - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - } - }, { - key: 'getValue', + /** + * 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 + * @param group + * @returns {string} + * @private + */ + Line._catmullRom = function (data, group) { + var alpha = group.options.interpolation.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++) { - /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.options.value; - } - }, { - key: 'setValueRange', + p0 = i == 0 ? data[0] : data[i - 1]; + p1 = data[i]; + p2 = data[i + 1]; + p3 = i + 2 < length ? data[i + 2] : p2; - /** - * Adjust the value range of the node. The node will adjust it's size - * based on its value. - * @param {Number} min - * @param {Number} max - */ - value: function setValueRange(min, max, total) { - if (this.options.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); - var sizeDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled === true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.size = this.options.scaling.min + scale * sizeDiff; - } else { - this.options.size = this.baseSize; - this.options.font.size = this.baseFontSize; + 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 ] + + 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 + ' '; } - }, { - key: 'draw', - /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); + return d; + } + }; + + /** + * this generates the SVG path for a linear drawing between datapoints. + * @param data + * @returns {string} + * @private + */ + Line._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; } - }, { - key: 'updateBoundingBox', + } + return d; + }; - /** - * Update the bounding box of the shape - */ - value: function updateBoundingBox(ctx) { - this.shape.updateBoundingBox(this.x, this.y, ctx); - } - }, { - key: 'resize', + module.exports = Line; - /** - * Recalculate the size of this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function resize(ctx) { - this.shape.resize(ctx); - } - }, { - key: 'isOverlappingWith', +/***/ }, +/* 61 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isOverlappingWith(obj) { - return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; - } - }, { - key: 'isBoundingBoxOverlappingWith', + 'use strict'; - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isBoundingBoxOverlappingWith(obj) { - return this.shape.boundingBox.left < obj.right && this.shape.boundingBox.right > obj.left && this.shape.boundingBox.top < obj.bottom && this.shape.boundingBox.bottom > obj.top; - } - }], [{ - key: 'parseOptions', + var DOMutil = __webpack_require__(19); - /** - * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. - * Static so it can also be used by the handler. - * @param parentOptions - * @param newOptions - */ - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + function Points(groupId, options) { + this.groupId = groupId; + this.options = options; + } - var fields = ['color', 'font', 'fixed', 'shadow']; - util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion); + Points.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + }; - // merge the shadow options into the parent. - util.mergeOptions(parentOptions, newOptions, 'shadow'); + Points.prototype.draw = function (dataset, group, framework, offset) { + Points.draw(dataset, group, framework, offset); + }; - // individual shape newOptions - if (newOptions.color !== undefined && newOptions.color !== null) { - var parsedColor = util.parseColor(newOptions.color); - util.fillIfDefined(parentOptions.color, parsedColor); - } else if (allowDeletion === true && newOptions.color === null) { - parentOptions.color = undefined; - delete parentOptions.color; - } + /** + * draw the data points + * + * @param {Array} dataset + * @param {Object} JSONcontainer + * @param {Object} svg | SVG DOM element + * @param {GraphGroup} group + * @param {Number} [offset] + */ + Points.draw = function (dataset, group, framework, offset) { + offset = offset || 0; + var callback = getCallback(); - // handle the fixed options - if (newOptions.fixed !== undefined && newOptions.fixed !== null) { - if (typeof newOptions.fixed === 'boolean') { - parentOptions.fixed.x = newOptions.fixed; - parentOptions.fixed.y = newOptions.fixed; - } else { - if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { - parentOptions.fixed.x = newOptions.fixed.x; - } - if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { - parentOptions.fixed.y = newOptions.fixed.y; - } - } + for (var i = 0; i < dataset.length; i++) { + if (!callback) { + // draw the point the simple way. + DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(), framework.svgElements, framework.svg, dataset[i].label); + } else { + var callbackResult = callback(dataset[i], group, framework); // result might be true, false or an object + if (callbackResult === true || typeof callbackResult === 'object') { + DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, getGroupTemplate(callbackResult), framework.svgElements, framework.svg, dataset[i].label); } + } + } - // handle the font options - if (newOptions.font !== undefined) { - _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); - } + function getGroupTemplate(callbackResult) { + callbackResult = typeof callbackResult === 'undefined' ? {} : callbackResult; + return { + style: callbackResult.style || group.options.drawPoints.style, + size: callbackResult.size || group.options.drawPoints.size, + className: callbackResult.className || group.className + }; + } - // handle the scaling options, specifically the label part - if (newOptions.scaling !== undefined) { - util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); - } + function getCallback() { + var callback = undefined; + // check for the graph2d onRender + if (framework.options.drawPoints.onRender && typeof framework.options.drawPoints.onRender == 'function') { + callback = framework.options.drawPoints.onRender; } - }]); - return Node; - })(); + // override it with the group onRender if defined + if (group.group.options && group.group.options.drawPoints && group.group.options.drawPoints.onRender && typeof group.group.options.drawPoints.onRender == 'function') { + callback = group.group.options.drawPoints.onRender; + } - exports['default'] = Node; - module.exports = exports['default']; + return callback; + } + }; + + module.exports = Points; /***/ }, -/* 64 */ +/* 62 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var DOMutil = __webpack_require__(19); + var Points = __webpack_require__(61); - function _slicedToArray(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } } + function Bargraph(groupId, options) { + this.groupId = groupId; + this.options = options; + } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + Bargraph.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + }; - var util = __webpack_require__(9); + Bargraph.prototype.getData = function (groupData) { + var combinedData = []; + for (var j = 0; j < groupData.length; j++) { + combinedData.push({ + x: groupData[j].x, + y: groupData[j].y, + groupId: this.groupId + }); + } + return combinedData; + }; - var Label = (function () { - function Label(body, options) { - _classCallCheck(this, Label); + /** + * draw a bar graph + * + * @param groupIds + * @param processedGroupData + */ + Bargraph.draw = function (groupIds, processedGroupData, framework) { + var combinedData = []; + var intersections = {}; + var coreDistance; + var key, drawData; + var group; + var i, j; + var barPoints = 0; - this.body = body; + // combine all barchart data + for (i = 0; i < groupIds.length; i++) { + group = framework.groups[groupIds[i]]; + if (group.options.style === 'bar') { + if (group.visible === true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.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], + label: processedGroupData[groupIds[i]][j].label + }); + barPoints += 1; + } + } + } + } - this.pointToSelf = false; - this.baseSize = undefined; - this.setOptions(options); - this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + if (barPoints === 0) { + return; } - _createClass(Label, [{ - key: 'setOptions', - value: function setOptions(options) { - var allowDeletion = arguments[1] === undefined ? false : arguments[1]; + // sort by time and by group + combinedData.sort(function (a, b) { + if (a.x === b.x) { + return a.groupId < b.groupId ? -1 : 1; + } else { + return a.x - b.x; + } + }); - this.options = options; + // get intersections + Bargraph._getDataIntersections(intersections, combinedData); - if (options.label !== undefined) { - this.labelDirty = true; + // plot barchart + for (i = 0; i < combinedData.length; i++) { + group = framework.groups[combinedData[i].groupId]; + var minWidth = 0.1 * group.options.barChart.width; + + 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 = Bargraph._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 = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + intersections[key].resolved += 1; - if (options.font !== undefined) { - Label.parseOptions(this.options.font, options, allowDeletion); - if (typeof options.font === 'string') { - this.baseSize = this.options.font.size; - } else if (typeof options.font === 'object') { - if (options.font.size !== undefined) { - this.baseSize = options.font.size; - } + if (group.options.stack === true) { + if (combinedData[i].y < group.zeroPosition) { + heightOffset = intersections[key].accumulatedNegative; + intersections[key].accumulatedNegative += group.zeroPosition - combinedData[i].y; + } else { + heightOffset = intersections[key].accumulatedPositive; + intersections[key].accumulatedPositive += group.zeroPosition - combinedData[i].y; + } + } else if (group.options.barChart.sideBySide === true) { + 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; } } } - }, { - key: 'draw', + DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' vis-bar', framework.svgElements, framework.svg, group.style); + // draw points + if (group.options.drawPoints.enabled === true) { + Points.draw([combinedData[i]], group, framework, drawData.offset); + //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); + } + } + }; - /** - * Main function. This is called from anything that wants to draw a label. - * @param ctx - * @param x - * @param y - * @param selected - * @param baseline - */ - value: function draw(ctx, x, y, selected) { - var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; + /** + * Fill the intersections object with counters of how many datapoints share the same x coordinates + * @param intersections + * @param combinedData + * @private + */ + Bargraph._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, accumulatedPositive: 0, accumulatedNegative: 0 }; + } + intersections[combinedData[i].x].amount += 1; + } + } + }; - // if no label, return - if (this.options.label === undefined) return; + /** + * 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 + */ + Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { + var width, offset; + if (coreDistance < group.options.barChart.width && coreDistance > 0) { + width = coreDistance < minWidth ? minWidth : coreDistance; - // check if we have to render the label - var viewFontSize = this.options.font.size * this.body.view.scale; - if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) return; + 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; + } + } - // update the size cache if required - this.calculateLabelSize(ctx, selected, x, y, baseline); + return { width: width, offset: offset }; + }; - // create the fontfill background - this._drawBackground(ctx); - // draw text - this._drawText(ctx, selected, x, y, baseline); + Bargraph.getStackedYRange = function (combinedData, groupRanges, groupIds, groupLabel, orientation) { + if (combinedData.length > 0) { + // sort by time and by group + combinedData.sort(function (a, b) { + if (a.x === b.x) { + return a.groupId < b.groupId ? -1 : 1; + } else { + return a.x - b.x; + } + }); + var intersections = {}; + + Bargraph._getDataIntersections(intersections, combinedData); + groupRanges[groupLabel] = Bargraph._getStackedYRange(intersections, combinedData); + groupRanges[groupLabel].yAxisOrientation = orientation; + groupIds.push(groupLabel); + } + }; + + Bargraph._getStackedYRange = 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 { + if (combinedData[i].y < 0) { + intersections[key].accumulatedNegative += combinedData[i].y; + } else { + intersections[key].accumulatedPositive += combinedData[i].y; + } } - }, { - key: '_drawBackground', + } + for (var xpos in intersections) { + if (intersections.hasOwnProperty(xpos)) { + yMin = yMin > intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMin; + yMin = yMin > intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMin; + yMax = yMax < intersections[xpos].accumulatedNegative ? intersections[xpos].accumulatedNegative : yMax; + yMax = yMax < intersections[xpos].accumulatedPositive ? intersections[xpos].accumulatedPositive : yMax; + } + } - /** - * Draws the label background - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _drawBackground(ctx) { - if (this.options.font.background !== undefined && this.options.font.background !== 'none') { - ctx.fillStyle = this.options.font.background; + return { min: yMin, max: yMax }; + }; - var lineMargin = 2; + module.exports = Bargraph; - switch (this.options.font.align) { - case 'middle': - ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); - break; - case 'top': - ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); - break; - case 'bottom': - ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); - break; - default: - ctx.fillRect(this.size.left, this.size.top - 0.5 * lineMargin, this.size.width, this.size.height); - break; - } - } +/***/ }, +/* 63 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var util = __webpack_require__(14); + var DOMutil = __webpack_require__(19); + var Component = __webpack_require__(33); + + /** + * 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 } - }, { - key: '_drawText', + }; + this.side = side; + this.options = util.extend({}, this.defaultOptions); + this.linegraphOptions = linegraphOptions; - /** - * - * @param ctx - * @param x - * @param baseline - * @private - */ - value: function _drawText(ctx, selected, x, y) { - var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; + this.svgElements = {}; + this.dom = {}; + this.groups = {}; + this.amountOfGroups = 0; + this._create(); + + this.setOptions(options); + } + + Legend.prototype = new Component(); + + Legend.prototype.clear = function () { + this.groups = {}; + this.amountOfGroups = 0; + }; + + Legend.prototype.addGroup = function (label, graphOptions) { - var fontSize = this.options.font.size; - var viewFontSize = fontSize * this.body.view.scale; - // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) - if (viewFontSize >= this.options.scaling.label.maxVisible) { - fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; - } + if (!this.groups.hasOwnProperty(label)) { + this.groups[label] = graphOptions; + } + this.amountOfGroups += 1; + }; - var yLine = this.size.yLine; + Legend.prototype.updateGroup = function (label, graphOptions) { + this.groups[label] = graphOptions; + }; - var _getColor2 = this._getColor(viewFontSize); + Legend.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; + } + }; - var _getColor22 = _slicedToArray(_getColor2, 2); + Legend.prototype._create = function () { + this.dom.frame = document.createElement('div'); + this.dom.frame.className = 'vis-legend'; + this.dom.frame.style.position = 'absolute'; + this.dom.frame.style.top = '10px'; + this.dom.frame.style.display = 'block'; - var fontColor = _getColor22[0]; - var strokeColor = _getColor22[1]; + this.dom.textArea = document.createElement('div'); + this.dom.textArea.className = 'vis-legend-text'; + this.dom.textArea.style.position = 'relative'; + this.dom.textArea.style.top = '0px'; - var _setAlignment2 = this._setAlignment(ctx, x, yLine, baseline); + 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.svg.style.height = '100%'; - var _setAlignment22 = _slicedToArray(_setAlignment2, 2); + this.dom.frame.appendChild(this.svg); + this.dom.frame.appendChild(this.dom.textArea); + }; - x = _setAlignment22[0]; - yLine = _setAlignment22[1]; + /** + * Hide the component from the DOM + */ + Legend.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } + }; - // configure context for drawing the text - ctx.font = (selected ? 'bold ' : '') + fontSize + 'px ' + this.options.font.face; - ctx.fillStyle = fontColor; - ctx.textAlign = 'center'; + /** + * 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); + } + }; - // set the strokeWidth - if (this.options.font.strokeWidth > 0) { - ctx.lineWidth = this.options.font.strokeWidth; - ctx.strokeStyle = strokeColor; - ctx.lineJoin = 'round'; - } + Legend.prototype.setOptions = function (options) { + var fields = ['enabled', 'orientation', 'icons', 'left', 'right']; + util.selectiveDeepExtend(fields, this.options, options); + }; - // draw the text - for (var i = 0; i < this.lineCount; i++) { - if (this.options.font.strokeWidth > 0) { - ctx.strokeText(this.lines[i], x, yLine); - } - ctx.fillText(this.lines[i], x, yLine); - yLine += fontSize; - } + Legend.prototype.redraw = function () { + var activeGroups = 0; + var groupArray = Object.keys(this.groups); + groupArray.sort(function (a, b) { + return a < b ? -1 : 1; + }); + + for (var i = 0; i < groupArray.length; i++) { + var groupId = groupArray[i]; + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + activeGroups++; } - }, { - key: '_setAlignment', - value: function _setAlignment(ctx, x, yLine, baseline) { - // check for label alignment (for edges) - // TODO: make alignment for nodes - if (this.options.font.align !== 'horizontal' && this.pointToSelf === false) { - x = 0; - yLine = 0; + } - var lineMargin = 2; - if (this.options.font.align === 'top') { - ctx.textBaseline = 'alphabetic'; - yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } else if (this.options.font.align === 'bottom') { - ctx.textBaseline = 'hanging'; - yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers - } else { - ctx.textBaseline = 'middle'; - } - } else { - ctx.textBaseline = baseline; - } + if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) { + this.hide(); + } else { + this.show(); + if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') { + this.dom.frame.style.left = '4px'; + this.dom.frame.style.textAlign = 'left'; + this.dom.textArea.style.textAlign = 'left'; + this.dom.textArea.style.left = this.options.iconSize + 15 + 'px'; + this.dom.textArea.style.right = ''; + this.svg.style.left = 0 + 'px'; + this.svg.style.right = ''; + } else { + this.dom.frame.style.right = '4px'; + this.dom.frame.style.textAlign = 'right'; + this.dom.textArea.style.textAlign = 'right'; + this.dom.textArea.style.right = this.options.iconSize + 15 + 'px'; + this.dom.textArea.style.left = ''; + this.svg.style.right = 0 + 'px'; + this.svg.style.left = ''; + } - return [x, yLine]; + 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 { + var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height; + this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace('px', '')) + 'px'; + this.dom.frame.style.top = ''; } - }, { - key: '_getColor', - /** - * fade in when relative scale is between threshold and threshold - 1. - * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. - * - * @param viewFontSize - * @returns {*[]} - * @private - */ - value: function _getColor(viewFontSize) { - var fontColor = this.options.font.color || '#000000'; - var strokeColor = this.options.font.strokeColor || '#ffffff'; - if (viewFontSize <= this.options.scaling.label.drawThreshold) { - var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); - fontColor = util.overrideOpacity(fontColor, opacity); - strokeColor = util.overrideOpacity(strokeColor, opacity); + 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 i = 0; i < groupArray.length; i++) { + var groupId = groupArray[i]; + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + content += this.groups[groupId].content + '
'; } - return [fontColor, strokeColor]; } - }, { - key: 'getTextSize', + this.dom.textArea.innerHTML = content; + this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + 'px'; + } + }; - /** - * - * @param ctx - * @param selected - * @returns {{width: number, height: number}} - */ - value: function getTextSize(ctx) { - var selected = arguments[1] === undefined ? false : arguments[1]; + Legend.prototype.drawLegendIcons = function () { + if (this.dom.frame.parentNode) { + var groupArray = Object.keys(this.groups); + groupArray.sort(function (a, b) { + return a < b ? -1 : 1; + }); - var size = { - width: this._processLabel(ctx, selected), - height: this.options.font.size * this.lineCount, - lineCount: this.lineCount - }; - return size; - } - }, { - key: 'calculateLabelSize', + 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; - /** - * - * @param ctx - * @param selected - * @param x - * @param y - * @param baseline - */ - value: function calculateLabelSize(ctx, selected) { - var x = arguments[2] === undefined ? 0 : arguments[2]; - var y = arguments[3] === undefined ? 0 : arguments[3]; - var baseline = arguments[4] === undefined ? 'middle' : arguments[4]; + this.svg.style.width = iconWidth + 5 + iconOffset + 'px'; - if (this.labelDirty === true) { - this.size.width = this._processLabel(ctx, selected); - } - this.size.height = this.options.font.size * this.lineCount; - this.size.left = x - this.size.width * 0.5; - this.size.top = y - this.size.height * 0.5; - this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.options.font.size; - if (baseline === 'hanging') { - this.size.top += 0.5 * this.options.font.size; - this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers - this.size.yLine += 4; // distance from node + for (var i = 0; i < groupArray.length; i++) { + var groupId = groupArray[i]; + 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; } - - this.labelDirty = false; } - }, { - key: '_processLabel', - /** - * This calculates the width as well as explodes the label string and calculates the amount of lines. - * @param ctx - * @param selected - * @returns {number} - * @private - */ - value: function _processLabel(ctx, selected) { - var width = 0; - var lines = ['']; - var lineCount = 0; - if (this.options.label !== undefined) { - lines = String(this.options.label).split('\n'); - lineCount = lines.length; - ctx.font = (selected ? 'bold ' : '') + this.options.font.size + 'px ' + this.options.font.face; - width = ctx.measureText(lines[0]).width; - for (var i = 1; i < lineCount; i++) { - var lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; - } - } - this.lines = lines; - this.lineCount = lineCount; + DOMutil.cleanupElements(this.svgElements); + } + }; - return width; - } - }], [{ - key: 'parseOptions', - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + module.exports = Legend; - if (typeof newOptions.font === 'string') { - var newOptionsArray = newOptions.font.split(' '); - parentOptions.size = newOptionsArray[0].replace('px', ''); - parentOptions.face = newOptionsArray[1]; - parentOptions.color = newOptionsArray[2]; - } else if (typeof newOptions.font === 'object') { - util.fillIfDefined(parentOptions, newOptions.font, allowDeletion); +/***/ }, +/* 64 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * This object contains all possible options. It will check if the types are correct, if required if the option is one + * of the allowed values. + * + * __any__ means that the name of the property does not matter. + * __type__ is a required field for all objects and contains the allowed types of all objects + */ + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + var string = 'string'; + var boolean = 'boolean'; + var number = 'number'; + var array = 'array'; + var date = 'date'; + var object = 'object'; // should only be in a __type__ property + var dom = 'dom'; + var moment = 'moment'; + var any = 'any'; + + var allOptions = { + configure: { + enabled: { boolean: boolean }, + filter: { boolean: boolean, 'function': 'function' }, + container: { dom: dom }, + __type__: { object: object, boolean: boolean, 'function': 'function' } + }, + + //globals : + yAxisOrientation: { string: ['left', 'right'] }, + defaultGroup: { string: string }, + sort: { boolean: boolean }, + sampling: { boolean: boolean }, + stack: { boolean: boolean }, + graphHeight: { string: string, number: number }, + shaded: { + enabled: { boolean: boolean }, + orientation: { string: ['bottom', 'top'] }, // top, bottom + __type__: { boolean: boolean, object: object } + }, + style: { string: ['line', 'bar', 'points'] }, // line, bar + barChart: { + width: { number: number }, + sideBySide: { boolean: boolean }, + align: { string: ['left', 'center', 'right'] }, + __type__: { object: object } + }, + interpolation: { + enabled: { boolean: boolean }, + parametrization: { string: ['centripetal', 'chordal', 'uniform'] }, // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) + alpha: { number: number }, + __type__: { object: object, boolean: boolean } + }, + drawPoints: { + enabled: { boolean: boolean }, + onRender: { 'function': 'function' }, + size: { number: number }, + style: { string: ['square', 'circle'] }, // square, circle + __type__: { object: object, boolean: boolean, 'function': 'function' } + }, + dataAxis: { + showMinorLabels: { boolean: boolean }, + showMajorLabels: { boolean: boolean }, + icons: { boolean: boolean }, + width: { string: string, number: number }, + visible: { boolean: boolean }, + alignZeros: { boolean: boolean }, + left: { + range: { min: { number: number }, max: { number: number }, __type__: { object: object } }, + format: { 'function': 'function' }, + title: { text: { string: string, number: number }, style: { string: string }, __type__: { object: object } }, + __type__: { object: object } + }, + right: { + range: { min: { number: number }, max: { number: number }, __type__: { object: object } }, + format: { 'function': 'function' }, + title: { text: { string: string, number: number }, style: { string: string }, __type__: { object: object } }, + __type__: { object: object } + }, + __type__: { object: object } + }, + legend: { + enabled: { boolean: boolean }, + icons: { boolean: boolean }, + left: { + visible: { boolean: boolean }, + position: { string: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] }, + __type__: { object: object } + }, + right: { + visible: { boolean: boolean }, + position: { string: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] }, + __type__: { object: object } + }, + __type__: { object: object, boolean: boolean } + }, + groups: { + visibility: { any: any }, + __type__: { object: object } + }, + + autoResize: { boolean: boolean }, + clickToUse: { boolean: boolean }, + end: { number: number, date: date, string: string, moment: moment }, + format: { + minorLabels: { + millisecond: { string: string, 'undefined': 'undefined' }, + second: { string: string, 'undefined': 'undefined' }, + minute: { string: string, 'undefined': 'undefined' }, + hour: { string: string, 'undefined': 'undefined' }, + weekday: { string: string, 'undefined': 'undefined' }, + day: { string: string, 'undefined': 'undefined' }, + month: { string: string, 'undefined': 'undefined' }, + year: { string: string, 'undefined': 'undefined' }, + __type__: { object: object } + }, + majorLabels: { + millisecond: { string: string, 'undefined': 'undefined' }, + second: { string: string, 'undefined': 'undefined' }, + minute: { string: string, 'undefined': 'undefined' }, + hour: { string: string, 'undefined': 'undefined' }, + weekday: { string: string, 'undefined': 'undefined' }, + day: { string: string, 'undefined': 'undefined' }, + month: { string: string, 'undefined': 'undefined' }, + year: { string: string, 'undefined': 'undefined' }, + __type__: { object: object } + }, + __type__: { object: object } + }, + height: { string: string, number: number }, + hiddenDates: { object: object, array: array }, + locale: { string: string }, + locales: { + __any__: { any: any }, + __type__: { object: object } + }, + max: { date: date, number: number, string: string, moment: moment }, + maxHeight: { number: number, string: string }, + min: { date: date, number: number, string: string, moment: moment }, + minHeight: { number: number, string: string }, + moveable: { boolean: boolean }, + multiselect: { boolean: boolean }, + orientation: { string: string }, + showCurrentTime: { boolean: boolean }, + showMajorLabels: { boolean: boolean }, + showMinorLabels: { boolean: boolean }, + start: { date: date, number: number, string: string, moment: moment }, + timeAxis: { + scale: { string: string, 'undefined': 'undefined' }, + step: { number: number, 'undefined': 'undefined' }, + __type__: { object: object } + }, + width: { string: string, number: number }, + zoomable: { boolean: boolean }, + zoomMax: { number: number }, + zoomMin: { number: number }, + __type__: { object: object } + }; + + var configureOptions = { + global: { + //yAxisOrientation: ['left','right'], // TDOO: enable as soon as Grahp2d doesn't crash when changing this on the fly + sort: true, + sampling: true, + stack: false, + shaded: { + enabled: false, + orientation: ['top', 'bottom'] // top, bottom + }, + style: ['line', 'bar', 'points'], // line, bar + barChart: { + width: [50, 5, 100, 5], + sideBySide: false, + align: ['left', 'center', 'right'] // left, center, right + }, + interpolation: { + enabled: true, + parametrization: ['centripetal', 'chordal', 'uniform'] // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) + }, + drawPoints: { + enabled: true, + size: [6, 2, 30, 1], + style: ['square', 'circle'] // square, circle + }, + dataAxis: { + showMinorLabels: true, + showMajorLabels: true, + icons: false, + width: [40, 0, 200, 1], + visible: true, + alignZeros: true, + left: { + //range: {min:'undefined': 'undefined'ined,max:'undefined': 'undefined'ined}, + //format: function (value) {return value;}, + title: { text: '', style: '' } + }, + right: { + //range: {min:'undefined': 'undefined'ined,max:'undefined': 'undefined'ined}, + //format: function (value) {return value;}, + title: { text: '', style: '' } + } + }, + legend: { + enabled: false, + icons: true, + left: { + visible: true, + position: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] // top/bottom - left,right + }, + right: { + visible: true, + position: ['top-right', 'bottom-right', 'top-left', 'bottom-left'] // top/bottom - left,right + } + }, + + autoResize: true, + clickToUse: false, + end: '', + format: { + minorLabels: { + millisecond: 'SSS', + second: 's', + minute: 'HH:mm', + hour: 'HH:mm', + weekday: 'ddd D', + day: 'D', + month: 'MMM', + year: 'YYYY' + }, + majorLabels: { + millisecond: 'HH:mm:ss', + second: 'D MMMM HH:mm', + minute: 'ddd D MMMM', + hour: 'ddd D MMMM', + weekday: 'MMMM YYYY', + day: 'MMMM YYYY', + month: 'YYYY', + year: '' } - parentOptions.size = Number(parentOptions.size); - } - }]); + }, - return Label; - })(); + height: '', + locale: '', + max: '', + maxHeight: '', + min: '', + minHeight: '', + moveable: true, + orientation: ['both', 'bottom', 'top'], + showCurrentTime: false, + showMajorLabels: true, + showMinorLabels: true, + start: '', + width: '100%', + zoomable: true, + zoomMax: [315360000000000, 10, 315360000000000, 1], + zoomMin: [10, 10, 315360000000000, 1] + } + }; - exports['default'] = Label; - module.exports = exports['default']; + exports.allOptions = allOptions; + exports.configureOptions = configureOptions; /***/ }, /* 65 */ /***/ function(module, exports, __webpack_require__) { + // Load custom shapes into CanvasRenderingContext2D 'use strict'; - Object.defineProperty(exports, '__esModule', { - value: true - }); + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var _modulesGroups = __webpack_require__(66); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + var _modulesGroups2 = _interopRequireDefault(_modulesGroups); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + var _modulesNodesHandler = __webpack_require__(2); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + var _modulesNodesHandler2 = _interopRequireDefault(_modulesNodesHandler); - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + var _modulesEdgesHandler = __webpack_require__(9); - var _utilNodeBase = __webpack_require__(66); + var _modulesEdgesHandler2 = _interopRequireDefault(_modulesEdgesHandler); - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + var _modulesPhysicsEngine = __webpack_require__(90); - var Box = (function (_NodeBase) { - function Box(options, body, labelModule) { - _classCallCheck(this, Box); + var _modulesPhysicsEngine2 = _interopRequireDefault(_modulesPhysicsEngine); - _get(Object.getPrototypeOf(Box.prototype), 'constructor', this).call(this, options, body, labelModule); - } + var _modulesClustering = __webpack_require__(99); - _inherits(Box, _NodeBase); + var _modulesClustering2 = _interopRequireDefault(_modulesClustering); - _createClass(Box, [{ - key: 'resize', - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - this.radius = 0.5 * this.width; - } - } - }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + var _modulesCanvasRenderer = __webpack_require__(101); - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + var _modulesCanvasRenderer2 = _interopRequireDefault(_modulesCanvasRenderer); - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + var _modulesCanvas = __webpack_require__(102); - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + var _modulesCanvas2 = _interopRequireDefault(_modulesCanvas); - var borderRadius = 6; - ctx.roundRect(this.left, this.top, this.width, this.height, borderRadius); + var _modulesView = __webpack_require__(103); - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); + var _modulesView2 = _interopRequireDefault(_modulesView); - // disable shadows for other elements. - this.disableShadow(ctx); + var _modulesInteractionHandler = __webpack_require__(104); - ctx.stroke(); + var _modulesInteractionHandler2 = _interopRequireDefault(_modulesInteractionHandler); - this.updateBoundingBox(x, y); - this.labelModule.draw(ctx, x, y, selected); - } - }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; + var _modulesSelectionHandler = __webpack_require__(5); - this.boundingBox.left = this.left; - this.boundingBox.top = this.top; - this.boundingBox.bottom = this.top + this.height; - this.boundingBox.right = this.left + this.width; - } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); - } - }]); + var _modulesSelectionHandler2 = _interopRequireDefault(_modulesSelectionHandler); - return Box; - })(_utilNodeBase2['default']); + var _modulesLayoutEngine = __webpack_require__(107); - exports['default'] = Box; - module.exports = exports['default']; + var _modulesLayoutEngine2 = _interopRequireDefault(_modulesLayoutEngine); -/***/ }, -/* 66 */ -/***/ function(module, exports, __webpack_require__) { + var _modulesManipulationSystem = __webpack_require__(108); - 'use strict'; + var _modulesManipulationSystem2 = _interopRequireDefault(_modulesManipulationSystem); - Object.defineProperty(exports, '__esModule', { - value: true - }); + var _sharedConfigurator = __webpack_require__(51); - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var _sharedConfigurator2 = _interopRequireDefault(_sharedConfigurator); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + var _sharedValidator = __webpack_require__(53); - var NodeBase = (function () { - function NodeBase(options, body, labelModule) { - _classCallCheck(this, NodeBase); + var _sharedValidator2 = _interopRequireDefault(_sharedValidator); - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.top = undefined; - this.left = undefined; - this.height = undefined; - this.width = undefined; - this.radius = undefined; - this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; + var _optionsJs = __webpack_require__(7); + + __webpack_require__(109); + + var Emitter = __webpack_require__(25); + var Hammer = __webpack_require__(10); + var util = __webpack_require__(14); + var DataSet = __webpack_require__(20); + var DataView = __webpack_require__(22); + var dotparser = __webpack_require__(110); + var gephiParser = __webpack_require__(111); + var Images = __webpack_require__(112); + var Activator = __webpack_require__(48); + var locales = __webpack_require__(113); + + /** + * @constructor Network + * Create a network visualization, displaying nodes and edges. + * + * @param {Element} container The DOM element in which the Network will + * be created. Normally a div element. + * @param {Object} data An object containing parameters + * {Array} nodes + * {Array} edges + * @param {Object} options Options + */ + function Network(container, data, options) { + var _this = this; + + if (!(this instanceof Network)) { + throw new SyntaxError('Constructor must be called with the new operator'); } - _createClass(NodeBase, [{ - key: 'setOptions', - value: function setOptions(options) { - this.options = options; - } - }, { - key: '_distanceToBorder', - value: function _distanceToBorder(angle) { - var borderWidth = 1; - return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - } - }, { - key: 'enableShadow', - value: function enableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0.5)'; - ctx.shadowBlur = this.options.shadow.size; - ctx.shadowOffsetX = this.options.shadow.x; - ctx.shadowOffsetY = this.options.shadow.y; - } - } - }, { - key: 'disableShadow', - value: function disableShadow(ctx) { - if (this.options.shadow.enabled === true) { - ctx.shadowColor = 'rgba(0,0,0,0)'; - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - } + // set constant values + this.options = {}; + this.defaultOptions = { + locale: 'en', + locales: locales, + clickToUse: false + }; + util.extend(this.options, this.defaultOptions); + + // containers for nodes and edges + this.body = { + container: container, + nodes: {}, + nodeIndices: [], + edges: {}, + edgeIndices: [], + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this), + once: this.once.bind(this) + }, + eventListeners: { + onTap: function onTap() {}, + onTouch: function onTouch() {}, + onDoubleTap: function onDoubleTap() {}, + onHold: function onHold() {}, + onDragStart: function onDragStart() {}, + onDrag: function onDrag() {}, + onDragEnd: function onDragEnd() {}, + onMouseWheel: function onMouseWheel() {}, + onPinch: function onPinch() {}, + onMouseMove: function onMouseMove() {}, + onRelease: function onRelease() {}, + onContext: function onContext() {} + }, + data: { + nodes: null, // A DataSet or DataView + edges: null // A DataSet or DataView + }, + functions: { + createNode: function createNode() {}, + createEdge: function createEdge() {}, + getPointer: function getPointer() {} + }, + view: { + scale: 1, + translation: { x: 0, y: 0 } } - }]); + }; - return NodeBase; - })(); + // bind the event listeners + this.bindEventListeners(); - exports['default'] = NodeBase; - module.exports = exports['default']; + // setting up all modules + this.images = new Images(function () { + return _this.body.emitter.emit('_requestRedraw'); + }); // object with images + this.groups = new _modulesGroups2['default'](); // object with groups + this.canvas = new _modulesCanvas2['default'](this.body); // DOM handler + this.selectionHandler = new _modulesSelectionHandler2['default'](this.body, this.canvas); // Selection handler + this.interactionHandler = new _modulesInteractionHandler2['default'](this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key + this.view = new _modulesView2['default'](this.body, this.canvas); // camera handler, does animations and zooms + this.renderer = new _modulesCanvasRenderer2['default'](this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into + this.physics = new _modulesPhysicsEngine2['default'](this.body); // physics engine, does all the simulations + this.layoutEngine = new _modulesLayoutEngine2['default'](this.body); // layout engine for inital layout and hierarchical layout + this.clustering = new _modulesClustering2['default'](this.body); // clustering api + this.manipulation = new _modulesManipulationSystem2['default'](this.body, this.canvas, this.selectionHandler); // data manipulation system -/***/ }, -/* 67 */ -/***/ function(module, exports, __webpack_require__) { + this.nodesHandler = new _modulesNodesHandler2['default'](this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options + this.edgesHandler = new _modulesEdgesHandler2['default'](this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options - 'use strict'; + // create the DOM elements + this.canvas._create(); - Object.defineProperty(exports, '__esModule', { - value: true - }); + // apply options + this.setOptions(options); - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + // load data (the disable start variable will be the same as the enabled clustering) + this.setData(data); + } - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + // Extend Network with an Emitter mixin + Emitter(Network.prototype); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + /** + * Set options + * @param {Object} options + */ + Network.prototype.setOptions = function (options) { + var _this2 = this; - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + if (options !== undefined) { - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + var errorFound = _sharedValidator2['default'].validate(options, _optionsJs.allOptions); + if (errorFound === true) { + console.log('%cErrors have been found in the supplied options object.', _sharedValidator.printStyle); + } - var _utilCircleImageBase = __webpack_require__(68); + // copy the global fields over + var fields = ['locale', 'locales', 'clickToUse']; + util.selectiveDeepExtend(fields, this.options, options); - var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); + // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. + options = this.layoutEngine.setOptions(options.layout, options); - var Circle = (function (_CircleImageBase) { - function Circle(options, body, labelModule) { - _classCallCheck(this, Circle); + this.canvas.setOptions(options); // options for canvas are in globals - _get(Object.getPrototypeOf(Circle.prototype), 'constructor', this).call(this, options, body, labelModule); - } + // pass the options to the modules + this.groups.setOptions(options.groups); + this.nodesHandler.setOptions(options.nodes); + this.edgesHandler.setOptions(options.edges); + this.physics.setOptions(options.physics); + this.manipulation.setOptions(options.manipulation, options, this.options); // manipulation uses the locales in the globals - _inherits(Circle, _CircleImageBase); + this.interactionHandler.setOptions(options.interaction); + this.renderer.setOptions(options.interaction); // options for rendering are in interaction + this.selectionHandler.setOptions(options.interaction); // options for selection are in interaction - _createClass(Circle, [{ - key: 'resize', - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.options.size = diameter / 2; + // reload the settings of the nodes to apply changes in groups that are not referenced by pointer. + if (options.groups !== undefined) { + this.body.emitter.emit('refreshNodes'); + } + // these two do not have options at the moment, here for completeness + //this.view.setOptions(options.view); + //this.clustering.setOptions(options.clustering); - this.width = diameter; - this.height = diameter; - this.radius = 0.5 * this.width; + if ('configure' in options) { + if (!this.configurator) { + this.configurator = new _sharedConfigurator2['default'](this, this.body.container, _optionsJs.configureOptions, this.canvas.pixelRatio); } + + this.configurator.setOptions(options.configure); } - }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); + // if the configuration system is enabled, copy all options and put them into the config system + if (this.configurator && this.configurator.options.enabled === true) { + var networkOptions = { nodes: {}, edges: {}, layout: {}, interaction: {}, manipulation: {}, physics: {}, global: {} }; + util.deepExtend(networkOptions.nodes, this.nodesHandler.options); + util.deepExtend(networkOptions.edges, this.edgesHandler.options); + util.deepExtend(networkOptions.layout, this.layoutEngine.options); + // load the selectionHandler and render default options in to the interaction group + util.deepExtend(networkOptions.interaction, this.selectionHandler.options); + util.deepExtend(networkOptions.interaction, this.renderer.options); - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + util.deepExtend(networkOptions.interaction, this.interactionHandler.options); + util.deepExtend(networkOptions.manipulation, this.manipulation.options); + util.deepExtend(networkOptions.physics, this.physics.options); - this.updateBoundingBox(x, y); - this.labelModule.draw(ctx, x, y, selected); - } - }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + // load globals into the global object + util.deepExtend(networkOptions.global, this.canvas.options); + util.deepExtend(networkOptions.global, this.options); + + this.configurator.setModuleOptions(networkOptions); } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + + // handle network global options + if (options.clickToUse !== undefined) { + if (options.clickToUse === true) { + if (this.activator === undefined) { + this.activator = new Activator(this.canvas.frame); + this.activator.on('change', function () { + _this2.body.emitter.emit('activate'); + }); + } + } else { + if (this.activator !== undefined) { + this.activator.destroy(); + delete this.activator; + } + this.body.emitter.emit('activate'); + } + } else { + this.body.emitter.emit('activate'); } - }]); - return Circle; - })(_utilCircleImageBase2['default']); + this.canvas.setSize(); + // start the physics simulation. Can be safely called multiple times. + this.body.emitter.emit('startSimulation'); + } + }; - exports['default'] = Circle; - module.exports = exports['default']; + /** + * Update the this.body.nodeIndices with the most recent node index list + * @private + */ + Network.prototype._updateVisibleIndices = function () { + var nodes = this.body.nodes; + var edges = this.body.edges; + this.body.nodeIndices = []; + this.body.edgeIndices = []; -/***/ }, -/* 68 */ -/***/ function(module, exports, __webpack_require__) { + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].options.hidden === false) { + this.body.nodeIndices.push(nodeId); + } + } + } - 'use strict'; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].options.hidden === false) { + this.body.edgeIndices.push(edgeId); + } + } + } + }; - Object.defineProperty(exports, '__esModule', { - value: true - }); + /** + * Bind all events + */ + Network.prototype.bindEventListeners = function () { + var _this3 = this; - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. + this.body.emitter.on('_dataChanged', function () { + // update shortcut lists + _this3._updateVisibleIndices(); + _this3.physics.updatePhysicsData(); + _this3.body.emitter.emit('_requestRedraw'); + // call the dataUpdated event because the only difference between the two is the updating of the indices + _this3.body.emitter.emit('_dataUpdated'); + }); + + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on('_dataUpdated', function () { + // update values + _this3._updateValueRange(_this3.body.nodes); + _this3._updateValueRange(_this3.body.edges); + // start simulation (can be called safely, even if already running) + _this3.body.emitter.emit('startSimulation'); + _this3.body.emitter.emit('_requestRedraw'); + }); + }; + + /** + * Set nodes and edges, and optionally options as well. + * + * @param {Object} data Object containing parameters: + * {Array | DataSet | DataView} [nodes] Array with nodes + * {Array | DataSet | DataView} [edges] Array with edges + * {String} [dot] String containing data in DOT format + * {String} [gephi] String containing data in gephi JSON format + * {Options} [options] Object with options + */ + Network.prototype.setData = function (data) { + // reset the physics engine. + this.body.emitter.emit('resetPhysics'); + this.body.emitter.emit('_resetData'); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + // unselect all to ensure no selections from old data are carried over. + this.selectionHandler.unselectAll(); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + if (data && data.dot && (data.nodes || data.edges)) { + throw new SyntaxError('Data must contain either parameter "dot" or ' + ' parameter pair "nodes" and "edges", but not both.'); + } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + // set options + this.setOptions(data && data.options); + // set all data + if (data && data.dot) { + console.log('The dot property has been depricated. Please use the static convertDot method to convert DOT into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertDot(dotString);'); + // parse DOT file + var dotData = dotparser.DOTToGraph(data.dot); + this.setData(dotData); + return; + } else if (data && data.gephi) { + // parse DOT file + console.log('The gephi property has been depricated. Please use the static convertGephi method to convert gephi into vis.network format and use the normal data format with nodes and edges. This converter is used like this: var data = vis.network.convertGephi(gephiJson);'); + var gephiData = gephiParser.parseGephi(data.gephi); + this.setData(gephiData); + return; + } else { + this.nodesHandler.setData(data && data.nodes, true); + this.edgesHandler.setData(data && data.edges, true); + } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + // emit change in data + this.body.emitter.emit('_dataChanged'); - var _utilNodeBase = __webpack_require__(66); + // find a stable position or start animating to a stable position + this.body.emitter.emit('initPhysics'); + }; - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + /** + * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. + * var network = new vis.Network(..); + * network.destroy(); + * network = null; + */ + Network.prototype.destroy = function () { + this.body.emitter.emit('destroy'); + // clear events + this.body.emitter.off(); + this.off(); - var CircleImageBase = (function (_NodeBase) { - function CircleImageBase(options, body, labelModule) { - _classCallCheck(this, CircleImageBase); + // delete modules + delete this.groups; + delete this.canvas; + delete this.selectionHandler; + delete this.interactionHandler; + delete this.view; + delete this.renderer; + delete this.physics; + delete this.layoutEngine; + delete this.clustering; + delete this.manipulation; + delete this.nodesHandler; + delete this.edgesHandler; + delete this.configurator; + delete this.images; - _get(Object.getPrototypeOf(CircleImageBase.prototype), 'constructor', this).call(this, options, body, labelModule); - this.labelOffset = 0; - this.imageLoaded = false; + for (var nodeId in this.body.nodes) { + delete this.body.nodes[nodeId]; + } + for (var edgeId in this.body.edges) { + delete this.body.edges[edgeId]; } - _inherits(CircleImageBase, _NodeBase); - - _createClass(CircleImageBase, [{ - key: '_resizeImage', + // remove the container and everything inside it recursively + util.recursiveDOMDelete(this.body.container); + }; - /** - * This function resizes the image by the options size when the image has not yet loaded. If the image has loaded, we - * force the update of the size again. - * - * @private - */ - value: function _resizeImage() { - var force = false; - if (!this.imageObj.width || !this.imageObj.height) { - // undefined or 0 - this.imageLoaded = false; - } else if (this.imageLoaded === false) { - this.imageLoaded = true; - force = true; - } + /** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Object} obj An object containing a set of Edges or Nodes + * The objects must have a method getValue() and + * setValueRange(min, max). + * @private + */ + Network.prototype._updateValueRange = function (obj) { + var id; - if (!this.width || !this.height || force === true) { - // undefined or 0 - var width, height, ratio; - if (this.imageObj.width && this.imageObj.height) { - // not undefined or 0 - width = 0; - height = 0; - } - if (this.imageObj.width > this.imageObj.height) { - ratio = this.imageObj.width / this.imageObj.height; - width = this.options.size * 2 * ratio || this.imageObj.width; - height = this.options.size * 2 || this.imageObj.height; - } else { - if (this.imageObj.width && this.imageObj.height) { - // not undefined or 0 - ratio = this.imageObj.height / this.imageObj.width; - } else { - ratio = 1; - } - width = this.options.size * 2 || this.imageObj.width; - height = this.options.size * 2 * ratio || this.imageObj.height; - } - this.width = width; - this.height = height; - this.radius = 0.5 * this.width; + // determine the range of the objects + var valueMin = undefined; + var valueMax = undefined; + var valueTotal = 0; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + var value = obj[id].getValue(); + if (value !== undefined) { + valueMin = valueMin === undefined ? value : Math.min(value, valueMin); + valueMax = valueMax === undefined ? value : Math.max(value, valueMax); + valueTotal += value; } } - }, { - key: '_drawRawCircle', - value: function _drawRawCircle(ctx, x, y, selected, hover, size) { - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(x, y, size); - - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); - - // disable shadows for other elements. - this.disableShadow(ctx); - - ctx.stroke(); - } - }, { - key: '_drawImageAtPosition', - value: function _drawImageAtPosition(ctx) { - if (this.imageObj.width != 0) { - // draw the image - ctx.globalAlpha = 1; - - // draw shadow if enabled - this.enableShadow(ctx); - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + } - // disable shadows for other elements. - this.disableShadow(ctx); + // adjust the range of all objects + if (valueMin !== undefined && valueMax !== undefined) { + for (id in obj) { + if (obj.hasOwnProperty(id)) { + obj[id].setValueRange(valueMin, valueMax, valueTotal); } } - }, { - key: '_drawImageLabel', - value: function _drawImageLabel(ctx, x, y, selected) { - var yLabel; - var offset = 0; - - if (this.height !== undefined) { - offset = this.height * 0.5; - var labelDimensions = this.labelModule.getTextSize(ctx); - if (labelDimensions.lineCount >= 1) { - offset += labelDimensions.height / 2; - } - } - - yLabel = y + offset; + } + }; - if (this.options.label) { - this.labelOffset = offset; - } - this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); - } - }]); + /** + * Returns true when the Network is active. + * @returns {boolean} + */ + Network.prototype.isActive = function () { + return !this.activator || this.activator.active; + }; - return CircleImageBase; - })(_utilNodeBase2['default']); + Network.prototype.setSize = function () { + return this.canvas.setSize.apply(this.canvas, arguments); + }; + Network.prototype.canvasToDOM = function () { + return this.canvas.canvasToDOM.apply(this.canvas, arguments); + }; + Network.prototype.DOMtoCanvas = function () { + return this.canvas.DOMtoCanvas(this.canvas, arguments); + }; + Network.prototype.findNode = function () { + return this.clustering.findNode.apply(this.clustering, arguments); + }; + Network.prototype.isCluster = function () { + return this.clustering.isCluster.apply(this.clustering, arguments); + }; + Network.prototype.openCluster = function () { + return this.clustering.openCluster.apply(this.clustering, arguments); + }; + Network.prototype.cluster = function () { + return this.clustering.cluster.apply(this.clustering, arguments); + }; + Network.prototype.getNodesInCluster = function () { + return this.clustering.getNodesInCluster.apply(this.clustering, arguments); + }; + Network.prototype.clusterByConnection = function () { + return this.clustering.clusterByConnection.apply(this.clustering, arguments); + }; + Network.prototype.clusterByHubsize = function () { + return this.clustering.clusterByHubsize.apply(this.clustering, arguments); + }; + Network.prototype.clusterOutliers = function () { + return this.clustering.clusterOutliers.apply(this.clustering, arguments); + }; + Network.prototype.getSeed = function () { + return this.layoutEngine.getSeed.apply(this.layoutEngine, arguments); + }; + Network.prototype.enableEditMode = function () { + return this.manipulation.enableEditMode.apply(this.manipulation, arguments); + }; + Network.prototype.disableEditMode = function () { + return this.manipulation.disableEditMode.apply(this.manipulation, arguments); + }; + Network.prototype.addNodeMode = function () { + return this.manipulation.addNodeMode.apply(this.manipulation, arguments); + }; + Network.prototype.editNode = function () { + return this.manipulation.editNode.apply(this.manipulation, arguments); + }; + Network.prototype.editNodeMode = function () { + console.log('Depricated: Please use editNode instead of editNodeMode.');return this.manipulation.editNode.apply(this.manipulation, arguments); + }; + Network.prototype.addEdgeMode = function () { + return this.manipulation.addEdgeMode.apply(this.manipulation, arguments); + }; + Network.prototype.editEdgeMode = function () { + return this.manipulation.editEdgeMode.apply(this.manipulation, arguments); + }; + Network.prototype.deleteSelected = function () { + return this.manipulation.deleteSelected.apply(this.manipulation, arguments); + }; + Network.prototype.getPositions = function () { + return this.nodesHandler.getPositions.apply(this.nodesHandler, arguments); + }; + Network.prototype.storePositions = function () { + return this.nodesHandler.storePositions.apply(this.nodesHandler, arguments); + }; + Network.prototype.getBoundingBox = function () { + return this.nodesHandler.getBoundingBox.apply(this.nodesHandler, arguments); + }; + Network.prototype.getConnectedNodes = function (objectId) { + if (this.body.nodes[objectId] !== undefined) { + return this.nodesHandler.getConnectedNodes.apply(this.nodesHandler, arguments); + } else { + return this.edgesHandler.getConnectedNodes.apply(this.edgesHandler, arguments); + } + }; + Network.prototype.getConnectedEdges = function () { + return this.nodesHandler.getConnectedEdges.apply(this.nodesHandler, arguments); + }; + Network.prototype.startSimulation = function () { + return this.physics.startSimulation.apply(this.physics, arguments); + }; + Network.prototype.stopSimulation = function () { + return this.physics.stopSimulation.apply(this.physics, arguments); + }; + Network.prototype.stabilize = function () { + return this.physics.stabilize.apply(this.physics, arguments); + }; + Network.prototype.getSelection = function () { + return this.selectionHandler.getSelection.apply(this.selectionHandler, arguments); + }; + Network.prototype.getSelectedNodes = function () { + return this.selectionHandler.getSelectedNodes.apply(this.selectionHandler, arguments); + }; + Network.prototype.getSelectedEdges = function () { + return this.selectionHandler.getSelectedEdges.apply(this.selectionHandler, arguments); + }; + Network.prototype.getNodeAt = function () { + var node = this.selectionHandler.getNodeAt.apply(this.selectionHandler, arguments); + if (node !== undefined && node.id !== undefined) { + return node.id; + } + return node; + }; + Network.prototype.getEdgeAt = function () { + var edge = this.selectionHandler.getEdgeAt.apply(this.selectionHandler, arguments); + if (edge !== undefined && edge.id !== undefined) { + return edge.id; + } + return edge; + }; + Network.prototype.selectNodes = function () { + return this.selectionHandler.selectNodes.apply(this.selectionHandler, arguments); + }; + Network.prototype.selectEdges = function () { + return this.selectionHandler.selectEdges.apply(this.selectionHandler, arguments); + }; + Network.prototype.unselectAll = function () { + return this.selectionHandler.unselectAll.apply(this.selectionHandler, arguments); + }; + Network.prototype.redraw = function () { + return this.renderer.redraw.apply(this.renderer, arguments); + }; + Network.prototype.getScale = function () { + return this.view.getScale.apply(this.view, arguments); + }; + Network.prototype.getViewPosition = function () { + return this.view.getViewPosition.apply(this.view, arguments); + }; + Network.prototype.fit = function () { + return this.view.fit.apply(this.view, arguments); + }; + Network.prototype.moveTo = function () { + return this.view.moveTo.apply(this.view, arguments); + }; + Network.prototype.focus = function () { + return this.view.focus.apply(this.view, arguments); + }; + Network.prototype.releaseNode = function () { + return this.view.releaseNode.apply(this.view, arguments); + }; - exports['default'] = CircleImageBase; - module.exports = exports['default']; + module.exports = Network; /***/ }, -/* 69 */ +/* 66 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; + "use strict"; - Object.defineProperty(exports, '__esModule', { + Object.defineProperty(exports, "__esModule", { value: true }); - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + var util = __webpack_require__(14); - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + /** + * @class Groups + * This class can store groups and options specific for groups. + */ - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + var Groups = (function () { + function Groups() { + _classCallCheck(this, Groups); - var _utilCircleImageBase = __webpack_require__(68); + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; - var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); + this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue + { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow + { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red + { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green + { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta + { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple + { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange + { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue + { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink + { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint - var CircularImage = (function (_CircleImageBase) { - function CircularImage(options, body, labelModule, imageObj) { - _classCallCheck(this, CircularImage); + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - _get(Object.getPrototypeOf(CircularImage.prototype), 'constructor', this).call(this, options, body, labelModule); - this.imageObj = imageObj; - this._swapToImageResizeWhenImageLoaded = true; + { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange + { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue + { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green + { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta + { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple + { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue + { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink + { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint + + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } } // 20:bright red + ]; + + this.options = {}; + this.defaultOptions = { + useDefaultGroups: true + }; + util.extend(this.options, this.defaultOptions); } - _inherits(CircularImage, _CircleImageBase); + _createClass(Groups, [{ + key: "setOptions", + value: function setOptions(options) { + var optionFields = ["useDefaultGroups"]; - _createClass(CircularImage, [{ - key: 'resize', - value: function resize() { - if (this.imageObj.src === undefined || this.imageObj.width === undefined || this.imageObj.height === undefined) { - if (!this.width) { - var diameter = this.options.size * 2; - this.width = diameter; - this.height = diameter; - this._swapToImageResizeWhenImageLoaded = true; - this.radius = 0.5 * this.width; - } - } else { - if (this._swapToImageResizeWhenImageLoaded) { - this.width = undefined; - this.height = undefined; - this._swapToImageResizeWhenImageLoaded = false; + if (options !== undefined) { + for (var groupName in options) { + if (options.hasOwnProperty(groupName)) { + if (optionFields.indexOf(groupName) === -1) { + var group = options[groupName]; + this.add(groupName, group); + } + } } - this._resizeImage(); } } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(); - - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var size = Math.min(0.5 * this.height, 0.5 * this.width); - - this._drawRawCircle(ctx, x, y, selected, hover, size); - - ctx.save(); - ctx.circle(x, y, size); - ctx.stroke(); - ctx.clip(); - - this._drawImageAtPosition(ctx); - - ctx.restore(); - - this._drawImageLabel(ctx, x, y, selected); + key: "clear", - this.updateBoundingBox(x, y); + /** + * Clear all groups + */ + value: function clear() { + this.groups = {}; + this.groupsArray = []; } }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); + key: "get", + + /** + * get group options of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group options + */ + value: function get(groupname) { + var group = this.groups[groupname]; + if (group === undefined) { + if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } else { + // create new group + var index = this.defaultIndex % this.defaultGroups.length; + this.defaultIndex++; + group = {}; + group.color = this.defaultGroups[index]; + this.groups[groupname] = group; + } + } + + return group; } }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); + key: "add", + + /** + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ + value: function add(groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; } }]); - return CircularImage; - })(_utilCircleImageBase2['default']); + return Groups; + })(); - exports['default'] = CircularImage; - module.exports = exports['default']; + exports["default"] = Groups; + module.exports = exports["default"]; /***/ }, -/* 70 */ +/* 67 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -29562,159 +30009,516 @@ return /******/ (function(modules) { // webpackBootstrap var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + var _sharedLabel = __webpack_require__(4); - var _utilNodeBase = __webpack_require__(66); + var _sharedLabel2 = _interopRequireDefault(_sharedLabel); - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + var _nodesShapesBox = __webpack_require__(68); - var Database = (function (_NodeBase) { - function Database(options, body, labelModule) { - _classCallCheck(this, Database); + var _nodesShapesBox2 = _interopRequireDefault(_nodesShapesBox); - _get(Object.getPrototypeOf(Database.prototype), 'constructor', this).call(this, options, body, labelModule); + var _nodesShapesCircle = __webpack_require__(70); + + var _nodesShapesCircle2 = _interopRequireDefault(_nodesShapesCircle); + + var _nodesShapesCircularImage = __webpack_require__(72); + + var _nodesShapesCircularImage2 = _interopRequireDefault(_nodesShapesCircularImage); + + var _nodesShapesDatabase = __webpack_require__(73); + + var _nodesShapesDatabase2 = _interopRequireDefault(_nodesShapesDatabase); + + var _nodesShapesDiamond = __webpack_require__(74); + + var _nodesShapesDiamond2 = _interopRequireDefault(_nodesShapesDiamond); + + var _nodesShapesDot = __webpack_require__(76); + + var _nodesShapesDot2 = _interopRequireDefault(_nodesShapesDot); + + var _nodesShapesEllipse = __webpack_require__(77); + + var _nodesShapesEllipse2 = _interopRequireDefault(_nodesShapesEllipse); + + var _nodesShapesIcon = __webpack_require__(78); + + var _nodesShapesIcon2 = _interopRequireDefault(_nodesShapesIcon); + + var _nodesShapesImage = __webpack_require__(79); + + var _nodesShapesImage2 = _interopRequireDefault(_nodesShapesImage); + + var _nodesShapesSquare = __webpack_require__(80); + + var _nodesShapesSquare2 = _interopRequireDefault(_nodesShapesSquare); + + var _nodesShapesStar = __webpack_require__(81); + + var _nodesShapesStar2 = _interopRequireDefault(_nodesShapesStar); + + var _nodesShapesText = __webpack_require__(82); + + var _nodesShapesText2 = _interopRequireDefault(_nodesShapesText); + + var _nodesShapesTriangle = __webpack_require__(83); + + var _nodesShapesTriangle2 = _interopRequireDefault(_nodesShapesTriangle); + + var _nodesShapesTriangleDown = __webpack_require__(84); + + var _nodesShapesTriangleDown2 = _interopRequireDefault(_nodesShapesTriangleDown); + + var _sharedValidator = __webpack_require__(53); + + var _sharedValidator2 = _interopRequireDefault(_sharedValidator); + + var util = __webpack_require__(14); + + /** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} options An object containing options for the node. All + * options are optional, except for the id. + * {number} id Id of the node. Required + * {string} label Text label for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} shape Node shape, available: + * "database", "circle", "ellipse", + * "box", "image", "text", "dot", + * "star", "triangle", "triangleDown", + * "square", "icon" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Network.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Network.Groups} grouplist A list with groups. Needed for + * retrieving group options + * @param {Object} constants An object with default values for + * example for the color + * + */ + + var Node = (function () { + function Node(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Node); + + this.options = util.bridgeObject(globalOptions); + this.body = body; + + this.edges = []; // all edges connected to this node + + // set defaults for the options + this.id = undefined; + this.imagelist = imagelist; + this.grouplist = grouplist; + + // state options + this.x = undefined; + this.y = undefined; + this.baseSize = this.options.size; + this.baseFontSize = this.options.font.size; + this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate + this.selected = false; + this.hover = false; + + this.labelModule = new _sharedLabel2['default'](this.body, this.options); + this.setOptions(options); } - _inherits(Database, _NodeBase); + _createClass(Node, [{ + key: 'attachEdge', - _createClass(Database, [{ - key: 'resize', - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; - this.radius = 0.5 * this.width; + /** + * Attach a edge to the node + * @param {Edge} edge + */ + value: function attachEdge(edge) { + if (this.edges.indexOf(edge) === -1) { + this.edges.push(edge); } } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + key: 'detachEdge', - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + /** + * Detach a edge from the node + * @param {Edge} edge + */ + value: function detachEdge(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + } + }, { + key: 'togglePhysics', - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + /** + * Enable or disable the physics. + * @param status + */ + value: function togglePhysics(status) { + this.options.physics = status; + } + }, { + key: 'setOptions', - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); + /** + * Set or overwrite options for the node + * @param {Object} options an object with options + * @param {Object} constants and object with default, global options + */ + value: function setOptions(options) { + if (!options) { + return; + } + // basic options + if (options.id !== undefined) { + this.id = options.id; + } - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); + if (this.id === undefined) { + throw 'Node must have an id'; + } - // disable shadows for other elements. - this.disableShadow(ctx); + // set these options locally + if (options.x !== undefined) { + this.x = parseInt(options.x);this.predefinedPosition = true; + } + if (options.y !== undefined) { + this.y = parseInt(options.y);this.predefinedPosition = true; + } + if (options.size !== undefined) { + this.baseSize = options.size; + } + if (options.value !== undefined) { + options.value = parseFloat(options.value); + } - ctx.stroke(); + // copy group options + if (typeof options.group === 'number' || typeof options.group === 'string' && options.group != '') { + var groupObj = this.grouplist.get(options.group); + util.deepExtend(this.options, groupObj); + // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. + this.options.color = util.parseColor(this.options.color); + } - this.updateBoundingBox(x, y, ctx); + // this transforms all shorthands into fully defined options + Node.parseOptions(this.options, options, true); - this.labelModule.draw(ctx, x, y, selected); + // load the images + if (this.options.image !== undefined) { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id); + } else { + throw 'No imagelist provided'; + } + } + + this.updateShape(); + this.updateLabelModule(); + + // reset the size of the node, this can be changed + this._reset(); + + if (options.hidden !== undefined || options.physics !== undefined) { + return true; + } + return false; } }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y, ctx) { - this.resize(ctx); + key: 'updateLabelModule', + value: function updateLabelModule() { + if (this.options.label === undefined || this.options.label === null) { + this.options.label = ''; + } + this.labelModule.setOptions(this.options, true); + if (this.labelModule.baseSize !== undefined) { + this.baseFontSize = this.labelModule.baseSize; + } + } + }, { + key: 'updateShape', + value: function updateShape() { + // choose draw method depending on the shape + switch (this.options.shape) { + case 'box': + this.shape = new _nodesShapesBox2['default'](this.options, this.body, this.labelModule); + break; + case 'circle': + this.shape = new _nodesShapesCircle2['default'](this.options, this.body, this.labelModule); + break; + case 'circularImage': + this.shape = new _nodesShapesCircularImage2['default'](this.options, this.body, this.labelModule, this.imageObj); + break; + case 'database': + this.shape = new _nodesShapesDatabase2['default'](this.options, this.body, this.labelModule); + break; + case 'diamond': + this.shape = new _nodesShapesDiamond2['default'](this.options, this.body, this.labelModule); + break; + case 'dot': + this.shape = new _nodesShapesDot2['default'](this.options, this.body, this.labelModule); + break; + case 'ellipse': + this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); + break; + case 'icon': + this.shape = new _nodesShapesIcon2['default'](this.options, this.body, this.labelModule); + break; + case 'image': + this.shape = new _nodesShapesImage2['default'](this.options, this.body, this.labelModule, this.imageObj); + break; + case 'square': + this.shape = new _nodesShapesSquare2['default'](this.options, this.body, this.labelModule); + break; + case 'star': + this.shape = new _nodesShapesStar2['default'](this.options, this.body, this.labelModule); + break; + case 'text': + this.shape = new _nodesShapesText2['default'](this.options, this.body, this.labelModule); + break; + case 'triangle': + this.shape = new _nodesShapesTriangle2['default'](this.options, this.body, this.labelModule); + break; + case 'triangleDown': + this.shape = new _nodesShapesTriangleDown2['default'](this.options, this.body, this.labelModule); + break; + default: + this.shape = new _nodesShapesEllipse2['default'](this.options, this.body, this.labelModule); + break; + } + this._reset(); + } + }, { + key: 'select', - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; + /** + * select this node + */ + value: function select() { + this.selected = true; + this._reset(); + } + }, { + key: 'unselect', - this.boundingBox.left = this.left; - this.boundingBox.top = this.top; - this.boundingBox.bottom = this.top + this.height; - this.boundingBox.right = this.left + this.width; + /** + * unselect this node + */ + value: function unselect() { + this.selected = false; + this._reset(); + } + }, { + key: '_reset', + + /** + * Reset the calculated size of the node, forces it to recalculate its size + * @private + */ + value: function _reset() { + this.shape.width = undefined; + this.shape.height = undefined; + } + }, { + key: 'getTitle', + + /** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ + value: function getTitle() { + return this.options.title; } }, { key: 'distanceToBorder', + + /** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + return this.shape.distanceToBorder(ctx, angle); } - }]); - - return Database; - })(_utilNodeBase2['default']); + }, { + key: 'isFixed', - exports['default'] = Database; - module.exports = exports['default']; + /** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ + value: function isFixed() { + return this.options.fixed.x && this.options.fixed.y; + } + }, { + key: 'isSelected', -/***/ }, -/* 71 */ -/***/ function(module, exports, __webpack_require__) { + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + } + }, { + key: 'getValue', - 'use strict'; + /** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.options.value; + } + }, { + key: 'setValueRange', - Object.defineProperty(exports, '__esModule', { - value: true - }); + /** + * Adjust the value range of the node. The node will adjust it's size + * based on its value. + * @param {Number} min + * @param {Number} max + */ + value: function setValueRange(min, max, total) { + if (this.options.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); + var sizeDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled === true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + } + this.options.size = this.options.scaling.min + scale * sizeDiff; + } else { + this.options.size = this.baseSize; + this.options.font.size = this.baseFontSize; + } + } + }, { + key: 'draw', - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + /** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function draw(ctx) { + this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); + } + }, { + key: 'updateBoundingBox', - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + /** + * Update the bounding box of the shape + */ + value: function updateBoundingBox(ctx) { + this.shape.updateBoundingBox(this.x, this.y, ctx); + } + }, { + key: 'resize', - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + /** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function resize(ctx) { + this.shape.resize(ctx); + } + }, { + key: 'isOverlappingWith', - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ + value: function isOverlappingWith(obj) { + return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; + } + }, { + key: 'isBoundingBoxOverlappingWith', - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ + value: function isBoundingBoxOverlappingWith(obj) { + return this.shape.boundingBox.left < obj.right && this.shape.boundingBox.right > obj.left && this.shape.boundingBox.top < obj.bottom && this.shape.boundingBox.bottom > obj.top; + } + }], [{ + key: 'parseOptions', - var _utilShapeBase = __webpack_require__(72); + /** + * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined. + * Static so it can also be used by the handler. + * @param parentOptions + * @param newOptions + */ + value: function parseOptions(parentOptions, newOptions) { + var allowDeletion = arguments[2] === undefined ? false : arguments[2]; - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + var fields = ['color', 'font', 'fixed', 'shadow']; + util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion); - var Diamond = (function (_ShapeBase) { - function Diamond(options, body, labelModule) { - _classCallCheck(this, Diamond); + // merge the shadow options into the parent. + util.mergeOptions(parentOptions, newOptions, 'shadow'); - _get(Object.getPrototypeOf(Diamond.prototype), 'constructor', this).call(this, options, body, labelModule); - } + // individual shape newOptions + if (newOptions.color !== undefined && newOptions.color !== null) { + var parsedColor = util.parseColor(newOptions.color); + util.fillIfDefined(parentOptions.color, parsedColor); + } else if (allowDeletion === true && newOptions.color === null) { + parentOptions.color = undefined; + delete parentOptions.color; + } - _inherits(Diamond, _ShapeBase); + // handle the fixed options + if (newOptions.fixed !== undefined && newOptions.fixed !== null) { + if (typeof newOptions.fixed === 'boolean') { + parentOptions.fixed.x = newOptions.fixed; + parentOptions.fixed.y = newOptions.fixed; + } else { + if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') { + parentOptions.fixed.x = newOptions.fixed.x; + } + if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') { + parentOptions.fixed.y = newOptions.fixed.y; + } + } + } - _createClass(Diamond, [{ - key: 'resize', - value: function resize(ctx) { - this._resizeShape(); - } - }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'diamond', 4, x, y, selected, hover); - } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); + // handle the font options + if (newOptions.font !== undefined) { + _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); + } + + // handle the scaling options, specifically the label part + if (newOptions.scaling !== undefined) { + util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); + } } }]); - return Diamond; - })(_utilShapeBase2['default']); + return Node; + })(); - exports['default'] = Diamond; + exports['default'] = Node; module.exports = exports['default']; /***/ }, -/* 72 */ +/* 68 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -29733,34 +30537,34 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(66); + var _utilNodeBase = __webpack_require__(69); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var ShapeBase = (function (_NodeBase) { - function ShapeBase(options, body, labelModule) { - _classCallCheck(this, ShapeBase); + var Box = (function (_NodeBase) { + function Box(options, body, labelModule) { + _classCallCheck(this, Box); - _get(Object.getPrototypeOf(ShapeBase.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Box.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(ShapeBase, _NodeBase); + _inherits(Box, _NodeBase); - _createClass(ShapeBase, [{ - key: '_resizeShape', - value: function _resizeShape() { + _createClass(Box, [{ + key: 'resize', + value: function resize(ctx, selected) { if (this.width === undefined) { - var size = 2 * this.options.size; - this.width = size; - this.height = size; + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; this.radius = 0.5 * this.width; } } }, { - key: '_drawShape', - value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { - this._resizeShape(); - + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); this.left = x - this.width / 2; this.top = y - this.height / 2; @@ -29771,8 +30575,11 @@ return /******/ (function(modules) { // webpackBootstrap ctx.lineWidth = selected ? selectionLineWidth : borderWidth; ctx.lineWidth /= this.body.view.scale; ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](x, y, this.options.size); + + var borderRadius = 6; + ctx.roundRect(this.left, this.top, this.width, this.height, borderRadius); // draw shadow if enabled this.enableShadow(ctx); @@ -29783,37 +30590,40 @@ return /******/ (function(modules) { // webpackBootstrap ctx.stroke(); - if (this.options.label !== undefined) { - var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. - this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); - } - this.updateBoundingBox(x, y); + this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; - if (this.options.label !== undefined && this.labelModule.size.width > 0) { - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + 3); - } + this.boundingBox.left = this.left; + this.boundingBox.top = this.top; + this.boundingBox.bottom = this.top + this.height; + this.boundingBox.right = this.left + this.width; + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); } }]); - return ShapeBase; + return Box; })(_utilNodeBase2['default']); - exports['default'] = ShapeBase; + exports['default'] = Box; module.exports = exports['default']; /***/ }, -/* 73 */ +/* 69 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -29824,52 +30634,64 @@ return /******/ (function(modules) { // webpackBootstrap var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - - var _utilShapeBase = __webpack_require__(72); - - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - - var Dot = (function (_ShapeBase) { - function Dot(options, body, labelModule) { - _classCallCheck(this, Dot); + var NodeBase = (function () { + function NodeBase(options, body, labelModule) { + _classCallCheck(this, NodeBase); - _get(Object.getPrototypeOf(Dot.prototype), 'constructor', this).call(this, options, body, labelModule); + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.top = undefined; + this.left = undefined; + this.height = undefined; + this.width = undefined; + this.radius = undefined; + this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; } - _inherits(Dot, _ShapeBase); - - _createClass(Dot, [{ - key: 'resize', - value: function resize(ctx) { - this._resizeShape(); + _createClass(NodeBase, [{ + key: 'setOptions', + value: function setOptions(options) { + this.options = options; } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'circle', 2, x, y, selected, hover); + key: '_distanceToBorder', + value: function _distanceToBorder(angle) { + var borderWidth = 1; + return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; } }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - return this.options.size + this.options.borderWidth; + key: 'enableShadow', + value: function enableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0.5)'; + ctx.shadowBlur = this.options.shadow.size; + ctx.shadowOffsetX = this.options.shadow.x; + ctx.shadowOffsetY = this.options.shadow.y; + } + } + }, { + key: 'disableShadow', + value: function disableShadow(ctx) { + if (this.options.shadow.enabled === true) { + ctx.shadowColor = 'rgba(0,0,0,0)'; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + } } }]); - return Dot; - })(_utilShapeBase2['default']); + return NodeBase; + })(); - exports['default'] = Dot; + exports['default'] = NodeBase; module.exports = exports['default']; /***/ }, -/* 74 */ +/* 70 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -29888,30 +30710,30 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(66); + var _utilCircleImageBase = __webpack_require__(71); - var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); + var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); - var Ellipse = (function (_NodeBase) { - function Ellipse(options, body, labelModule) { - _classCallCheck(this, Ellipse); + var Circle = (function (_CircleImageBase) { + function Circle(options, body, labelModule) { + _classCallCheck(this, Circle); - _get(Object.getPrototypeOf(Ellipse.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Circle.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Ellipse, _NodeBase); + _inherits(Circle, _CircleImageBase); - _createClass(Ellipse, [{ + _createClass(Circle, [{ key: 'resize', value: function resize(ctx, selected) { if (this.width === undefined) { + var margin = 5; var textSize = this.labelModule.getTextSize(ctx, selected); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.options.size = diameter / 2; - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } + this.width = diameter; + this.height = diameter; this.radius = 0.5 * this.width; } } @@ -29919,66 +30741,47 @@ return /******/ (function(modules) { // webpackBootstrap key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(ctx, selected); - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; - - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.ellipse(this.left, this.top, this.width, this.height); - - // draw shadow if enabled - this.enableShadow(ctx); - ctx.fill(); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - // disable shadows for other elements. - this.disableShadow(ctx); + this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); - ctx.stroke(); + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; this.updateBoundingBox(x, y); this.labelModule.draw(ctx, x, y, selected); } }, { key: 'updateBoundingBox', - value: function updateBoundingBox(x, y, ctx) { - this.resize(ctx, false); // just in case - - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; - - this.boundingBox.left = this.left; - this.boundingBox.top = this.top; - this.boundingBox.bottom = this.top + this.height; - this.boundingBox.right = this.left + this.width; + value: function updateBoundingBox(x, y) { + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); - var a = this.width * 0.5; - var b = this.height * 0.5; + var a = this.width / 2; + var b = this.height / 2; var w = Math.sin(angle) * a; var h = Math.cos(angle) * b; return a * b / Math.sqrt(w * w + h * h); } }]); - return Ellipse; - })(_utilNodeBase2['default']); + return Circle; + })(_utilCircleImageBase2['default']); - exports['default'] = Ellipse; + exports['default'] = Circle; module.exports = exports['default']; /***/ }, -/* 75 */ +/* 71 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -29997,104 +30800,137 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(66); + var _utilNodeBase = __webpack_require__(69); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var Icon = (function (_NodeBase) { - function Icon(options, body, labelModule) { - _classCallCheck(this, Icon); + var CircleImageBase = (function (_NodeBase) { + function CircleImageBase(options, body, labelModule) { + _classCallCheck(this, CircleImageBase); - _get(Object.getPrototypeOf(Icon.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(CircleImageBase.prototype), 'constructor', this).call(this, options, body, labelModule); + this.labelOffset = 0; + this.imageLoaded = false; } - _inherits(Icon, _NodeBase); + _inherits(CircleImageBase, _NodeBase); - _createClass(Icon, [{ - key: 'resize', - value: function resize(ctx) { - if (this.width === undefined) { - var margin = 5; - var iconSize = { - width: Number(this.options.icon.size), - height: Number(this.options.icon.size) - }; - this.width = iconSize.width + 2 * margin; - this.height = iconSize.height + 2 * margin; + _createClass(CircleImageBase, [{ + key: '_resizeImage', + + /** + * This function resizes the image by the options size when the image has not yet loaded. If the image has loaded, we + * force the update of the size again. + * + * @private + */ + value: function _resizeImage() { + var force = false; + if (!this.imageObj.width || !this.imageObj.height) { + // undefined or 0 + this.imageLoaded = false; + } else if (this.imageLoaded === false) { + this.imageLoaded = true; + force = true; + } + + if (!this.width || !this.height || force === true) { + // undefined or 0 + var width, height, ratio; + if (this.imageObj.width && this.imageObj.height) { + // not undefined or 0 + width = 0; + height = 0; + } + if (this.imageObj.width > this.imageObj.height) { + ratio = this.imageObj.width / this.imageObj.height; + width = this.options.size * 2 * ratio || this.imageObj.width; + height = this.options.size * 2 || this.imageObj.height; + } else { + if (this.imageObj.width && this.imageObj.height) { + // not undefined or 0 + ratio = this.imageObj.height / this.imageObj.width; + } else { + ratio = 1; + } + width = this.options.size * 2 || this.imageObj.width; + height = this.options.size * 2 * ratio || this.imageObj.height; + } + this.width = width; + this.height = height; this.radius = 0.5 * this.width; } } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.options.icon.size = this.options.icon.size || 50; + key: '_drawRawCircle', + value: function _drawRawCircle(ctx, x, y, selected, hover, size) { + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; - this._icon(ctx, x, y, selected); + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - if (this.options.label !== undefined) { - var iconTextSpacing = 5; - this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); - } + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - this.updateBoundingBox(x, y); - } - }, { - key: 'updateBoundingBox', - value: function updateBoundingBox(x, y) { - this.boundingBox.top = y - this.options.icon.size * 0.5; - this.boundingBox.left = x - this.options.icon.size * 0.5; - this.boundingBox.right = x + this.options.icon.size * 0.5; - this.boundingBox.bottom = y + this.options.icon.size * 0.5; + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.circle(x, y, size); - if (this.options.label !== undefined && this.labelModule.size.width > 0) { - var iconTextSpacing = 5; - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + iconTextSpacing); - } - } - }, { - key: '_icon', - value: function _icon(ctx, x, y, selected) { - var iconSize = Number(this.options.icon.size); + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); - if (this.options.icon.code !== undefined) { - ctx.font = (selected ? 'bold ' : '') + iconSize + 'px ' + this.options.icon.face; + // disable shadows for other elements. + this.disableShadow(ctx); - // draw icon - ctx.fillStyle = this.options.icon.color || 'black'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + ctx.stroke(); + } + }, { + key: '_drawImageAtPosition', + value: function _drawImageAtPosition(ctx) { + if (this.imageObj.width != 0) { + // draw the image + ctx.globalAlpha = 1; // draw shadow if enabled this.enableShadow(ctx); - ctx.fillText(this.options.icon.code, x, y); + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); // disable shadows for other elements. this.disableShadow(ctx); - } else { - console.error('When using the icon shape, you need to define the code in the icon options object. This can be done per node or globally.'); } } }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); + key: '_drawImageLabel', + value: function _drawImageLabel(ctx, x, y, selected) { + var yLabel; + var offset = 0; + + if (this.height !== undefined) { + offset = this.height * 0.5; + var labelDimensions = this.labelModule.getTextSize(ctx); + if (labelDimensions.lineCount >= 1) { + offset += labelDimensions.height / 2; + } + } + + yLabel = y + offset; + + if (this.options.label) { + this.labelOffset = offset; + } + this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); } }]); - return Icon; + return CircleImageBase; })(_utilNodeBase2['default']); - exports['default'] = Icon; + exports['default'] = CircleImageBase; module.exports = exports['default']; /***/ }, -/* 76 */ +/* 72 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30113,76 +30949,93 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilCircleImageBase = __webpack_require__(68); + var _utilCircleImageBase = __webpack_require__(71); var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); - var Image = (function (_CircleImageBase) { - function Image(options, body, labelModule, imageObj) { - _classCallCheck(this, Image); + var CircularImage = (function (_CircleImageBase) { + function CircularImage(options, body, labelModule, imageObj) { + _classCallCheck(this, CircularImage); - _get(Object.getPrototypeOf(Image.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(CircularImage.prototype), 'constructor', this).call(this, options, body, labelModule); this.imageObj = imageObj; + this._swapToImageResizeWhenImageLoaded = true; } - _inherits(Image, _CircleImageBase); + _inherits(CircularImage, _CircleImageBase); - _createClass(Image, [{ + _createClass(CircularImage, [{ key: 'resize', value: function resize() { - this._resizeImage(); + if (this.imageObj.src === undefined || this.imageObj.width === undefined || this.imageObj.height === undefined) { + if (!this.width) { + var diameter = this.options.size * 2; + this.width = diameter; + this.height = diameter; + this._swapToImageResizeWhenImageLoaded = true; + this.radius = 0.5 * this.width; + } + } else { + if (this._swapToImageResizeWhenImageLoaded) { + this.width = undefined; + this.height = undefined; + this._swapToImageResizeWhenImageLoaded = false; + } + this._resizeImage(); + } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { this.resize(); + this.left = x - this.width / 2; this.top = y - this.height / 2; + var size = Math.min(0.5 * this.height, 0.5 * this.width); + + this._drawRawCircle(ctx, x, y, selected, hover, size); + + ctx.save(); + ctx.circle(x, y, size); + ctx.stroke(); + ctx.clip(); + this._drawImageAtPosition(ctx); - this._drawImageLabel(ctx, x, y, selected || hover); + ctx.restore(); + + this._drawImageLabel(ctx, x, y, selected); this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { - this.resize(); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - - if (this.options.label !== undefined && this.labelModule.size.width > 0) { - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); - } + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + return this._distanceToBorder(angle); } }]); - return Image; + return CircularImage; })(_utilCircleImageBase2['default']); - exports['default'] = Image; + exports['default'] = CircularImage; module.exports = exports['default']; /***/ }, -/* 77 */ +/* 73 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30201,45 +31054,95 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(72); + var _utilNodeBase = __webpack_require__(69); - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var Square = (function (_ShapeBase) { - function Square(options, body, labelModule) { - _classCallCheck(this, Square); + var Database = (function (_NodeBase) { + function Database(options, body, labelModule) { + _classCallCheck(this, Database); - _get(Object.getPrototypeOf(Square.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Database.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Square, _ShapeBase); + _inherits(Database, _NodeBase); - _createClass(Square, [{ + _createClass(Database, [{ key: 'resize', - value: function resize() { - this._resizeShape(); + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + this.radius = 0.5 * this.width; + } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'square', 2, x, y, selected, hover); + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); + + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); + + // disable shadows for other elements. + this.disableShadow(ctx); + + ctx.stroke(); + + this.updateBoundingBox(x, y, ctx); + + this.labelModule.draw(ctx, x, y, selected); + } + }, { + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y, ctx) { + this.resize(ctx); + + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; + + this.boundingBox.left = this.left; + this.boundingBox.top = this.top; + this.boundingBox.bottom = this.top + this.height; + this.boundingBox.right = this.left + this.width; } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { - this.resize(); - return this._distanceToBorder(angle); + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); } }]); - return Square; - })(_utilShapeBase2['default']); + return Database; + })(_utilNodeBase2['default']); - exports['default'] = Square; + exports['default'] = Database; module.exports = exports['default']; /***/ }, -/* 78 */ +/* 74 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30258,20 +31161,20 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(72); + var _utilShapeBase = __webpack_require__(75); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - var Star = (function (_ShapeBase) { - function Star(options, body, labelModule) { - _classCallCheck(this, Star); + var Diamond = (function (_ShapeBase) { + function Diamond(options, body, labelModule) { + _classCallCheck(this, Diamond); - _get(Object.getPrototypeOf(Star.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Diamond.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Star, _ShapeBase); + _inherits(Diamond, _ShapeBase); - _createClass(Star, [{ + _createClass(Diamond, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); @@ -30279,7 +31182,7 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'star', 4, x, y, selected, hover); + this._drawShape(ctx, 'diamond', 4, x, y, selected, hover); } }, { key: 'distanceToBorder', @@ -30288,14 +31191,14 @@ return /******/ (function(modules) { // webpackBootstrap } }]); - return Star; + return Diamond; })(_utilShapeBase2['default']); - exports['default'] = Star; + exports['default'] = Diamond; module.exports = exports['default']; /***/ }, -/* 79 */ +/* 75 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30314,75 +31217,87 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilNodeBase = __webpack_require__(66); + var _utilNodeBase = __webpack_require__(69); var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var Text = (function (_NodeBase) { - function Text(options, body, labelModule) { - _classCallCheck(this, Text); + var ShapeBase = (function (_NodeBase) { + function ShapeBase(options, body, labelModule) { + _classCallCheck(this, ShapeBase); - _get(Object.getPrototypeOf(Text.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(ShapeBase.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Text, _NodeBase); + _inherits(ShapeBase, _NodeBase); - _createClass(Text, [{ - key: 'resize', - value: function resize(ctx, selected) { + _createClass(ShapeBase, [{ + key: '_resizeShape', + value: function _resizeShape() { if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + var size = 2 * this.options.size; + this.width = size; + this.height = size; this.radius = 0.5 * this.width; } } }, { - key: 'draw', - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected || hover); + key: '_drawShape', + value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { + this._resizeShape(); + this.left = x - this.width / 2; this.top = y - this.height / 2; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx[shape](x, y, this.options.size); + // draw shadow if enabled this.enableShadow(ctx); - this.labelModule.draw(ctx, x, y, selected || hover); + ctx.fill(); // disable shadows for other elements. this.disableShadow(ctx); + ctx.stroke(); + + if (this.options.label !== undefined) { + var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. + this.labelModule.draw(ctx, x, yLabel, selected, 'hanging'); + } + this.updateBoundingBox(x, y); } }, { key: 'updateBoundingBox', value: function updateBoundingBox(x, y) { - this.resize(); - - this.left = x - this.width / 2; - this.top = y - this.height / 2; + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); + if (this.options.label !== undefined && this.labelModule.size.width > 0) { + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + 3); + } } }]); - return Text; + return ShapeBase; })(_utilNodeBase2['default']); - exports['default'] = Text; + exports['default'] = ShapeBase; module.exports = exports['default']; /***/ }, -/* 80 */ +/* 76 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30401,20 +31316,20 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(72); + var _utilShapeBase = __webpack_require__(75); var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - var Triangle = (function (_ShapeBase) { - function Triangle(options, body, labelModule) { - _classCallCheck(this, Triangle); + var Dot = (function (_ShapeBase) { + function Dot(options, body, labelModule) { + _classCallCheck(this, Dot); - _get(Object.getPrototypeOf(Triangle.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Dot.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(Triangle, _ShapeBase); + _inherits(Dot, _ShapeBase); - _createClass(Triangle, [{ + _createClass(Dot, [{ key: 'resize', value: function resize(ctx) { this._resizeShape(); @@ -30422,23 +31337,23 @@ return /******/ (function(modules) { // webpackBootstrap }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'triangle', 3, x, y, selected, hover); + this._drawShape(ctx, 'circle', 2, x, y, selected, hover); } }, { key: 'distanceToBorder', value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); + return this.options.size + this.options.borderWidth; } }]); - return Triangle; + return Dot; })(_utilShapeBase2['default']); - exports['default'] = Triangle; + exports['default'] = Dot; module.exports = exports['default']; /***/ }, -/* 81 */ +/* 77 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30457,479 +31372,213 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilShapeBase = __webpack_require__(72); + var _utilNodeBase = __webpack_require__(69); - var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var TriangleDown = (function (_ShapeBase) { - function TriangleDown(options, body, labelModule) { - _classCallCheck(this, TriangleDown); + var Ellipse = (function (_NodeBase) { + function Ellipse(options, body, labelModule) { + _classCallCheck(this, Ellipse); - _get(Object.getPrototypeOf(TriangleDown.prototype), 'constructor', this).call(this, options, body, labelModule); + _get(Object.getPrototypeOf(Ellipse.prototype), 'constructor', this).call(this, options, body, labelModule); } - _inherits(TriangleDown, _ShapeBase); + _inherits(Ellipse, _NodeBase); - _createClass(TriangleDown, [{ + _createClass(Ellipse, [{ key: 'resize', - value: function resize(ctx) { - this._resizeShape(); + value: function resize(ctx, selected) { + if (this.width === undefined) { + var textSize = this.labelModule.getTextSize(ctx, selected); + + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; + } + this.radius = 0.5 * this.width; + } } }, { key: 'draw', value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, 'triangleDown', 3, x, y, selected, hover); - } - }, { - key: 'distanceToBorder', - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); - } - }]); - - return TriangleDown; - })(_utilShapeBase2['default']); - - exports['default'] = TriangleDown; - module.exports = exports['default']; - -/***/ }, -/* 82 */ -/***/ function(module, exports, __webpack_require__) { - - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - - var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - - function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - - var _componentsEdge = __webpack_require__(83); - - var _componentsEdge2 = _interopRequireDefault(_componentsEdge); - - var _componentsSharedLabel = __webpack_require__(64); - - var _componentsSharedLabel2 = _interopRequireDefault(_componentsSharedLabel); - - var util = __webpack_require__(9); - var DataSet = __webpack_require__(15); - var DataView = __webpack_require__(17); - - var EdgesHandler = (function () { - function EdgesHandler(body, images, groups) { - var _this = this; - - _classCallCheck(this, EdgesHandler); - - this.body = body; - this.images = images; - this.groups = groups; - - // create the edge API in the body container - this.body.functions.createEdge = this.create.bind(this); - - this.edgesListeners = { - add: function add(event, params) { - _this.add(params.items); - }, - update: function update(event, params) { - _this.update(params.items); - }, - remove: function remove(event, params) { - _this.remove(params.items); - } - }; + this.resize(ctx, selected); + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; - this.options = {}; - this.defaultOptions = { - arrows: { - to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: { enabled: false, scaleFactor: 1 }, - from: { enabled: false, scaleFactor: 1 } - }, - color: { - color: '#848484', - highlight: '#848484', - hover: '#848484', - inherit: 'from', - opacity: 1 - }, - dashes: false, - font: { - color: '#343434', - size: 14, // px - face: 'arial', - background: 'none', - strokeWidth: 2, // px - strokeColor: '#ffffff', - align: 'horizontal' - }, - hidden: false, - hoverWidth: 1.5, - label: undefined, - length: undefined, - physics: true, - scaling: { - min: 1, - max: 15, - label: { - enabled: true, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 5 - }, - customScalingFunction: function customScalingFunction(min, max, total, value) { - if (max === min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - selectionWidth: 1.5, - selfReferenceSize: 20, - shadow: { - enabled: false, - size: 10, - x: 5, - y: 5 - }, - smooth: { - enabled: true, - type: 'dynamic', - roundness: 0.5 - }, - title: undefined, - width: 1, - value: undefined - }; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - util.extend(this.options, this.defaultOptions); + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - this.bindEventListeners(); - } + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - _createClass(EdgesHandler, [{ - key: 'bindEventListeners', - value: function bindEventListeners() { - var _this2 = this; + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.ellipse(this.left, this.top, this.width, this.height); - // this allows external modules to force all dynamic curves to turn static. - this.body.emitter.on('_forceDisableDynamicCurves', function (type) { - if (type === 'dynamic') { - type = 'continuous'; - } - var emitChange = false; - for (var edgeId in _this2.body.edges) { - if (_this2.body.edges.hasOwnProperty(edgeId)) { - var edge = _this2.body.edges[edgeId]; - var edgeData = _this2.body.data.edges._data[edgeId]; + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fill(); - // only forcilby remove the smooth curve if the data has been set of the edge has the smooth curves defined. - // this is because a change in the global would not affect these curves. - if (edgeData !== undefined) { - var edgeOptions = edgeData.smooth; - if (edgeOptions !== undefined) { - if (edgeOptions.enabled === true && edgeOptions.type === 'dynamic') { - if (type === undefined) { - edge.setOptions({ smooth: false }); - } else { - edge.setOptions({ smooth: { type: type } }); - } - emitChange = true; - } - } - } - } - } - if (emitChange === true) { - _this2.body.emitter.emit('_dataChanged'); - } - }); + // disable shadows for other elements. + this.disableShadow(ctx); - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on('_dataUpdated', function () { - _this2.reconnectEdges(); - _this2.markAllEdgesAsDirty(); - }); + ctx.stroke(); - // refresh the edges. Used when reverting from hierarchical layout - this.body.emitter.on('refreshEdges', this.refresh.bind(this)); - this.body.emitter.on('refresh', this.refresh.bind(this)); - this.body.emitter.on('destroy', function () { - delete _this2.body.functions.createEdge; - delete _this2.edgesListeners.add; - delete _this2.edgesListeners.update; - delete _this2.edgesListeners.remove; - delete _this2.edgesListeners; - }); + this.updateBoundingBox(x, y); + this.labelModule.draw(ctx, x, y, selected); } }, { - key: 'setOptions', - value: function setOptions(options) { - if (options !== undefined) { - // use the parser from the Edge class to fill in all shorthand notations - _componentsEdge2['default'].parseOptions(this.options, options); - - // hanlde multiple input cases for color - if (options.color !== undefined) { - this.markAllEdgesAsDirty(); - } - - // update smooth settings in all edges - var dataChanged = false; - if (options.smooth !== undefined) { - for (var edgeId in this.body.edges) { - if (this.body.edges.hasOwnProperty(edgeId)) { - dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged; - } - } - } + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y, ctx) { + this.resize(ctx, false); // just in case - // update fonts in all edges - if (options.font !== undefined) { - // use the parser from the Label class to fill in all shorthand notations - _componentsSharedLabel2['default'].parseOptions(this.options.font, options); - for (var edgeId in this.body.edges) { - if (this.body.edges.hasOwnProperty(edgeId)) { - this.body.edges[edgeId].updateLabelModule(); - } - } - } + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; - // update the state of the variables if needed - if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) { - this.body.emitter.emit('_dataChanged'); - } - } + this.boundingBox.left = this.left; + this.boundingBox.top = this.top; + this.boundingBox.bottom = this.top + this.height; + this.boundingBox.right = this.left + this.width; } }, { - key: 'setData', - - /** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private - * @private - */ - value: function setData(edges) { - var _this3 = this; + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width * 0.5; + var b = this.height * 0.5; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + } + }]); - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + return Ellipse; + })(_utilNodeBase2['default']); - var oldEdgesData = this.body.data.edges; + exports['default'] = Ellipse; + module.exports = exports['default']; - if (edges instanceof DataSet || edges instanceof DataView) { - this.body.data.edges = edges; - } else if (Array.isArray(edges)) { - this.body.data.edges = new DataSet(); - this.body.data.edges.add(edges); - } else if (!edges) { - this.body.data.edges = new DataSet(); - } else { - throw new TypeError('Array or DataSet expected'); - } +/***/ }, +/* 78 */ +/***/ function(module, exports, __webpack_require__) { - // TODO: is this null or undefined or false? - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.off(event, callback); - }); - } + 'use strict'; - // remove drawn edges - this.body.edges = {}; + Object.defineProperty(exports, '__esModule', { + value: true + }); - // TODO: is this null or undefined or false? - if (this.body.data.edges) { - // subscribe to new dataset - util.forEach(this.edgesListeners, function (callback, event) { - _this3.body.data.edges.on(event, callback); - }); + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - // draw all new nodes - var ids = this.body.data.edges.getIds(); - this.add(ids, true); - } + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'add', + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - /** - * Add edges - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var edges = this.body.edges; - var edgesData = this.body.data.edges; + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; + var _utilNodeBase = __webpack_require__(69); - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - var data = edgesData.get(id, { 'showInternalIds': true }); - edges[id] = this.create(data); - } + var Icon = (function (_NodeBase) { + function Icon(options, body, labelModule) { + _classCallCheck(this, Icon); - if (doNotEmit === false) { - this.body.emitter.emit('_dataChanged'); - } - } - }, { - key: 'update', + _get(Object.getPrototypeOf(Icon.prototype), 'constructor', this).call(this, options, body, labelModule); + } - /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids) { - var edges = this.body.edges; - var edgesData = this.body.data.edges; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var data = edgesData.get(id); - var edge = edges[id]; - if (edge === null) { - // update edge - edge.disconnect(); - dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. - edge.connect(); - } else { - // create edge - this.body.edges[id] = this.create(data); - dataChanged = true; - } - } + _inherits(Icon, _NodeBase); - if (dataChanged === true) { - this.body.emitter.emit('_dataChanged'); - } else { - this.body.emitter.emit('_dataUpdated'); + _createClass(Icon, [{ + key: 'resize', + value: function resize(ctx) { + if (this.width === undefined) { + var margin = 5; + var iconSize = { + width: Number(this.options.icon.size), + height: Number(this.options.icon.size) + }; + this.width = iconSize.width + 2 * margin; + this.height = iconSize.height + 2 * margin; + this.radius = 0.5 * this.width; } } }, { - key: 'remove', + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.options.icon.size = this.options.icon.size || 50; - /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var edges = this.body.edges; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge !== undefined) { - edge.edgeType.cleanup(); - edge.disconnect(); - delete edges[id]; - } - } + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; + this._icon(ctx, x, y, selected); - this.body.emitter.emit('_dataChanged'); - } - }, { - key: 'refresh', - value: function refresh() { - var edges = this.body.edges; - for (var edgeId in edges) { - var edge = undefined; - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - } - var data = this.body.data.edges._data[edgeId]; - if (edge !== undefined && data !== undefined) { - edge.setOptions(data); - } + if (this.options.label !== undefined) { + var iconTextSpacing = 5; + this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); } + + this.updateBoundingBox(x, y); } }, { - key: 'create', - value: function create(properties) { - return new _componentsEdge2['default'](properties, this.body, this.options); - } - }, { - key: 'markAllEdgesAsDirty', - value: function markAllEdgesAsDirty() { - for (var edgeId in this.body.edges) { - this.body.edges[edgeId].edgeType.colorDirty = true; + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y) { + this.boundingBox.top = y - this.options.icon.size * 0.5; + this.boundingBox.left = x - this.options.icon.size * 0.5; + this.boundingBox.right = x + this.options.icon.size * 0.5; + this.boundingBox.bottom = y + this.options.icon.size * 0.5; + + if (this.options.label !== undefined && this.labelModule.size.width > 0) { + var iconTextSpacing = 5; + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + iconTextSpacing); } } }, { - key: 'reconnectEdges', + key: '_icon', + value: function _icon(ctx, x, y, selected) { + var iconSize = Number(this.options.icon.size); - /** - * Reconnect all edges - * @private - */ - value: function reconnectEdges() { - var id; - var nodes = this.body.nodes; - var edges = this.body.edges; + if (this.options.icon.code !== undefined) { + ctx.font = (selected ? 'bold ' : '') + iconSize + 'px ' + this.options.icon.face; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } + // draw icon + ctx.fillStyle = this.options.icon.color || 'black'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } + // draw shadow if enabled + this.enableShadow(ctx); + ctx.fillText(this.options.icon.code, x, y); + + // disable shadows for other elements. + this.disableShadow(ctx); + } else { + console.error('When using the icon shape, you need to define the code in the icon options object. This can be done per node or globally.'); } } }, { - key: 'getConnectedNodes', - value: function getConnectedNodes(edgeId) { - var nodeList = []; - if (this.body.edges[edgeId] !== undefined) { - var edge = this.body.edges[edgeId]; - if (edge.fromId) { - nodeList.push(edge.fromId); - } - if (edge.toId) { - nodeList.push(edge.toId); - } - } - return nodeList; + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); } }]); - return EdgesHandler; - })(); + return Icon; + })(_utilNodeBase2['default']); - exports['default'] = EdgesHandler; + exports['default'] = Icon; module.exports = exports['default']; /***/ }, -/* 83 */ +/* 79 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -30940,560 +31589,396 @@ return /******/ (function(modules) { // webpackBootstrap var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _sharedLabel = __webpack_require__(64); + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _sharedLabel2 = _interopRequireDefault(_sharedLabel); + var _utilCircleImageBase = __webpack_require__(71); - var _edgesBezierEdgeDynamic = __webpack_require__(84); + var _utilCircleImageBase2 = _interopRequireDefault(_utilCircleImageBase); - var _edgesBezierEdgeDynamic2 = _interopRequireDefault(_edgesBezierEdgeDynamic); + var Image = (function (_CircleImageBase) { + function Image(options, body, labelModule, imageObj) { + _classCallCheck(this, Image); - var _edgesBezierEdgeStatic = __webpack_require__(87); + _get(Object.getPrototypeOf(Image.prototype), 'constructor', this).call(this, options, body, labelModule); + this.imageObj = imageObj; + } - var _edgesBezierEdgeStatic2 = _interopRequireDefault(_edgesBezierEdgeStatic); + _inherits(Image, _CircleImageBase); - var _edgesStraightEdge = __webpack_require__(88); + _createClass(Image, [{ + key: 'resize', + value: function resize() { + this._resizeImage(); + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - var _edgesStraightEdge2 = _interopRequireDefault(_edgesStraightEdge); + this._drawImageAtPosition(ctx); - var util = __webpack_require__(9); + this._drawImageLabel(ctx, x, y, selected || hover); - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with options. Must contain - * At least options from and to. - * Available options: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color - */ + this.updateBoundingBox(x, y); + } + }, { + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y) { + this.resize(); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - var Edge = (function () { - function Edge(options, body, globalOptions) { - _classCallCheck(this, Edge); + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - if (body === undefined) { - throw 'No body provided'; + if (this.options.label !== undefined && this.labelModule.size.width > 0) { + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset); + } } - this.options = util.bridgeObject(globalOptions); - this.body = body; - - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.selected = false; - this.hover = false; - this.labelDirty = true; - this.colorDirty = true; + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + } + }]); - this.baseWidth = this.options.width; - this.baseFontSize = this.options.font.size; + return Image; + })(_utilCircleImageBase2['default']); - this.from = undefined; // a node - this.to = undefined; // a node + exports['default'] = Image; + module.exports = exports['default']; - this.edgeType = undefined; +/***/ }, +/* 80 */ +/***/ function(module, exports, __webpack_require__) { - this.connected = false; + 'use strict'; - this.labelModule = new _sharedLabel2['default'](this.body, this.options); + Object.defineProperty(exports, '__esModule', { + value: true + }); - this.setOptions(options); - } + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - _createClass(Edge, [{ - key: 'setOptions', + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - /** - * Set or overwrite options for the edge - * @param {Object} options an object with options - * @param doNotEmit - */ - value: function setOptions(options) { - if (!options) { - return; - } - this.colorDirty = true; + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - Edge.parseOptions(this.options, options, true); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - if (options.id !== undefined) { - this.id = options.id; - } - if (options.from !== undefined) { - this.fromId = options.from; - } - if (options.to !== undefined) { - this.toId = options.to; - } - if (options.title !== undefined) { - this.title = options.title; - } - if (options.value !== undefined) { - options.value = parseFloat(options.value); - } + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - // update label Module - this.updateLabelModule(); + var _utilShapeBase = __webpack_require__(75); - var dataChanged = this.updateEdgeType(); + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); - // if anything has been updates, reset the selection width and the hover width - this._setInteractionWidths(); + var Square = (function (_ShapeBase) { + function Square(options, body, labelModule) { + _classCallCheck(this, Square); - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); + _get(Object.getPrototypeOf(Square.prototype), 'constructor', this).call(this, options, body, labelModule); + } - if (options.hidden !== undefined || options.physics !== undefined) { - dataChanged = true; - } + _inherits(Square, _ShapeBase); - return dataChanged; + _createClass(Square, [{ + key: 'resize', + value: function resize() { + this._resizeShape(); } }, { - key: 'updateLabelModule', - - /** - * update the options in the label module - */ - value: function updateLabelModule() { - this.labelModule.setOptions(this.options, true); - if (this.labelModule.baseSize !== undefined) { - this.baseFontSize = this.labelModule.baseSize; - } + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, 'square', 2, x, y, selected, hover); } }, { - key: 'updateEdgeType', + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(); + return this._distanceToBorder(angle); + } + }]); - /** - * update the edge type, set the options - * @returns {boolean} - */ - value: function updateEdgeType() { - var dataChanged = false; - var changeInType = true; - if (this.edgeType !== undefined) { - if (this.edgeType instanceof _edgesBezierEdgeDynamic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type === 'dynamic') { - changeInType = false; - } - if (this.edgeType instanceof _edgesBezierEdgeStatic2['default'] && this.options.smooth.enabled === true && this.options.smooth.type !== 'dynamic') { - changeInType = false; - } - if (this.edgeType instanceof _edgesStraightEdge2['default'] && this.options.smooth.enabled === false) { - changeInType = false; - } + return Square; + })(_utilShapeBase2['default']); - if (changeInType === true) { - dataChanged = this.edgeType.cleanup(); - } - } + exports['default'] = Square; + module.exports = exports['default']; - if (changeInType === true) { - if (this.options.smooth.enabled === true) { - if (this.options.smooth.type === 'dynamic') { - dataChanged = true; - this.edgeType = new _edgesBezierEdgeDynamic2['default'](this.options, this.body, this.labelModule); - } else { - this.edgeType = new _edgesBezierEdgeStatic2['default'](this.options, this.body, this.labelModule); - } - } else { - this.edgeType = new _edgesStraightEdge2['default'](this.options, this.body, this.labelModule); - } - } else { - // if nothing changes, we just set the options. - this.edgeType.setOptions(this.options); - } +/***/ }, +/* 81 */ +/***/ function(module, exports, __webpack_require__) { - return dataChanged; + 'use strict'; + + Object.defineProperty(exports, '__esModule', { + value: true + }); + + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilShapeBase = __webpack_require__(75); + + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + + var Star = (function (_ShapeBase) { + function Star(options, body, labelModule) { + _classCallCheck(this, Star); + + _get(Object.getPrototypeOf(Star.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(Star, _ShapeBase); + + _createClass(Star, [{ + key: 'resize', + value: function resize(ctx) { + this._resizeShape(); } }, { - key: 'togglePhysics', + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, 'star', 4, x, y, selected, hover); + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); + } + }]); + + return Star; + })(_utilShapeBase2['default']); + + exports['default'] = Star; + module.exports = exports['default']; + +/***/ }, +/* 82 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - this.options.physics = status; - this.edgeType.togglePhysics(status); - } - }, { - key: 'connect', + 'use strict'; - /** - * Connect an edge to its nodes - */ - value: function connect() { - this.disconnect(); + Object.defineProperty(exports, '__esModule', { + value: true + }); - this.from = this.body.nodes[this.fromId] || undefined; - this.to = this.body.nodes[this.toId] || undefined; - this.connected = this.from !== undefined && this.to !== undefined; + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - if (this.connected === true) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } else { - if (this.from) { - this.from.detachEdge(this); - } - if (this.to) { - this.to.detachEdge(this); - } - } + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - this.edgeType.connect(); - } - }, { - key: 'disconnect', + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - /** - * Disconnect an edge from its nodes - */ - value: function disconnect() { - if (this.from) { - this.from.detachEdge(this); - this.from = undefined; - } - if (this.to) { - this.to.detachEdge(this); - this.to = undefined; - } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - this.connected = false; - } - }, { - key: 'getTitle', + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. - */ - value: function getTitle() { - return this.title; - } - }, { - key: 'isSelected', + var _utilNodeBase = __webpack_require__(69); - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - } - }, { - key: 'getValue', + var _utilNodeBase2 = _interopRequireDefault(_utilNodeBase); - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.options.value; - } - }, { - key: 'setValueRange', + var Text = (function (_NodeBase) { + function Text(options, body, labelModule) { + _classCallCheck(this, Text); - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - * @param total - */ - value: function setValueRange(min, max, total) { - if (this.options.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value); - var widthDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled === true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.width = this.options.scaling.min + scale * widthDiff; - } else { - this.options.width = this.baseWidth; - this.options.font.size = this.baseFontSize; - } + _get(Object.getPrototypeOf(Text.prototype), 'constructor', this).call(this, options, body, labelModule); + } - this._setInteractionWidths(); - } - }, { - key: '_setInteractionWidths', - value: function _setInteractionWidths() { - if (typeof this.options.hoverWidth === 'function') { - this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width); - } else { - this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width; - } + _inherits(Text, _NodeBase); - if (typeof this.options.selectionWidth === 'function') { - this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width); - } else { - this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width; + _createClass(Text, [{ + key: 'resize', + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + this.radius = 0.5 * this.width; } } }, { key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected || hover); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - var via = this.edgeType.drawLine(ctx, this.selected, this.hover); - this.drawArrows(ctx, via); - this.drawLabel(ctx, via); - } - }, { - key: 'drawArrows', - value: function drawArrows(ctx, viaNode) { - if (this.options.arrows.from.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'from', viaNode, this.selected, this.hover); - } - if (this.options.arrows.middle.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'middle', viaNode, this.selected, this.hover); - } - if (this.options.arrows.to.enabled === true) { - this.edgeType.drawArrowHead(ctx, 'to', viaNode, this.selected, this.hover); - } + // draw shadow if enabled + this.enableShadow(ctx); + this.labelModule.draw(ctx, x, y, selected || hover); + + // disable shadows for other elements. + this.disableShadow(ctx); + + this.updateBoundingBox(x, y); } }, { - key: 'drawLabel', - value: function drawLabel(ctx, viaNode) { - if (this.options.label !== undefined) { - // set style - var node1 = this.from; - var node2 = this.to; - var selected = this.from.selected || this.to.selected || this.selected; - if (node1.id != node2.id) { - this.labelModule.pointToSelf = false; - var point = this.edgeType.getPoint(0.5, viaNode); - ctx.save(); + key: 'updateBoundingBox', + value: function updateBoundingBox(x, y) { + this.resize(); - // if the label has to be rotated: - if (this.options.font.align !== 'horizontal') { - this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); - ctx.translate(point.x, this.labelModule.size.yLine); - this._rotateForLabelAlignment(ctx); - } + this.left = x - this.width / 2; + this.top = y - this.height / 2; - // draw the label - this.labelModule.draw(ctx, point.x, point.y, selected); - ctx.restore(); - } else { - // Ignore the orientations. - this.labelModule.pointToSelf = true; - var x, y; - var radius = this.options.selfReferenceSize; - if (node1.shape.width > node1.shape.height) { - x = node1.x + node1.shape.width * 0.5; - y = node1.y - radius; - } else { - x = node1.x + radius; - y = node1.y - node1.shape.height * 0.5; - } - point = this._pointOnCircle(x, y, radius, 0.125); - this.labelModule.draw(ctx, point.x, point.y, selected); - } - } + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; } }, { - key: 'isOverlappingWith', + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); + } + }]); - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top - * @return {boolean} True if location is located on the edge - */ - value: function isOverlappingWith(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; + return Text; + })(_utilNodeBase2['default']); - var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + exports['default'] = Text; + module.exports = exports['default']; - return dist < distMax; - } else { - return false; - } - } - }, { - key: '_rotateForLabelAlignment', +/***/ }, +/* 83 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _rotateForLabelAlignment(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); + 'use strict'; - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; - } + Object.defineProperty(exports, '__esModule', { + value: true + }); - ctx.rotate(angleInDegrees); - } - }, { - key: '_pointOnCircle', + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - /** - * Get a point on a circle - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private - */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilShapeBase = __webpack_require__(75); + + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + + var Triangle = (function (_ShapeBase) { + function Triangle(options, body, labelModule) { + _classCallCheck(this, Triangle); + + _get(Object.getPrototypeOf(Triangle.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(Triangle, _ShapeBase); + + _createClass(Triangle, [{ + key: 'resize', + value: function resize(ctx) { + this._resizeShape(); } }, { - key: 'select', - value: function select() { - this.selected = true; + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, 'triangle', 3, x, y, selected, hover); } }, { - key: 'unselect', - value: function unselect() { - this.selected = false; + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); } - }], [{ - key: 'parseOptions', - value: function parseOptions(parentOptions, newOptions) { - var allowDeletion = arguments[2] === undefined ? false : arguments[2]; + }]); - var fields = ['id', 'from', 'hidden', 'hoverWidth', 'label', 'length', 'line', 'opacity', 'physics', 'selectionWidth', 'selfReferenceSize', 'to', 'title', 'value', 'width']; + return Triangle; + })(_utilShapeBase2['default']); - // only deep extend the items in the field array. These do not have shorthand. - util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion); + exports['default'] = Triangle; + module.exports = exports['default']; - util.mergeOptions(parentOptions, newOptions, 'smooth'); - util.mergeOptions(parentOptions, newOptions, 'shadow'); +/***/ }, +/* 84 */ +/***/ function(module, exports, __webpack_require__) { - if (newOptions.dashes !== undefined && newOptions.dashes !== null) { - parentOptions.dashes = newOptions.dashes; - } else if (allowDeletion === true && newOptions.dashes === null) { - parentOptions.dashes = undefined; - delete parentOptions.dashes; - } + 'use strict'; - // set the scaling newOptions - if (newOptions.scaling !== undefined && newOptions.scaling !== null) { - if (newOptions.scaling.min !== undefined) { - parentOptions.scaling.min = newOptions.scaling.min; - } - if (newOptions.scaling.max !== undefined) { - parentOptions.scaling.max = newOptions.scaling.max; - } - util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label'); - } else if (allowDeletion === true && newOptions.scaling === null) { - parentOptions.scaling = undefined; - delete parentOptions.scaling; - } + Object.defineProperty(exports, '__esModule', { + value: true + }); - // hanlde multiple input cases for arrows - if (newOptions.arrows !== undefined && newOptions.arrows !== null) { - if (typeof newOptions.arrows === 'string') { - var arrows = newOptions.arrows.toLowerCase(); - if (arrows.indexOf('to') != -1) { - parentOptions.arrows.to.enabled = true; - } - if (arrows.indexOf('middle') != -1) { - parentOptions.arrows.middle.enabled = true; - } - if (arrows.indexOf('from') != -1) { - parentOptions.arrows.from.enabled = true; - } - } else if (typeof newOptions.arrows === 'object') { - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to'); - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle'); - util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from'); - } else { - throw new Error('The arrow newOptions can only be an object or a string. Refer to the documentation. You used:' + JSON.stringify(newOptions.arrows)); - } - } else if (allowDeletion === true && newOptions.arrows === null) { - parentOptions.arrows = undefined; - delete parentOptions.arrows; - } + var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); - // hanlde multiple input cases for color - if (newOptions.color !== undefined && newOptions.color !== null) { - if (util.isString(newOptions.color)) { - parentOptions.color.color = newOptions.color; - parentOptions.color.highlight = newOptions.color; - parentOptions.color.hover = newOptions.color; - parentOptions.color.inherit = false; - } else { - var colorsDefined = false; - if (newOptions.color.color !== undefined) { - parentOptions.color.color = newOptions.color.color;colorsDefined = true; - } - if (newOptions.color.highlight !== undefined) { - parentOptions.color.highlight = newOptions.color.highlight;colorsDefined = true; - } - if (newOptions.color.hover !== undefined) { - parentOptions.color.hover = newOptions.color.hover;colorsDefined = true; - } - if (newOptions.color.inherit !== undefined) { - parentOptions.color.inherit = newOptions.color.inherit; - } - if (newOptions.color.opacity !== undefined) { - parentOptions.color.opacity = Math.min(1, Math.max(0, newOptions.color.opacity)); - } + var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; - if (newOptions.color.inherit === undefined && colorsDefined === true) { - parentOptions.color.inherit = false; - } - } - } else if (allowDeletion === true && newOptions.color === null) { - parentOptions.color = undefined; - delete parentOptions.color; - } + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } - // handle the font settings - if (newOptions.font !== undefined) { - _sharedLabel2['default'].parseOptions(parentOptions.font, newOptions); - } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + + function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + + var _utilShapeBase = __webpack_require__(75); + + var _utilShapeBase2 = _interopRequireDefault(_utilShapeBase); + + var TriangleDown = (function (_ShapeBase) { + function TriangleDown(options, body, labelModule) { + _classCallCheck(this, TriangleDown); + + _get(Object.getPrototypeOf(TriangleDown.prototype), 'constructor', this).call(this, options, body, labelModule); + } + + _inherits(TriangleDown, _ShapeBase); + + _createClass(TriangleDown, [{ + key: 'resize', + value: function resize(ctx) { + this._resizeShape(); + } + }, { + key: 'draw', + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, 'triangleDown', 3, x, y, selected, hover); + } + }, { + key: 'distanceToBorder', + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); } }]); - return Edge; - })(); + return TriangleDown; + })(_utilShapeBase2['default']); - exports['default'] = Edge; + exports['default'] = TriangleDown; module.exports = exports['default']; /***/ }, -/* 84 */ +/* 85 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31512,7 +31997,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilBezierEdgeBase = __webpack_require__(85); + var _utilBezierEdgeBase = __webpack_require__(86); var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); @@ -31658,7 +32143,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 85 */ +/* 86 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31677,7 +32162,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _EdgeBase2 = __webpack_require__(86); + var _EdgeBase2 = __webpack_require__(87); var _EdgeBase3 = _interopRequireDefault(_EdgeBase2); @@ -31805,7 +32290,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 86 */ +/* 87 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -31820,7 +32305,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var util = __webpack_require__(9); + var util = __webpack_require__(14); var EdgeBase = (function () { function EdgeBase(options, body, labelModule) { @@ -32402,7 +32887,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 87 */ +/* 88 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -32421,7 +32906,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilBezierEdgeBase = __webpack_require__(85); + var _utilBezierEdgeBase = __webpack_require__(86); var _utilBezierEdgeBase2 = _interopRequireDefault(_utilBezierEdgeBase); @@ -32661,7 +33146,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 88 */ +/* 89 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -32680,7 +33165,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _utilEdgeBase = __webpack_require__(86); + var _utilEdgeBase = __webpack_require__(87); var _utilEdgeBase2 = _interopRequireDefault(_utilEdgeBase); @@ -32766,7 +33251,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 89 */ +/* 90 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -32781,39 +33266,39 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsPhysicsBarnesHutSolver = __webpack_require__(90); + var _componentsPhysicsBarnesHutSolver = __webpack_require__(91); var _componentsPhysicsBarnesHutSolver2 = _interopRequireDefault(_componentsPhysicsBarnesHutSolver); - var _componentsPhysicsRepulsionSolver = __webpack_require__(91); + var _componentsPhysicsRepulsionSolver = __webpack_require__(92); var _componentsPhysicsRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsRepulsionSolver); - var _componentsPhysicsHierarchicalRepulsionSolver = __webpack_require__(92); + var _componentsPhysicsHierarchicalRepulsionSolver = __webpack_require__(93); var _componentsPhysicsHierarchicalRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalRepulsionSolver); - var _componentsPhysicsSpringSolver = __webpack_require__(93); + var _componentsPhysicsSpringSolver = __webpack_require__(94); var _componentsPhysicsSpringSolver2 = _interopRequireDefault(_componentsPhysicsSpringSolver); - var _componentsPhysicsHierarchicalSpringSolver = __webpack_require__(94); + var _componentsPhysicsHierarchicalSpringSolver = __webpack_require__(95); var _componentsPhysicsHierarchicalSpringSolver2 = _interopRequireDefault(_componentsPhysicsHierarchicalSpringSolver); - var _componentsPhysicsCentralGravitySolver = __webpack_require__(95); + var _componentsPhysicsCentralGravitySolver = __webpack_require__(96); var _componentsPhysicsCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsCentralGravitySolver); - var _componentsPhysicsFA2BasedRepulsionSolver = __webpack_require__(96); + var _componentsPhysicsFA2BasedRepulsionSolver = __webpack_require__(97); var _componentsPhysicsFA2BasedRepulsionSolver2 = _interopRequireDefault(_componentsPhysicsFA2BasedRepulsionSolver); - var _componentsPhysicsFA2BasedCentralGravitySolver = __webpack_require__(97); + var _componentsPhysicsFA2BasedCentralGravitySolver = __webpack_require__(98); var _componentsPhysicsFA2BasedCentralGravitySolver2 = _interopRequireDefault(_componentsPhysicsFA2BasedCentralGravitySolver); - var util = __webpack_require__(9); + var util = __webpack_require__(14); var PhysicsEngine = (function () { function PhysicsEngine(body) { @@ -33406,7 +33891,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 90 */ +/* 91 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -33905,7 +34390,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 91 */ +/* 92 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34000,7 +34485,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 92 */ +/* 93 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34091,7 +34576,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 93 */ +/* 94 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34201,7 +34686,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 94 */ +/* 95 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34330,7 +34815,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 95 */ +/* 96 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34399,7 +34884,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 96 */ +/* 97 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34418,7 +34903,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _BarnesHutSolver2 = __webpack_require__(90); + var _BarnesHutSolver2 = __webpack_require__(91); var _BarnesHutSolver3 = _interopRequireDefault(_BarnesHutSolver2); @@ -34473,7 +34958,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 97 */ +/* 98 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -34492,7 +34977,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _CentralGravitySolver2 = __webpack_require__(95); + var _CentralGravitySolver2 = __webpack_require__(96); var _CentralGravitySolver3 = _interopRequireDefault(_CentralGravitySolver2); @@ -34529,7 +35014,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 98 */ +/* 99 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -34544,11 +35029,11 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNodesCluster = __webpack_require__(99); + var _componentsNodesCluster = __webpack_require__(100); var _componentsNodesCluster2 = _interopRequireDefault(_componentsNodesCluster); - var util = __webpack_require__(9); + var util = __webpack_require__(14); var ClusterEngine = (function () { function ClusterEngine(body) { @@ -35284,7 +35769,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 99 */ +/* 100 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -35301,7 +35786,7 @@ return /******/ (function(modules) { // webpackBootstrap function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } - var _Node2 = __webpack_require__(63); + var _Node2 = __webpack_require__(67); var _Node3 = _interopRequireDefault(_Node2); @@ -35329,7 +35814,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 100 */ +/* 101 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -35346,7 +35831,7 @@ return /******/ (function(modules) { // webpackBootstrap window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; } - var util = __webpack_require__(9); + var util = __webpack_require__(14); var CanvasRenderer = (function () { function CanvasRenderer(body, canvas) { @@ -35717,7 +36202,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 101 */ +/* 102 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -35730,10 +36215,10 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var Hammer = __webpack_require__(5); - var hammerUtil = __webpack_require__(31); + var Hammer = __webpack_require__(10); + var hammerUtil = __webpack_require__(36); - var util = __webpack_require__(9); + var util = __webpack_require__(14); /** * Create the main frame for the Network. @@ -36095,7 +36580,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 102 */ +/* 103 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -36108,7 +36593,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - var util = __webpack_require__(9); + var util = __webpack_require__(14); var View = (function () { function View(body, canvas) { @@ -36500,7 +36985,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports["default"]; /***/ }, -/* 103 */ +/* 104 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -36515,15 +37000,15 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var _componentsNavigationHandler = __webpack_require__(104); + var _componentsNavigationHandler = __webpack_require__(105); var _componentsNavigationHandler2 = _interopRequireDefault(_componentsNavigationHandler); - var _componentsPopup = __webpack_require__(105); + var _componentsPopup = __webpack_require__(106); var _componentsPopup2 = _interopRequireDefault(_componentsPopup); - var util = __webpack_require__(9); + var util = __webpack_require__(14); var InteractionHandler = (function () { function InteractionHandler(body, canvas, selectionHandler) { @@ -37264,7 +37749,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 104 */ +/* 105 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37277,10 +37762,10 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var util = __webpack_require__(9); - var Hammer = __webpack_require__(5); - var hammerUtil = __webpack_require__(31); - var keycharm = __webpack_require__(44); + var util = __webpack_require__(14); + var Hammer = __webpack_require__(10); + var hammerUtil = __webpack_require__(36); + var keycharm = __webpack_require__(49); var NavigationHandler = (function () { function NavigationHandler(body, canvas) { @@ -37590,7 +38075,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 105 */ +/* 106 */ /***/ function(module, exports, __webpack_require__) { /** @@ -37716,7 +38201,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 106 */ +/* 107 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -37729,7 +38214,7 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var util = __webpack_require__(9); + var util = __webpack_require__(14); var LayoutEngine = (function () { function LayoutEngine(body) { @@ -38224,7 +38709,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = exports['default']; /***/ }, -/* 107 */ +/* 108 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; @@ -38237,9 +38722,9 @@ return /******/ (function(modules) { // webpackBootstrap function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } - var util = __webpack_require__(9); - var Hammer = __webpack_require__(5); - var hammerUtil = __webpack_require__(31); + var util = __webpack_require__(14); + var Hammer = __webpack_require__(10); + var hammerUtil = __webpack_require__(36); /** * clears the toolbar div element of children @@ -39437,485 +39922,6 @@ return /******/ (function(modules) { // webpackBootstrap exports['default'] = ManipulationSystem; module.exports = exports['default']; -/***/ }, -/* 108 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * This object contains all possible options. It will check if the types are correct, if required if the option is one - * of the allowed values. - * - * __any__ means that the name of the property does not matter. - * __type__ is a required field for all objects and contains the allowed types of all objects - */ - 'use strict'; - - Object.defineProperty(exports, '__esModule', { - value: true - }); - var string = 'string'; - var boolean = 'boolean'; - var number = 'number'; - var array = 'array'; - var object = 'object'; // should only be in a __type__ property - var dom = 'dom'; - var any = 'any'; - - var allOptions = { - configure: { - enabled: { boolean: boolean }, - filter: { boolean: boolean, string: string, array: array, 'function': 'function' }, - container: { dom: dom }, - showButton: { boolean: boolean }, - __type__: { object: object, boolean: boolean, string: string, array: array, 'function': 'function' } - }, - edges: { - arrows: { - to: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, - middle: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, - from: { enabled: { boolean: boolean }, scaleFactor: { number: number }, __type__: { object: object, boolean: boolean } }, - __type__: { string: ['from', 'to', 'middle'], object: object } - }, - color: { - color: { string: string }, - highlight: { string: string }, - hover: { string: string }, - inherit: { string: ['from', 'to', 'both'], boolean: boolean }, - opacity: { number: number }, - __type__: { object: object, string: string } - }, - dashes: { boolean: boolean, array: array }, - font: { - color: { string: string }, - size: { number: number }, // px - face: { string: string }, - background: { string: string }, - strokeWidth: { number: number }, // px - strokeColor: { string: string }, - align: { string: ['horizontal', 'top', 'middle', 'bottom'] }, - __type__: { object: object, string: string } - }, - hidden: { boolean: boolean }, - hoverWidth: { 'function': 'function', number: number }, - label: { string: string, 'undefined': 'undefined' }, - length: { number: number, 'undefined': 'undefined' }, - physics: { boolean: boolean }, - scaling: { - min: { number: number }, - max: { number: number }, - label: { - enabled: { boolean: boolean }, - min: { number: number }, - max: { number: number }, - maxVisible: { number: number }, - drawThreshold: { number: number }, - __type__: { object: object, boolean: boolean } - }, - customScalingFunction: { 'function': 'function' }, - __type__: { object: object } - }, - selectionWidth: { 'function': 'function', number: number }, - selfReferenceSize: { number: number }, - shadow: { - enabled: { boolean: boolean }, - size: { number: number }, - x: { number: number }, - y: { number: number }, - __type__: { object: object, boolean: boolean } - }, - smooth: { - enabled: { boolean: boolean }, - type: { string: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'] }, - roundness: { number: number }, - __type__: { object: object, boolean: boolean } - }, - title: { string: string, 'undefined': 'undefined' }, - width: { number: number }, - value: { number: number, 'undefined': 'undefined' }, - __type__: { object: object } - }, - groups: { - useDefaultGroups: { boolean: boolean }, - __any__: 'get from nodes, will be overwritten below', - __type__: { object: object } - }, - interaction: { - dragNodes: { boolean: boolean }, - dragView: { boolean: boolean }, - hideEdgesOnDrag: { boolean: boolean }, - hideNodesOnDrag: { boolean: boolean }, - hover: { boolean: boolean }, - keyboard: { - enabled: { boolean: boolean }, - speed: { x: { number: number }, y: { number: number }, zoom: { number: number }, __type__: { object: object } }, - bindToWindow: { boolean: boolean }, - __type__: { object: object, boolean: boolean } - }, - multiselect: { boolean: boolean }, - navigationButtons: { boolean: boolean }, - selectable: { boolean: boolean }, - selectConnectedEdges: { boolean: boolean }, - hoverConnectedEdges: { boolean: boolean }, - tooltipDelay: { number: number }, - zoomView: { boolean: boolean }, - __type__: { object: object } - }, - layout: { - randomSeed: { 'undefined': 'undefined', number: number }, - hierarchical: { - enabled: { boolean: boolean }, - levelSeparation: { number: number }, - direction: { string: ['UD', 'DU', 'LR', 'RL'] }, // UD, DU, LR, RL - sortMethod: { string: ['hubsize', 'directed'] }, // hubsize, directed - __type__: { object: object, boolean: boolean } - }, - __type__: { object: object } - }, - manipulation: { - enabled: { boolean: boolean }, - initiallyActive: { boolean: boolean }, - addNode: { boolean: boolean, 'function': 'function' }, - addEdge: { boolean: boolean, 'function': 'function' }, - editNode: { 'function': 'function' }, - editEdge: { boolean: boolean, 'function': 'function' }, - deleteNode: { boolean: boolean, 'function': 'function' }, - deleteEdge: { boolean: boolean, 'function': 'function' }, - controlNodeStyle: 'get from nodes, will be overwritten below', - __type__: { object: object, boolean: boolean } - }, - nodes: { - borderWidth: { number: number }, - borderWidthSelected: { number: number, 'undefined': 'undefined' }, - brokenImage: { string: string, 'undefined': 'undefined' }, - color: { - border: { string: string }, - background: { string: string }, - highlight: { - border: { string: string }, - background: { string: string }, - __type__: { object: object, string: string } - }, - hover: { - border: { string: string }, - background: { string: string }, - __type__: { object: object, string: string } - }, - __type__: { object: object, string: string } - }, - fixed: { - x: { boolean: boolean }, - y: { boolean: boolean }, - __type__: { object: object, boolean: boolean } - }, - font: { - color: { string: string }, - size: { number: number }, // px - face: { string: string }, - background: { string: string }, - strokeWidth: { number: number }, // px - strokeColor: { string: string }, - __type__: { object: object, string: string } - }, - group: { string: string, number: number, 'undefined': 'undefined' }, - hidden: { boolean: boolean }, - icon: { - face: { string: string }, - code: { string: string }, //'\uf007', - size: { number: number }, //50, - color: { string: string }, - __type__: { object: object } - }, - id: { string: string, number: number }, - image: { string: string, 'undefined': 'undefined' }, // --> URL - label: { string: string, 'undefined': 'undefined' }, - level: { number: number, 'undefined': 'undefined' }, - mass: { number: number }, - physics: { boolean: boolean }, - scaling: { - min: { number: number }, - max: { number: number }, - label: { - enabled: { boolean: boolean }, - min: { number: number }, - max: { number: number }, - maxVisible: { number: number }, - drawThreshold: { number: number }, - __type__: { object: object, boolean: boolean } - }, - customScalingFunction: { 'function': 'function' }, - __type__: { object: object } - }, - shadow: { - enabled: { boolean: boolean }, - size: { number: number }, - x: { number: number }, - y: { number: number }, - __type__: { object: object, boolean: boolean } - }, - shape: { string: ['ellipse', 'circle', 'database', 'box', 'text', 'image', 'circularImage', 'diamond', 'dot', 'star', 'triangle', 'triangleDown', 'square', 'icon'] }, - size: { number: number }, - title: { string: string, 'undefined': 'undefined' }, - value: { number: number, 'undefined': 'undefined' }, - x: { number: number }, - y: { number: number }, - __type__: { object: object } - }, - physics: { - enabled: { boolean: boolean }, - barnesHut: { - gravitationalConstant: { number: number }, - centralGravity: { number: number }, - springLength: { number: number }, - springConstant: { number: number }, - damping: { number: number }, - avoidOverlap: { number: number }, - __type__: { object: object } - }, - forceAtlas2Based: { - gravitationalConstant: { number: number }, - centralGravity: { number: number }, - springLength: { number: number }, - springConstant: { number: number }, - damping: { number: number }, - avoidOverlap: { number: number }, - __type__: { object: object } - }, - repulsion: { - centralGravity: { number: number }, - springLength: { number: number }, - springConstant: { number: number }, - nodeDistance: { number: number }, - damping: { number: number }, - __type__: { object: object } - }, - hierarchicalRepulsion: { - centralGravity: { number: number }, - springLength: { number: number }, - springConstant: { number: number }, - nodeDistance: { number: number }, - damping: { number: number }, - __type__: { object: object } - }, - maxVelocity: { number: number }, - minVelocity: { number: number }, // px/s - solver: { string: ['barnesHut', 'repulsion', 'hierarchicalRepulsion', 'forceAtlas2Based'] }, - stabilization: { - enabled: { boolean: boolean }, - iterations: { number: number }, // maximum number of iteration to stabilize - updateInterval: { number: number }, - onlyDynamicEdges: { boolean: boolean }, - fit: { boolean: boolean }, - __type__: { object: object, boolean: boolean } - }, - timestep: { number: number }, - __type__: { object: object, boolean: boolean } - }, - - //globals : - autoResize: { boolean: boolean }, - clickToUse: { boolean: boolean }, - locale: { string: string }, - locales: { - __any__: { any: any }, - __type__: { object: object } - }, - height: { string: string }, - width: { string: string }, - __type__: { object: object } - }; - - allOptions.groups.__any__ = allOptions.nodes; - allOptions.manipulation.controlNodeStyle = allOptions.nodes; - - var configureOptions = { - nodes: { - borderWidth: [1, 0, 10, 1], - borderWidthSelected: [2, 0, 10, 1], - color: { - border: ['color', '#2B7CE9'], - background: ['color', '#97C2FC'], - highlight: { - border: ['color', '#2B7CE9'], - background: ['color', '#D2E5FF'] - }, - hover: { - border: ['color', '#2B7CE9'], - background: ['color', '#D2E5FF'] - } - }, - fixed: { - x: false, - y: false - }, - font: { - color: ['color', '#343434'], - size: [14, 0, 100, 1], // px - face: ['arial', 'verdana', 'tahoma'], - background: ['color', 'none'], - strokeWidth: [0, 0, 50, 1], // px - strokeColor: ['color', '#ffffff'] - }, - //group: 'string', - hidden: false, - //icon: { - // face: 'string', //'FontAwesome', - // code: 'string', //'\uf007', - // size: [50, 0, 200, 1], //50, - // color: ['color','#2B7CE9'] //'#aa00ff' - //}, - //image: 'string', // --> URL - physics: true, - scaling: { - min: [10, 0, 200, 1], - max: [30, 0, 200, 1], - label: { - enabled: false, - min: [14, 0, 200, 1], - max: [30, 0, 200, 1], - maxVisible: [30, 0, 200, 1], - drawThreshold: [5, 0, 20, 1] - } - }, - shadow: { - enabled: false, - size: [10, 0, 20, 1], - x: [5, -30, 30, 1], - y: [5, -30, 30, 1] - }, - shape: ['ellipse', 'box', 'circle', 'database', 'diamond', 'dot', 'square', 'star', 'text', 'triangle', 'triangleDown'], - size: [25, 0, 200, 1] - }, - edges: { - arrows: { - to: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, - from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] } - }, - color: { - color: ['color', '#848484'], - highlight: ['color', '#848484'], - hover: ['color', '#848484'], - inherit: ['from', 'to', 'both', true, false], - opacity: [1, 0, 1, 0.05] - }, - dashes: false, - font: { - color: ['color', '#343434'], - size: [14, 0, 100, 1], // px - face: ['arial', 'verdana', 'tahoma'], - background: ['color', 'none'], - strokeWidth: [2, 0, 50, 1], // px - strokeColor: ['color', '#ffffff'], - align: ['horizontal', 'top', 'middle', 'bottom'] - }, - hidden: false, - hoverWidth: [1.5, 0, 5, 0.1], - physics: true, - scaling: { - min: [1, 0, 100, 1], - max: [15, 0, 100, 1], - label: { - enabled: true, - min: [14, 0, 200, 1], - max: [30, 0, 200, 1], - maxVisible: [30, 0, 200, 1], - drawThreshold: [5, 0, 20, 1] - } - }, - selectionWidth: [1.5, 0, 5, 0.1], - selfReferenceSize: [20, 0, 200, 1], - shadow: { - enabled: false, - size: [10, 0, 20, 1], - x: [5, -30, 30, 1], - y: [5, -30, 30, 1] - }, - smooth: { - enabled: true, - type: ['dynamic', 'continuous', 'discrete', 'diagonalCross', 'straightCross', 'horizontal', 'vertical', 'curvedCW', 'curvedCCW'], - roundness: [0.5, 0, 1, 0.05] - }, - width: [1, 0, 30, 1] - }, - layout: { - //randomSeed: [0, 0, 500, 1], - hierarchical: { - enabled: false, - levelSeparation: [150, 20, 500, 5], - direction: ['UD', 'DU', 'LR', 'RL'], // UD, DU, LR, RL - sortMethod: ['hubsize', 'directed'] // hubsize, directed - } - }, - interaction: { - dragNodes: true, - dragView: true, - hideEdgesOnDrag: false, - hideNodesOnDrag: false, - hover: false, - keyboard: { - enabled: false, - speed: { x: [10, 0, 40, 1], y: [10, 0, 40, 1], zoom: [0.02, 0, 0.1, 0.005] }, - bindToWindow: true - }, - multiselect: false, - navigationButtons: false, - selectable: true, - selectConnectedEdges: true, - hoverConnectedEdges: true, - tooltipDelay: [300, 0, 1000, 25], - zoomView: true - }, - manipulation: { - enabled: false, - initiallyActive: false - }, - physics: { - enabled: true, - barnesHut: { - //theta: [0.5, 0.1, 1, 0.05], - gravitationalConstant: [-2000, -30000, 0, 50], - centralGravity: [0.3, 0, 10, 0.05], - springLength: [95, 0, 500, 5], - springConstant: [0.04, 0, 1.2, 0.005], - damping: [0.09, 0, 1, 0.01], - avoidOverlap: [0, 0, 1, 0.01] - }, - forceAtlas2Based: { - //theta: [0.5, 0.1, 1, 0.05], - gravitationalConstant: [-50, -500, 0, 1], - centralGravity: [0.01, 0, 1, 0.005], - springLength: [95, 0, 500, 5], - springConstant: [0.08, 0, 1.2, 0.005], - damping: [0.4, 0, 1, 0.01], - avoidOverlap: [0, 0, 1, 0.01] - }, - repulsion: { - centralGravity: [0.2, 0, 10, 0.05], - springLength: [200, 0, 500, 5], - springConstant: [0.05, 0, 1.2, 0.005], - nodeDistance: [100, 0, 500, 5], - damping: [0.09, 0, 1, 0.01] - }, - hierarchicalRepulsion: { - centralGravity: [0.2, 0, 10, 0.05], - springLength: [100, 0, 500, 5], - springConstant: [0.01, 0, 1.2, 0.005], - nodeDistance: [120, 0, 500, 5], - damping: [0.09, 0, 1, 0.01] - }, - maxVelocity: [50, 0, 150, 1], - minVelocity: [0.1, 0.01, 0.5, 0.01], - solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'], - timestep: [0.5, 0.01, 1, 0.01] - }, - global: { - locale: ['en', 'nl'] - } - }; - - exports.allOptions = allOptions; - exports.configureOptions = configureOptions; - /***/ }, /* 109 */ /***/ function(module, exports, __webpack_require__) { diff --git a/docs/network/edges.html b/docs/network/edges.html index 54b71906..1fa394d8 100644 --- a/docs/network/edges.html +++ b/docs/network/edges.html @@ -125,6 +125,7 @@ var options = { hidden: false, hoverWidth: 1.5, label: undefined, + labelHighlightBold: true, length: undefined, physics: true, scaling:{ @@ -407,6 +408,12 @@ var options: { undefined The label of the edge. HTML does not work in here because the network uses HTML5 Canvas. + + labelHighlightBold + Boolean + true + Determines whether or not the label becomes bold when the edge is selected. + length Number diff --git a/docs/network/nodes.html b/docs/network/nodes.html index 2337bdb5..9f6c143c 100644 --- a/docs/network/nodes.html +++ b/docs/network/nodes.html @@ -129,6 +129,7 @@ var options = { }, image: undefined, label: undefined, + labelHighlightBold: true, level: undefined, mass: 1, physics: true, @@ -414,6 +415,12 @@ network.setOptions(options); undefined The label is the piece of text shown in or under the node, depending on the shape. + + labelHighlightBold + Boolean + true + Determines whether or not the label becomes bold when the node is selected. + level Number diff --git a/lib/network/modules/EdgesHandler.js b/lib/network/modules/EdgesHandler.js index a0dfbfe8..cde73d5b 100644 --- a/lib/network/modules/EdgesHandler.js +++ b/lib/network/modules/EdgesHandler.js @@ -47,6 +47,7 @@ class EdgesHandler { hidden: false, hoverWidth: 1.5, label: undefined, + labelHighlightBold: true, length: undefined, physics: true, scaling:{ diff --git a/lib/network/modules/NodesHandler.js b/lib/network/modules/NodesHandler.js index dad2bb0e..c6518fdf 100644 --- a/lib/network/modules/NodesHandler.js +++ b/lib/network/modules/NodesHandler.js @@ -61,6 +61,7 @@ class NodesHandler { }, image: undefined, // --> URL label: undefined, + labelHighlightBold: true, level: undefined, mass: 1, physics: true, diff --git a/lib/network/modules/components/Edge.js b/lib/network/modules/components/Edge.js index f3244894..ded3d039 100644 --- a/lib/network/modules/components/Edge.js +++ b/lib/network/modules/components/Edge.js @@ -98,6 +98,7 @@ class Edge { 'hidden', 'hoverWidth', 'label', + 'labelHighlightBold', 'length', 'line', 'opacity', diff --git a/lib/network/modules/components/shared/Label.js b/lib/network/modules/components/shared/Label.js index eb8f82c5..0141bc36 100644 --- a/lib/network/modules/components/shared/Label.js +++ b/lib/network/modules/components/shared/Label.js @@ -120,7 +120,7 @@ class Label { [x, yLine] = this._setAlignment(ctx, x, yLine, baseline); // configure context for drawing the text - ctx.font = (selected ? 'bold ' : '') + fontSize + "px " + this.options.font.face; + ctx.font = (selected && this.options.labelHighlightBold ? 'bold ' : '') + fontSize + "px " + this.options.font.face; ctx.fillStyle = fontColor; ctx.textAlign = 'center'; @@ -244,7 +244,7 @@ class Label { if (this.options.label !== undefined) { lines = String(this.options.label).split('\n'); lineCount = lines.length; - ctx.font = (selected ? 'bold ' : '') + this.options.font.size + "px " + this.options.font.face; + ctx.font = (selected && this.options.labelHighlightBold ? 'bold ' : '') + this.options.font.size + "px " + this.options.font.face; width = ctx.measureText(lines[0]).width; for (let i = 1; i < lineCount; i++) { let lineWidth = ctx.measureText(lines[i]).width; diff --git a/lib/network/options.js b/lib/network/options.js index 888f1c02..673ba664 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -51,6 +51,7 @@ let allOptions = { hidden: {boolean}, hoverWidth: {'function': 'function',number}, label: {string,'undefined': 'undefined'}, + labelHighlightBold: {boolean}, length: {number,'undefined': 'undefined'}, physics: {boolean}, scaling: { @@ -181,6 +182,7 @@ let allOptions = { id: {string, number}, image: {string,'undefined': 'undefined'}, // --> URL label: {string,'undefined': 'undefined'}, + labelHighlightBold: {boolean}, level: {number,'undefined': 'undefined'}, mass: {number}, physics: {boolean}, @@ -311,6 +313,7 @@ let configureOptions = { }, //group: 'string', hidden: false, + labelHighlightBold: true, //icon: { // face: 'string', //'FontAwesome', // code: 'string', //'\uf007', @@ -364,6 +367,7 @@ let configureOptions = { }, hidden: false, hoverWidth: [1.5, 0, 5, 0.1], + labelHighlightBold: true, physics: true, scaling: { min: [1, 0, 100, 1],