From c5f4b2afcd9d7b98956ac7ee46b8f2591b1999d9 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Fri, 22 Aug 2014 11:30:26 +0200 Subject: [PATCH] Fixed bug where empty groups would crash graph2d, cleaned code --- dist/vis.js | 48811 +++++++++++++------------- lib/timeline/component/LineGraph.js | 207 +- 2 files changed, 24518 insertions(+), 24500 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index e9c8fede..b225e5ef 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 3.2.1-SNAPSHOT - * @date 2014-08-21 + * @date 2014-08-22 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -81,63 +81,63 @@ return /******/ (function(modules) { // webpackBootstrap // utils exports.util = __webpack_require__(1); - exports.DOMutil = __webpack_require__(2); + exports.DOMutil = __webpack_require__(6); // data - exports.DataSet = __webpack_require__(3); - exports.DataView = __webpack_require__(4); + exports.DataSet = __webpack_require__(7); + exports.DataView = __webpack_require__(8); // Graph3d - exports.Graph3d = __webpack_require__(5); + exports.Graph3d = __webpack_require__(9); exports.graph3d = { - Camera: __webpack_require__(6), - Filter: __webpack_require__(7), - Point2d: __webpack_require__(8), - Point3d: __webpack_require__(9), - Slider: __webpack_require__(10), - StepNumber: __webpack_require__(11) + Camera: __webpack_require__(13), + Filter: __webpack_require__(14), + Point2d: __webpack_require__(12), + Point3d: __webpack_require__(11), + Slider: __webpack_require__(15), + StepNumber: __webpack_require__(16) }; // Timeline - exports.Timeline = __webpack_require__(12); - exports.Graph2d = __webpack_require__(13); + exports.Timeline = __webpack_require__(17); + exports.Graph2d = __webpack_require__(36); exports.timeline = { - DataStep: __webpack_require__(14), - Range: __webpack_require__(15), - stack: __webpack_require__(16), - TimeStep: __webpack_require__(17), + DataStep: __webpack_require__(38), + Range: __webpack_require__(20), + stack: __webpack_require__(31), + TimeStep: __webpack_require__(25), components: { items: { - Item: __webpack_require__(28), - ItemBox: __webpack_require__(29), - ItemPoint: __webpack_require__(30), - ItemRange: __webpack_require__(31) + Item: __webpack_require__(33), + ItemBox: __webpack_require__(34), + ItemPoint: __webpack_require__(35), + ItemRange: __webpack_require__(32) }, - Component: __webpack_require__(18), - CurrentTime: __webpack_require__(19), - CustomTime: __webpack_require__(20), - DataAxis: __webpack_require__(21), - GraphGroup: __webpack_require__(22), - Group: __webpack_require__(23), - ItemSet: __webpack_require__(24), - Legend: __webpack_require__(25), - LineGraph: __webpack_require__(26), - TimeAxis: __webpack_require__(27) + Component: __webpack_require__(22), + CurrentTime: __webpack_require__(26), + CustomTime: __webpack_require__(28), + DataAxis: __webpack_require__(39), + GraphGroup: __webpack_require__(40), + Group: __webpack_require__(30), + ItemSet: __webpack_require__(29), + Legend: __webpack_require__(41), + LineGraph: __webpack_require__(37), + TimeAxis: __webpack_require__(24) } }; // Network - exports.Network = __webpack_require__(32); + exports.Network = __webpack_require__(42); exports.network = { - Edge: __webpack_require__(33), - Groups: __webpack_require__(34), - Images: __webpack_require__(35), - Node: __webpack_require__(36), - Popup: __webpack_require__(37), - dotparser: __webpack_require__(38), - gephiParser: __webpack_require__(39) + Edge: __webpack_require__(49), + Groups: __webpack_require__(46), + Images: __webpack_require__(47), + Node: __webpack_require__(48), + Popup: __webpack_require__(50), + dotparser: __webpack_require__(44), + gephiParser: __webpack_require__(45) }; // Deprecated since v3.0.0 @@ -146,8 +146,8 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(40); - exports.hammer = __webpack_require__(41); + exports.moment = __webpack_require__(2); + exports.hammer = __webpack_require__(18); /***/ }, @@ -158,7 +158,7 @@ return /******/ (function(modules) { // webpackBootstrap // first check if moment.js is already loaded in the browser window, if so, // use this instance. Else, load via commonjs. - var moment = __webpack_require__(40); + var moment = __webpack_require__(2); /** * Test whether given object is a number @@ -1405,25321 +1405,24875 @@ return /******/ (function(modules) { // webpackBootstrap /* 2 */ /***/ function(module, exports, __webpack_require__) { - // DOM utility methods + // first check if moment.js is already loaded in the browser window, if so, + // use this instance. Else, load via commonjs. + module.exports = (typeof window !== 'undefined') && window['moment'] || __webpack_require__(3); - /** - * this prepares the JSON container for allocating SVG elements - * @param JSONcontainer - * @private - */ - 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 = []; - } - } - }; - /** - * 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]); - } - JSONcontainer[elementType].redundant = []; - } - } - } - }; +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { - /** - * 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; - }; + var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {//! moment.js + //! version : 2.7.0 + //! authors : Tim Wood, Iskren Chernev, Moment.js contributors + //! license : MIT + //! momentjs.com + (function (undefined) { - /** - * 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) { - 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); - 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: []}; - DOMContainer.appendChild(element); - } - JSONcontainer[elementType].used.push(element); - return element; - }; + /************************************ + Constants + ************************************/ + var moment, + VERSION = "2.7.0", + // the global-scope this is NOT the global object in Node.js + globalScope = typeof global !== 'undefined' ? global : this, + oldGlobalMoment, + round = Math.round, + i, + YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, + // internal storage for language config files + languages = {}, - /** - * draw a point object. this is a seperate 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 group - * @param JSONcontainer - * @param svgContainer - * @returns {*} - */ - exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) { - var point; - if (group.options.drawPoints.style == 'circle') { - point = exports.getSVGElement('circle',JSONcontainer,svgContainer); - point.setAttributeNS(null, "cx", x); - point.setAttributeNS(null, "cy", y); - point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size); - point.setAttributeNS(null, "class", group.className + " point"); - } - else { - point = exports.getSVGElement('rect',JSONcontainer,svgContainer); - point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size); - point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size); - point.setAttributeNS(null, "width", group.options.drawPoints.size); - point.setAttributeNS(null, "height", group.options.drawPoints.size); - point.setAttributeNS(null, "class", group.className + " point"); - } - return point; - }; + // moment internal properties + momentProperties = { + _isAMomentObject: null, + _i : null, + _f : null, + _l : null, + _strict : null, + _tzm : null, + _isUTC : null, + _offset : null, // optional. Combine with _isUTC + _pf : null, + _lang : null // optional + }, - /** - * 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) { - // if (height != 0) { - 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); - // } - }; + // check for nodeJS + hasModule = (typeof module !== 'undefined' && module.exports), -/***/ }, -/* 3 */ -/***/ function(module, exports, __webpack_require__) { + // ASP.NET json date format regex + aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, + aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, - var util = __webpack_require__(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 + isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, - /** - * 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 | DataTable} [data] Optional array with initial data - * @param {Object} [options] Available options: - * {String} fieldId Field name of the id in the - * items, 'id' by default. - * {Object. ["10", "00"] or "-1530" > ["-15", "30"] + parseTimezoneChunker = /([\+\-]|\d\d)/gi, - 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 (util.isDataTable(data)) { - // Google DataTable - var columns = this._getColumnNames(data); - for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { - var item = {}; - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - item[field] = data.getValue(row, col); - } + // getter and setter names + proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), + unitMillisecondFactors = { + 'Milliseconds' : 1, + 'Seconds' : 1e3, + 'Minutes' : 6e4, + 'Hours' : 36e5, + 'Days' : 864e5, + 'Months' : 2592e6, + 'Years' : 31536e6 + }, - id = me._addItem(item); - addedIds.push(id); - } - } - else if (data instanceof Object) { - // Single item - id = me._addItem(data); - addedIds.push(id); - } - else { - throw new Error('Unknown dataType'); - } + unitAliases = { + ms : 'millisecond', + s : 'second', + m : 'minute', + h : 'hour', + d : 'day', + D : 'date', + w : 'week', + W : 'isoWeek', + M : 'month', + Q : 'quarter', + y : 'year', + DDD : 'dayOfYear', + e : 'weekday', + E : 'isoWeekday', + gg: 'weekYear', + GG: 'isoWeekYear' + }, - if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); - } + camelFunctions = { + dayofyear : 'dayOfYear', + isoweekday : 'isoWeekday', + isoweek : 'isoWeek', + weekyear : 'weekYear', + isoweekyear : 'isoWeekYear' + }, - return addedIds; - }; + // format function strings + formatFunctions = {}, - /** - * Update existing items. When an item does not exist, it will be created - * @param {Object | Array | DataTable} data - * @param {String} [senderId] Optional sender id - * @return {Array} updatedIds The ids of the added or updated items - */ - DataSet.prototype.update = function (data, senderId) { - var addedIds = [], - updatedIds = [], - me = this, - fieldId = me._fieldId; + // default relative time thresholds + relativeTimeThresholds = { + s: 45, //seconds to minutes + m: 45, //minutes to hours + h: 22, //hours to days + dd: 25, //days to month (month == 1) + dm: 45, //days to months (months > 1) + dy: 345 //days to year + }, - var addOrUpdate = function (item) { - var id = item[fieldId]; - if (me._data[id]) { - // update item - id = me._updateItem(item); - updatedIds.push(id); - } - else { - // add new item - id = me._addItem(item); - addedIds.push(id); - } - }; + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w W M D d'.split(' '), + paddedTokens = 'M D H h m s w W'.split(' '), - if (Array.isArray(data)) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - addOrUpdate(data[i]); - } - } - else if (util.isDataTable(data)) { - // Google DataTable - var columns = this._getColumnNames(data); - for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { - var item = {}; - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - item[field] = data.getValue(row, col); - } - - addOrUpdate(item); - } - } - else if (data instanceof Object) { - // Single item - addOrUpdate(data); - } - else { - throw new Error('Unknown dataType'); - } - - if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); - } - if (updatedIds.length) { - this._trigger('update', {items: updatedIds}, senderId); - } - - return addedIds.concat(updatedIds); - }; - - /** - * Get a data item or multiple items. - * - * Usage: - * - * get() - * get(options: Object) - * get(options: Object, data: Array | DataTable) - * - * get(id: Number | String) - * get(id: Number | String, options: Object) - * get(id: Number | String, options: Object, data: Array | DataTable) - * - * get(ids: Number[] | String[]) - * get(ids: Number[] | String[], options: Object) - * get(ids: Number[] | String[], options: Object, data: Array | DataTable) - * - * Where: - * - * {Number | String} id The id of an item - * {Number[] | String{}} ids An array with ids of items - * {Object} options An Object with options. Available options: - * {String} [returnType] Type of data to be - * returned. Can be 'DataTable' or 'Array' (default) - * {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. - * {Array | DataTable} [data] If provided, items will be appended to this - * array or table. Required in case of Google - * DataTable. - * - * @throws Error - */ - DataSet.prototype.get = function (args) { - var me = this; - - // parse the arguments - var id, ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number') { - // get(id [, options] [, data]) - id = arguments[0]; - options = arguments[1]; - data = arguments[2]; - } - else if (firstType == 'Array') { - // get(ids [, options] [, data]) - ids = arguments[0]; - options = arguments[1]; - data = arguments[2]; - } - else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; - } - - // determine the return type - var returnType; - if (options && options.returnType) { - var allowedValues = ["DataTable", "Array", "Object"]; - returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; - - if (data && (returnType != util.getType(data))) { - throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + - 'does not correspond with specified options.type (' + options.type + ')'); - } - if (returnType == 'DataTable' && !util.isDataTable(data)) { - throw new Error('Parameter "data" must be a DataTable ' + - 'when options.type is "DataTable"'); - } - } - else if (data) { - returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; - } - else { - returnType = 'Array'; - } - - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], item, itemId, i, len; - - // 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); - } - } - } - } - - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } - - // filter fields of the items - if (options && options.fields) { - var fields = options.fields; - if (id != undefined) { - item = this._filterFields(item, fields); - } - else { - for (i = 0, len = items.length; i < len; i++) { - items[i] = this._filterFields(items[i], fields); - } - } - } - - // return the results - if (returnType == 'DataTable') { - var columns = this._getColumnNames(data); - if (id != undefined) { - // append a single item to the data table - me._appendRow(data, columns, item); - } - else { - // copy the items to the provided data table - for (i = 0; i < items.length; i++) { - me._appendRow(data, columns, items[i]); - } - } - return data; - } - else if (returnType == "Object") { - var result = {}; - for (i = 0; i < items.length; i++) { - result[items[i].id] = items[i]; - } - return result; - } - else { - // return an array - if (id != undefined) { - // a single item - return item; - } - else { - // multiple items - if (data) { - // copy the items to the provided array - for (i = 0, len = items.length; i < len; i++) { - data.push(items[i]); - } - return data; - } - else { - // just return our array - return items; - } - } - } - }; - - /** - * Get ids of all items or from a filtered set of items. - * @param {Object} [options] An Object with options. Available options: - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Array} ids - */ - DataSet.prototype.getIds = function (options) { - var data = this._data, - filter = options && options.filter, - order = options && options.order, - type = options && options.type || this._options.type, - i, - len, - id, - item, - items, - ids = []; - - if (filter) { - // get filtered items - if (order) { - // create ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - items.push(item); - } - } - } - - this._sort(items, order); - - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this._fieldId]; - } - } - else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, 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]); - } - } - - this._sort(items, order); - - for (i = 0, len = items.length; i < len; i++) { - ids[i] = items[i][this._fieldId]; - } - } - else { - // create unordered list - for (id in data) { - if (data.hasOwnProperty(id)) { - item = data[id]; - ids.push(item[this._fieldId]); - } - } - } - } - - return ids; - }; - - /** - * 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; - }; - - /** - * 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 (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); - - for (var i = 0, len = items.length; i < len; i++) { - item = items[i]; - id = item[this._fieldId]; - callback(item, id); - } - } - else { - // unordered - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - callback(item, id); - } - } - } - } - }; - - /** - * 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; - - // 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)); - } - } - } - - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } - - return mappedItems; - }; - - /** - * Filter the fields of an item - * @param {Object} item - * @param {String[]} fields Field names - * @return {Object} filteredItem - * @private - */ - DataSet.prototype._filterFields = function (item, fields) { - var filteredItem = {}; - - for (var field in item) { - if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { - filteredItem[field] = item[field]; - } - } - - return filteredItem; - }; - - /** - * Sort the provided array with items - * @param {Object[]} items - * @param {String | function} order A field name or custom sort function. - * @private - */ - DataSet.prototype._sort = function (items, order) { - if (util.isString(order)) { - // order by provided field name - var name = order; // field name - items.sort(function (a, b) { - var av = a[name]; - var bv = b[name]; - return (av > bv) ? 1 : ((av < bv) ? -1 : 0); - }); - } - else if (typeof order === 'function') { - // order by sort function - items.sort(order); - } - // TODO: extend order by an Object {field:String, direction:String} - // where direction can be 'asc' or 'desc' - else { - throw new TypeError('Order must be a function or a string'); - } - }; - - /** - * Remove an object by pointer or by id - * @param {String | Number | Object | Array} id Object or id, or an array with - * objects or ids to be removed - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds - */ - DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, len, removedId; - - if (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); - } - } - - if (removedIds.length) { - this._trigger('remove', {items: removedIds}, senderId); - } - - return removedIds; - }; - - /** - * Remove an item by its id - * @param {Number | String | Object} id id or item - * @returns {Number | String | null} id - * @private - */ - DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this._data[id]) { - delete this._data[id]; - return id; - } - } - else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - return itemId; - } - } - return null; - }; - - /** - * Clear the data - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds The ids of all removed items - */ - DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this._data); - - this._data = {}; - - this._trigger('remove', {items: ids}, senderId); - - return ids; - }; - - /** - * Find the item with maximum value of a specified field - * @param {String} field - * @return {Object | null} item Item containing max value, or null if no items - */ - DataSet.prototype.max = function (field) { - var data = this._data, - max = null, - maxField = null; - - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!max || itemField > maxField)) { - max = item; - maxField = itemField; - } - } - } - - return max; - }; - - /** - * Find the item with minimum value of a specified field - * @param {String} field - * @return {Object | null} item Item containing max value, or null if no items - */ - DataSet.prototype.min = function (field) { - var data = this._data, - min = null, - minField = null; - - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!min || itemField < minField)) { - min = item; - minField = itemField; - } - } - } - - return min; - }; - - /** - * Find all distinct values of a specified field - * @param {String} field - * @return {Array} values Array containing all distinct values. If 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; - - 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 (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); - } - } - - return values; - }; - - /** - * Add a single item. Will fail when an item with the same id already exists. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._addItem = function (item) { - var id = item[this._fieldId]; - - if (id != undefined) { - // check whether this id is already taken - if (this._data[id]) { - // item already exists - throw new Error('Cannot add item: item with id ' + id + ' already exists'); - } - } - else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; - } - - 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; - - return id; - }; - - /** - * 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; - - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; - } - - // 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; - }; - - /** - * Update a single item: merge with existing item. - * Will fail when the item has no id, or when there does not exist an item - * with the same id. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._updateItem = function (item) { - var id = item[this._fieldId]; - if (id == undefined) { - throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); - } - var d = this._data[id]; - if (!d) { - // item doesn't exist - throw new Error('Cannot update item: no item with id ' + id + ' found'); - } - - // merge with current item - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } - } - - return id; - }; - - /** - * Get an array with the column names of a Google DataTable - * @param {DataTable} dataTable - * @return {String[]} columnNames - * @private - */ - DataSet.prototype._getColumnNames = function (dataTable) { - var columns = []; - for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { - columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); - } - return columns; - }; - - /** - * Append an item as a row to the dataTable - * @param dataTable - * @param columns - * @param item - * @private - */ - DataSet.prototype._appendRow = function (dataTable, columns, item) { - var row = dataTable.addRow(); - - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - dataTable.setValue(row, col, item[field]); - } - }; - - module.exports = DataSet; - - -/***/ }, -/* 4 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - - /** - * 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._options = options || {}; - this._fieldId = 'id'; // name of the field containing id - this._subscribers = {}; // event subscribers - - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; - - this.setData(data); - } - - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly - - /** - * Set a data source for the view - * @param {DataSet | DataView} data - */ - DataView.prototype.setData = function (data) { - var ids, i, len; - - if (this._data) { - // unsubscribe from current dataset - if (this._data.unsubscribe) { - this._data.unsubscribe('*', this.listener); - } - - // trigger a remove of all items in memory - ids = []; - for (var id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - ids.push(id); - } - } - this._ids = {}; - this._trigger('remove', {items: ids}); - } - - this._data = data; - - if (this._data) { - // update fieldId - this._fieldId = this._options.fieldId || - (this._data && this._data.options && this._data.options.fieldId) || - 'id'; - - // trigger an add of all added items - ids = this._data.getIds({filter: this._options && this._options.filter}); - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - this._ids[id] = true; - } - this._trigger('add', {items: ids}); - - // subscribe to new dataset - if (this._data.on) { - this._data.on('*', this.listener); - } - } - }; - - /** - * Get data from the data view - * - * Usage: - * - * get() - * get(options: Object) - * get(options: Object, data: Array | DataTable) - * - * get(id: Number) - * get(id: Number, options: Object) - * get(id: Number, options: Object, data: Array | DataTable) - * - * get(ids: Number[]) - * get(ids: Number[], options: Object) - * get(ids: Number[], options: Object, data: Array | DataTable) - * - * Where: - * - * {Number | String} id The id of an item - * {Number[] | String{}} ids An array with ids of items - * {Object} options An Object with options. Available options: - * {String} [type] Type of data to be returned. Can - * be 'DataTable' or 'Array' (default) - * {Object.} [convert] - * {String[]} [fields] field names to be returned - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * {Array | DataTable} [data] If provided, items will be appended to this - * array or table. Required in case of Google - * DataTable. - * @param args - */ - DataView.prototype.get = function (args) { - var me = this; - - // parse the arguments - var ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') { - // get(id(s) [, options] [, data]) - ids = arguments[0]; // can be a single id or an array with ids - options = arguments[1]; - data = arguments[2]; - } - else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; - } - - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); - - // create a combined filter method when needed - if (this._options.filter && options && options.filter) { - viewOptions.filter = function (item) { - return me._options.filter(item) && options.filter(item); - } - } - - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); - - return this._data && this._data.get.apply(this._data, getArguments); - }; - - /** - * Get ids of all items or from a filtered set of items. - * @param {Object} [options] An Object with options. Available options: - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Array} ids - */ - DataView.prototype.getIds = function (options) { - var ids; - - if (this._data) { - var defaultFilter = this._options.filter; - var filter; - - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); - } - } - else { - filter = options.filter; - } - } - else { - filter = defaultFilter; - } - - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); - } - else { - ids = []; - } - - return ids; - }; - - /** - * 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; - }; - - /** - * Event listener. Will propagate all events from the connected data set to - * the subscribers of the DataView, but will filter the items and only trigger - * when there are changes in the filtered data set. - * @param {String} event - * @param {Object | null} params - * @param {String} senderId - * @private - */ - DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item, - ids = params && params.items, - data = this._data, - added = [], - updated = [], - removed = []; - - if (ids && data) { - switch (event) { - case 'add': - // filter the ids of the added items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - if (item) { - this._ids[id] = true; - added.push(id); - } - } - - break; - - case 'update': - // determine the event from the views viewpoint: an updated - // item can be added, updated, or removed from this view. - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - - if (item) { - if (this._ids[id]) { - updated.push(id); - } - else { - this._ids[id] = true; - added.push(id); - } - } - else { - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } - else { - // nothing interesting for me :-( - } - } - } - - break; - - case 'remove': - // filter the ids of the removed items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } - } - - break; - } - - if (added.length) { - this._trigger('add', {items: added}, senderId); - } - if (updated.length) { - this._trigger('update', {items: updated}, senderId); - } - if (removed.length) { - this._trigger('remove', {items: removed}, senderId); - } - } - }; - - // copy subscription functionality from DataSet - DataView.prototype.on = DataSet.prototype.on; - DataView.prototype.off = DataSet.prototype.off; - DataView.prototype._trigger = DataSet.prototype._trigger; - - // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) - DataView.prototype.subscribe = DataView.prototype.on; - DataView.prototype.unsubscribe = DataView.prototype.off; - - module.exports = DataView; - -/***/ }, -/* 5 */ -/***/ function(module, exports, __webpack_require__) { - - var Emitter = __webpack_require__(48); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var util = __webpack_require__(1); - var Point3d = __webpack_require__(9); - var Point2d = __webpack_require__(8); - var Camera = __webpack_require__(6); - var Filter = __webpack_require__(7); - var Slider = __webpack_require__(10); - var StepNumber = __webpack_require__(11); - - /** - * @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'); - } - - // 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.xLabel = 'x'; - this.yLabel = 'y'; - this.zLabel = 'z'; - this.filterLabel = 'time'; - this.legendLabel = 'value'; - - 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' - - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; - - this.camera = new Camera(); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects - - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; - - 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 - - // constants - this.colorAxis = '#4D4D4D'; - this.colorGrid = '#D3D3D3'; - this.colorDot = '#7DC1FF'; - this.colorDotBorder = '#3267D2'; - - // create a frame and canvas - this.create(); - - // apply options (also when undefined) - this.setOptions(options); - - // apply data - if (data) { - this.setData(data); - } - } - - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); - - /** - * 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)); - - // 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; - } - } - - // 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); - }; - - - /** - * 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); - }; - - /** - * 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, - - // 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)); - - return new Point3d(dx, dy, dz); - }; - - /** - * 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; - - // 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()); - } - - // 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); - }; - - /** - * 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; - - 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) { - // use use defaults - } - 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'; - }; - - - /// 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 - }; - - /** - * 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; - } - - return -1; - }; - - /** - * 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; - - 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; - - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } - else { - throw 'Unknown style "' + this.style + '"'; - } - }; - - Graph3d.prototype.getNumberOfRows = function(data) { - return data.length; - } - - - Graph3d.prototype.getNumberOfColumns = function(data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; - } - } - 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]); - } - } - 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]; } - 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; - - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off('*', this._onChange); - } - - if (rawData === undefined) - return; - - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); - } - - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } - else { - throw new Error('Array, DataSet, or DataView expected'); - } - - if (data.length == 0) - return; - - this.dataSet = rawData; - this.dataTable = data; - - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); - }; - this.dataSet.on('*', this._onChange); - - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange - - // 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'; - - - - // 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();}); - } - } - - - var withBars = this.style == Graph3d.STYLE.BAR || - this.style == Graph3d.STYLE.BARCOLOR || - this.style == Graph3d.STYLE.BARSIZE; - - // 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; - } - - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; - } - else { - var dataY = this.getDistinctValues(data,this.colY); - this.yBarWidth = (dataY[1] - dataY[0]) || 1; - } - } - - // 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 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; - - 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; - - 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(); - }; - - - - /** - * 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; - - var dataPoints = []; - - 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 - - // 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; - - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); - } - } - - 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; - - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); - - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } - - 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 (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; - - dataPoints.push(obj); - } - } - - return dataPoints; - }; - - /** - * 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); - } - - 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 (event) {me._onMouseDown(event);}; - var ontouchstart = function (event) {me._onTouchStart(event);}; - var onmousewheel = function (event) {me._onWheel(event);}; - var ontooltip = function (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 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; - - this._resizeCanvas(); - }; - - /** - * 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%'; - - 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'; - }; - - /** - * Start animation - */ - Graph3d.prototype.animationStart = function() { - if (!this.frame.filter || !this.frame.filter.slider) - throw 'No animation available'; - - this.frame.filter.slider.play(); - }; - - - /** - * Stop animation - */ - Graph3d.prototype.animationStop = function() { - if (!this.frame.filter || !this.frame.filter.slider) return; - - this.frame.filter.slider.stop(); - }; - - - /** - * 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 - } - - // 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 - } - }; - - /** - * 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 (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } - - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } - - this.redraw(); - }; - - - /** - * 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; - }; - - /** - * Load data into the 3D Graph - */ - Graph3d.prototype._readData = function(data) { - // read the data - this._dataInitialize(data, this.style); - - - if (this.dataFilter) { - // apply filtering - this.dataPoints = this.dataFilter._getDataPoints(); - } - else { - // no filtering. load all data - this.dataPoints = this._getDataPoints(this.dataTable); - } - - // draw the filter - this._redrawFilter(); - }; - - /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data - */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); - - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; - - /** - * Update the options. Options will be merged with current options - * @param {Object} options - */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; - - this.animationStop(); - - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; - - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - - 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; - - 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; - - if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart; - - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - - 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; - - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); - } - else { - this.camera.setArmRotation(1.0, 0.5); - this.camera.setArmLength(1.7); - } - } - - this._setBackgroundColor(options && options.backgroundColor); - - this.setSize(this.width, this.height); - - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); - } - - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; - - /** - * Redraw the Graph. - */ - Graph3d.prototype.redraw = function() { - if (this.dataPoints === undefined) { - throw 'Error: graph data not initialized'; - } - - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); - - 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._redrawInfo(); - this._redrawLegend(); - }; - - /** - * Clear the canvas before redrawing - */ - Graph3d.prototype._redrawClear = function() { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - - ctx.clearRect(0, 0, canvas.width, canvas.height); - }; - - - /** - * Redraw the legend showing the colors - */ - Graph3d.prototype._redrawLegend = function() { - var y; - - if (this.style === Graph3d.STYLE.DOTCOLOR || - this.style === Graph3d.STYLE.DOTSIZE) { - - var dotSize = this.frame.clientWidth * 0.02; - - 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 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; - } - - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - ctx.lineWidth = 1; - ctx.font = '14px arial'; // TODO: put in options - - 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); - - //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); - - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); - } - - ctx.strokeStyle = this.colorAxis; - ctx.strokeRect(left, top, widthMax, height); - } - - if (this.style === Graph3d.STYLE.DOTSIZE) { - // draw border around color bar - ctx.strokeStyle = this.colorAxis; - ctx.fillStyle = this.colorDot; - 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(); - } - - 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; - - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); - - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - 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); - } - }; - - /** - * Redraw the filter - */ - Graph3d.prototype._redrawFilter = function() { - this.frame.filter.innerHTML = ''; - - if (this.dataFilter) { - var options = { - 'visible': this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; - - // TODO: css here is not nice here... - this.frame.filter.style.padding = '10px'; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; - - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); - - // create an event handler - var me = this; - var onchange = function () { - var index = slider.getIndex(); - - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); - - me.redraw(); - }; - slider.setOnChangeCallback(onchange); - } - else { - this.frame.filter.slider = undefined; - } - }; - - /** - * Redraw the slider - */ - Graph3d.prototype._redrawSlider = function() { - if ( this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); - } - }; - - - /** - * Redraw common information - */ - Graph3d.prototype._redrawInfo = function() { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext('2d'); - - ctx.font = '14px arial'; // TODO: put in options - ctx.lineStyle = 'gray'; - ctx.fillStyle = 'gray'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); - } - }; - - - /** - * 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; - - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + 'px arial'; - - // 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; - - // 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(); - - 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.colorGrid; - 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.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - - from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); - to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } - - 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.colorAxis; - ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); - - step.next(); - } + formatTokenFunctions = { + M : function () { + return this.month() + 1; + }, + MMM : function (format) { + return this.lang().monthsShort(this, format); + }, + MMMM : function (format) { + return this.lang().months(this, format); + }, + D : function () { + return this.date(); + }, + DDD : function () { + return this.dayOfYear(); + }, + d : function () { + return this.day(); + }, + dd : function (format) { + return this.lang().weekdaysMin(this, format); + }, + ddd : function (format) { + return this.lang().weekdaysShort(this, format); + }, + dddd : function (format) { + return this.lang().weekdays(this, format); + }, + w : function () { + return this.week(); + }, + W : function () { + return this.isoWeek(); + }, + YY : function () { + return leftZeroFill(this.year() % 100, 2); + }, + YYYY : function () { + return leftZeroFill(this.year(), 4); + }, + YYYYY : function () { + return leftZeroFill(this.year(), 5); + }, + YYYYYY : function () { + var y = this.year(), sign = y >= 0 ? '+' : '-'; + return sign + leftZeroFill(Math.abs(y), 6); + }, + gg : function () { + return leftZeroFill(this.weekYear() % 100, 2); + }, + gggg : function () { + return leftZeroFill(this.weekYear(), 4); + }, + ggggg : function () { + return leftZeroFill(this.weekYear(), 5); + }, + GG : function () { + return leftZeroFill(this.isoWeekYear() % 100, 2); + }, + GGGG : function () { + return leftZeroFill(this.isoWeekYear(), 4); + }, + GGGGG : function () { + return leftZeroFill(this.isoWeekYear(), 5); + }, + e : function () { + return this.weekday(); + }, + E : function () { + return this.isoWeekday(); + }, + a : function () { + return this.lang().meridiem(this.hours(), this.minutes(), true); + }, + A : function () { + return this.lang().meridiem(this.hours(), this.minutes(), false); + }, + H : function () { + return this.hours(); + }, + h : function () { + return this.hours() % 12 || 12; + }, + m : function () { + return this.minutes(); + }, + s : function () { + return this.seconds(); + }, + S : function () { + return toInt(this.milliseconds() / 100); + }, + SS : function () { + return leftZeroFill(toInt(this.milliseconds() / 10), 2); + }, + SSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + SSSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + Z : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2); + }, + ZZ : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); + }, + z : function () { + return this.zoneAbbr(); + }, + zz : function () { + return this.zoneName(); + }, + X : function () { + return this.unix(); + }, + Q : function () { + return this.quarter(); + } + }, - // 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.colorGrid; - 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.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; - 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.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + // Pick the first defined of two or three arguments. dfl comes from + // default. + function dfl(a, b, c) { + switch (arguments.length) { + case 2: return a != null ? a : b; + case 3: return a != null ? a : b != null ? b : c; + default: throw new Error("Implement me"); + } } - 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'; + function defaultParsingFlags() { + // We need to deep clone this object, and es5 standard is not very + // helpful. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso: false + }; } - ctx.fillStyle = this.colorAxis; - ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); - - step.next(); - } - - // 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.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(from.x - textMargin, from.y); - ctx.stroke(); - - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.colorAxis; - ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y); - - 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.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - - // 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.colorAxis; - 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.colorAxis; - ctx.beginPath(); - ctx.moveTo(xMin2d.x, xMin2d.y); - ctx.lineTo(xMax2d.x, xMax2d.y); - ctx.stroke(); - - // 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.colorAxis; - 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.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // 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'; + function deprecate(msg, fn) { + var firstTime = true; + function printMsg() { + if (moment.suppressDeprecationWarnings === false && + typeof console !== 'undefined' && console.warn) { + console.warn("Deprecation warning: " + msg); + } + } + return extend(function () { + if (firstTime) { + printMsg(); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); } - ctx.fillStyle = this.colorAxis; - 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'; + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; } - else if (Math.sin(armAngle * 2) > 0){ - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; + function ordinalizeToken(func, period) { + return function (a) { + return this.lang().ordinal(func.call(this, a), period); + }; } - else { - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; + + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); } - ctx.fillStyle = this.colorAxis; - ctx.fillText(yLabel, text.x, text.y); - } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); - // 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.colorAxis; - ctx.fillText(zLabel, text.x - offset, text.y); - } - }; - /** - * 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; + /************************************ + Constructors + ************************************/ - C = V * S; - Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(((H/60) % 2) - 1)); + function Language() { - 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; - } + // Moment prototype object + function Moment(config) { + checkOverflow(config); + extend(this, config); + } - return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')'; - }; + // Duration Constructor + 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; + // 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; - /** - * 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; + this._data = {}; + this._bubble(); + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + /************************************ + Helpers + ************************************/ - // 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); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + function extend(a, b) { + for (var i in b) { + if (b.hasOwnProperty(i)) { + a[i] = b[i]; + } + } - // 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; - } + if (b.hasOwnProperty("toString")) { + a.toString = b.toString; + } - // sort the points on depth of their (x,y) position (not on z) - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + if (b.hasOwnProperty("valueOf")) { + a.valueOf = b.valueOf; + } - 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; + return a; + } - if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + function cloneMoment(m) { + var result = {}, i; + for (i in m) { + if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) { + result[i] = m[i]; + } + } - 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) + return result; + } - topSideVisible = (crossproduct.z > 0); + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); } - else { - topSideVisible = true; + } + + // left zero fill a number + // see http://jsperf.com/left-zero-filling for performance comparison + function leftZeroFill(number, targetLength, forceSign) { + var output = '' + Math.abs(number), + sign = number >= 0; + + while (output.length < targetLength) { + output = '0' + output; } + return (sign ? (forceSign ? '+' : '') : '-') + output; + } - 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 + // helper function for _.addTime and _.subtractTime + function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; - 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.colorAxis; - } + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); } - else { - fillStyle = 'gray'; - strokeStyle = this.colorAxis; + if (days) { + rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding); } - lineWidth = 0.5; + if (months) { + rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding); + } + if (updateOffset) { + moment.updateOffset(mom, days || months); + } + } - ctx.lineWidth = lineWidth; - 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(); - } + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; } - } - 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) { - if (this.showPerspective) { - lineWidth = 2 / -point.trans.z; - } - else { - lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); + function isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } + + // compare two arrays, return the number of differences + 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; + } - 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; + function normalizeUnits(units) { + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; + } + return units; + } - ctx.lineWidth = lineWidth; - 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 normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; - 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; + for (prop in inputObject) { + if (inputObject.hasOwnProperty(prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } - ctx.lineWidth = lineWidth; - 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 normalizedInput; } - } - }; + function makeList(field) { + var count, setter; - /** - * 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; + if (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; + } + else if (field.indexOf('month') === 0) { + count = 12; + setter = 'month'; + } + else { + return; + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + moment[field] = function (format, index) { + var i, getter, + method = moment.fn._lang[field], + results = []; - // 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; + if (typeof format === 'number') { + index = format; + format = undefined; + } - // 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; - } + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment.fn._lang, m, format || ''); + }; - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; + } - // 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]; + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - 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.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(point.screen.x, point.screen.y); - ctx.stroke(); + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } + + return value; } - // 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); + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); } - else { - size = dotSize; + + function weeksInYear(year, dow, doy) { + return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; } - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; } - else { - radius = size * -(this.eye.z / this.camera.getArmLength()); + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } - if (radius < 0) { - radius = 0; + + function checkOverflow(m) { + var overflow; + if (m._a && m._pf.overflow === -2) { + overflow = + m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : + m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : + m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR : + m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : + m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : + m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + + m._pf.overflow = overflow; + } } - 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); + function isValid(m) { + if (m._isValid == null) { + m._isValid = !isNaN(m._d.getTime()) && + m._pf.overflow < 0 && + !m._pf.empty && + !m._pf.invalidMonth && + !m._pf.nullInput && + !m._pf.invalidFormat && + !m._pf.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + m._pf.charsLeftOver === 0 && + m._pf.unusedTokens.length === 0; + } + } + return m._isValid; } - else if (this.style === Graph3d.STYLE.DOTSIZE) { - color = this.colorDot; - borderColor = this.colorDotBorder; + + function normalizeLanguage(key) { + return key ? key.toLowerCase().replace('_', '-') : key; } - 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); + + // Return a moment from input, that is local/utc/zone equivalent to model. + function makeAs(input, model) { + return model._isUTC ? moment(input).zone(model._offset || 0) : + moment(input).local(); } - // draw the circle - ctx.lineWidth = 1.0; - 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(); - } - }; + /************************************ + Languages + ************************************/ - /** - * 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; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + extend(Language.prototype, { - // 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; + set : function (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + }, - // 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; - } + _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), + months : function (m) { + return this._months[m.month()]; + }, - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, - // 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]; + monthsParse : function (monthName) { + var i, mom, regex; - // 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.colorDot; - borderColor = this.colorDotBorder; - } - 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 (!this._monthsParse) { + this._monthsParse = []; + } - // 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); - } + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + if (!this._monthsParse[i]) { + mom = moment.utc([2000, i]); + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._monthsParse[i].test(monthName)) { + return i; + } + } + }, - // 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)} - ]; + _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, - // 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); - }); + _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, - // 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; + _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, - // 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}) - } + weekdaysParse : function (weekdayName) { + var i, mom, regex; - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + if (!this._weekdaysParse[i]) { + mom = moment([2000, 1]).day(i); + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + }, - // both are equal - return 0; - }); + _longDateFormat : { + LT : "h:mm A", + L : "MM/DD/YYYY", + LL : "MMMM D YYYY", + LLL : "MMMM D YYYY LT", + LLLL : "dddd, MMMM D YYYY LT" + }, + longDateFormat : function (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; + } + return output; + }, - // draw the ordered surfaces - ctx.lineWidth = 1; - 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(); - } - } - }; + isPM : function (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + }, + + _meridiemParse : /[ap]\.?m?\.?/i, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, + + _calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }, + calendar : function (key, mom) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.apply(mom) : output; + }, + + _relativeTime : { + future : "in %s", + past : "%s ago", + s : "a few seconds", + m : "a minute", + mm : "%d minutes", + h : "an hour", + hh : "%d hours", + d : "a day", + dd : "%d days", + M : "a month", + MM : "%d months", + y : "a year", + yy : "%d years" + }, + relativeTime : function (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + }, + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, + ordinal : function (number) { + return this._ordinal.replace("%d", number); + }, + _ordinal : "%d", - /** - * 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; + preparse : function (string) { + return string; + }, - if (this.dataPoints === undefined || this.dataPoints.length <= 0) - return; // TODO: throw exception? + postformat : function (string) { + return string; + }, - // 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); + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + }, - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } + _week : { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }, - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; + } + }); - ctx.lineWidth = 1; // TODO: make customizable - ctx.strokeStyle = 'blue'; // TODO: make customizable - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - } + // Loads a language definition into the `languages` cache. The function + // takes a key and optionally values. If not in the browser and no values + // are provided, it will load the language file module. As a convenience, + // this function also returns the language values. + function loadLang(key, values) { + values.abbr = key; + if (!languages[key]) { + languages[key] = new Language(); + } + languages[key].set(values); + return languages[key]; + } - // 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); - } + // Remove a language from the `languages` cache. Mostly useful in tests. + function unloadLang(key) { + delete languages[key]; + } - // finish the line - if (this.dataPoints.length > 0) { - ctx.stroke(); - } - }; + // Determines which language definition to use and returns it. + // + // With no parameters, it will return the global language. If you + // pass in a language key, such as 'en', it will return the + // definition for 'en', so long as 'en' has already been loaded using + // moment.lang. + function getLangDefinition(key) { + var i = 0, j, lang, next, split, + get = function (k) { + if (!languages[k] && hasModule) { + try { + __webpack_require__(4)("./" + k); + } catch (e) { } + } + return languages[k]; + }; - /** - * 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; + if (!key) { + return moment.fn._lang; + } - // check if mouse is still down (may be up when focus is lost for example - // in an iframe) - if (this.leftButtonDown) { - this._onMouseUp(event); - } + if (!isArray(key)) { + //short-circuit everything else + lang = get(key); + if (lang) { + return lang; + } + key = [key]; + } - // only react on left mouse button down - this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); - if (!this.leftButtonDown && !this.touchDown) return; + //pick the language 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 + while (i < key.length) { + split = normalizeLanguage(key[i]).split('-'); + j = split.length; + next = normalizeLanguage(key[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + lang = get(split.slice(0, j).join('-')); + if (lang) { + return lang; + } + 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 moment.fn._lang; + } - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); + /************************************ + Formatting + ************************************/ - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); - this.frame.style.cursor = 'move'; + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ""); + } + return input.replace(/\\/g, ""); + } - // 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); - }; + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } - /** - * 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; + 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; + }; + } - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; + // format date using native date object + function formatMoment(m, format) { - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; + if (!m.isValid()) { + return m.lang().invalidDate(); + } - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + format = expandFormat(format, m.lang()); - // 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; - } + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } - // 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; - } + return formatFunctions[format](m); + } - this.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); + function expandFormat(format, lang) { + var i = 5; - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + function replaceLongDateFormatTokens(input) { + return lang.longDateFormat(input) || input; + } - util.preventDefault(event); - }; + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + return format; + } - /** - * Stop moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {event} event The event - */ - Graph3d.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; - this.leftButtonDown = false; - // remove event listeners here - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; + /************************************ + Parsing + ************************************/ + - /** - * 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 - */ - Graph3d.prototype._onTooltip = function (event) { - var delay = 300; // ms - var mouseX = getMouseX(event) - util.getAbsoluteLeft(this.frame); - var mouseY = getMouseY(event) - util.getAbsoluteTop(this.frame); + // get the regex to find the next token + function getParseRegexForToken(token, config) { + var a, strict = config._strict; + switch (token) { + case 'Q': + return parseTokenOneDigit; + case 'DDDD': + return parseTokenThreeDigits; + case 'YYYY': + case 'GGGG': + case 'gggg': + return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; + case 'Y': + case 'G': + case 'g': + return parseTokenSignedNumber; + case 'YYYYYY': + case 'YYYYY': + case 'GGGGG': + case 'ggggg': + return strict ? parseTokenSixDigits : parseTokenOneToSixDigits; + case 'S': + if (strict) { return parseTokenOneDigit; } + /* falls through */ + case 'SS': + if (strict) { return parseTokenTwoDigits; } + /* falls through */ + case 'SSS': + if (strict) { return parseTokenThreeDigits; } + /* falls through */ + case 'DDD': + return parseTokenOneToThreeDigits; + case 'MMM': + case 'MMMM': + case 'dd': + case 'ddd': + case 'dddd': + return parseTokenWord; + case 'a': + case 'A': + return getLangDefinition(config._l)._meridiemParse; + case 'X': + return parseTokenTimestampMs; + case 'Z': + case 'ZZ': + return parseTokenTimezone; + case 'T': + return parseTokenT; + case 'SSSS': + return parseTokenDigits; + case 'MM': + case 'DD': + case 'YY': + case 'GG': + case 'gg': + case 'HH': + case 'hh': + case 'mm': + case 'ss': + case 'ww': + case 'WW': + return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits; + case 'M': + case 'D': + case 'd': + case 'H': + case 'h': + case 'm': + case 's': + case 'w': + case 'W': + case 'e': + case 'E': + return parseTokenOneOrTwoDigits; + case 'Do': + return parseTokenOrdinal; + default : + a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i")); + return a; + } + } - if (!this.showTooltip) { - return; - } + function timezoneMinutesFromString(string) { + string = string || ""; + var possibleTzMatches = (string.match(parseTokenTimezone) || []), + tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], + parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0], + minutes = +(parts[1] * 60) + toInt(parts[2]); - if (this.tooltipTimeout) { - clearTimeout(this.tooltipTimeout); - } + return parts[0] === '+' ? -minutes : minutes; + } - // (delayed) display of a tooltip only if no mouse button is down - if (this.leftButtonDown) { - this._hideTooltip(); - return; - } + // function to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, datePartArray = config._a; - 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(); - } + switch (token) { + // QUARTER + case 'Q': + if (input != null) { + datePartArray[MONTH] = (toInt(input) - 1) * 3; + } + break; + // MONTH + case 'M' : // fall through to MM + case 'MM' : + if (input != null) { + datePartArray[MONTH] = toInt(input) - 1; + } + break; + case 'MMM' : // fall through to MMMM + case 'MMMM' : + a = getLangDefinition(config._l).monthsParse(input); + // if we didn't find a month name, mark the date as invalid. + if (a != null) { + datePartArray[MONTH] = a; + } else { + config._pf.invalidMonth = input; + } + break; + // DAY OF MONTH + case 'D' : // fall through to DD + case 'DD' : + if (input != null) { + datePartArray[DATE] = toInt(input); + } + break; + case 'Do' : + if (input != null) { + datePartArray[DATE] = toInt(parseInt(input, 10)); + } + break; + // DAY OF YEAR + case 'DDD' : // fall through to DDDD + case 'DDDD' : + if (input != null) { + config._dayOfYear = toInt(input); + } + + break; + // YEAR + case 'YY' : + datePartArray[YEAR] = moment.parseTwoDigitYear(input); + break; + case 'YYYY' : + case 'YYYYY' : + case 'YYYYYY' : + datePartArray[YEAR] = toInt(input); + break; + // AM / PM + case 'a' : // fall through to A + case 'A' : + config._isPm = getLangDefinition(config._l).isPM(input); + break; + // 24 HOUR + case 'H' : // fall through to hh + case 'HH' : // fall through to hh + case 'h' : // fall through to hh + case 'hh' : + datePartArray[HOUR] = toInt(input); + break; + // MINUTE + case 'm' : // fall through to mm + case 'mm' : + datePartArray[MINUTE] = toInt(input); + break; + // SECOND + case 's' : // fall through to ss + case 'ss' : + datePartArray[SECOND] = toInt(input); + break; + // MILLISECOND + case 'S' : + case 'SS' : + case 'SSS' : + case 'SSSS' : + datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); + break; + // UNIX TIMESTAMP WITH MS + case 'X': + config._d = new Date(parseFloat(input) * 1000); + break; + // TIMEZONE + case 'Z' : // fall through to ZZ + case 'ZZ' : + config._useUTC = true; + config._tzm = timezoneMinutesFromString(input); + break; + // WEEKDAY - human + case 'dd': + case 'ddd': + case 'dddd': + a = getLangDefinition(config._l).weekdaysParse(input); + // if we didn't get a weekday name, mark the date as invalid + if (a != null) { + config._w = config._w || {}; + config._w['d'] = a; + } else { + config._pf.invalidWeekday = input; + } + break; + // WEEK, WEEK DAY - numeric + case 'w': + case 'ww': + case 'W': + case 'WW': + case 'd': + case 'e': + case 'E': + token = token.substr(0, 1); + /* falls through */ + case 'gggg': + case 'GGGG': + case 'GGGGG': + token = token.substr(0, 2); + if (input) { + config._w = config._w || {}; + config._w[token] = toInt(input); + } + break; + case 'gg': + case 'GG': + config._w = config._w || {}; + config._w[token] = moment.parseTwoDigitYear(input); + } } - } - 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); - } - }; - - /** - * Event handler for touchstart event on mobile devices - */ - Graph3d.prototype._onTouchStart = function(event) { - this.touchDown = true; - - 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); - - this._onMouseDown(event); - }; - /** - * Event handler for touchmove event on mobile devices - */ - Graph3d.prototype._onTouchMove = function(event) { - this._onMouseMove(event); - }; - - /** - * Event handler for touchend event on mobile devices - */ - Graph3d.prototype._onTouchEnd = function(event) { - this.touchDown = false; - - util.removeEventListener(document, 'touchmove', this.ontouchmove); - util.removeEventListener(document, 'touchend', this.ontouchend); - - this._onMouseUp(event); - }; - - - /** - * Event handler for mouse wheel event, used to zoom the graph - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {event} event The event - */ - Graph3d.prototype._onWheel = function(event) { - if (!event) /* For IE. */ - event = window.event; + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp, lang; - // 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; - } + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - // 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); + // 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 = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year); + week = dfl(w.W, 1); + weekday = dfl(w.E, 1); + } else { + lang = getLangDefinition(config._l); + dow = lang._week.dow; + doy = lang._week.doy; - this.camera.setArmLength(newLength); - this.redraw(); + weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); + week = dfl(w.w, 1); - this._hideTooltip(); - } + 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); - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit('cameraPositionChange', parameters); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } - // 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); - }; + // 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 dateFromConfig(config) { + var i, date, input = [], currentDate, yearToUse; - /** - * 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 - */ - Graph3d.prototype._insideTriangle = function (point, triangle) { - var a = triangle[0], - b = triangle[1], - c = triangle[2]; + if (config._d) { + return; + } - function sign (x) { - return x > 0 ? 1 : x < 0 ? -1 : 0; - } + currentDate = currentDateArray(config); - 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)); + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } - // 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); - }; + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); - /** - * 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 - */ - Graph3d.prototype._dataPointFromXY = function (x, y) { - var i, - distMax = 100, // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } - 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; - } + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); } - } - } - } - 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); - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; + // 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 closestDataPoint; - }; - - /** - * Display a tooltip for given data point - * @param {Object} dataPoint - * @private - */ - Graph3d.prototype._showTooltip = function (dataPoint) { - var content, line, dot; + // 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]; + } - 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)'; + config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + // Apply timezone offset from input. The actual zone can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm); + } + } - line = document.createElement('div'); - line.style.position = 'absolute'; - line.style.height = '40px'; - line.style.width = '0'; - line.style.borderLeft = '1px solid #4d4d4d'; + function dateFromObject(config) { + var normalizedInput; - 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'; + if (config._d) { + return; + } - 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; - } + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; - this._hideTooltip(); + dateFromConfig(config); + } - 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 + '
'; - } + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [ + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + ]; + } else { + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } + } - content.style.left = '0'; - content.style.top = '0'; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); + // date from string and format string + function makeDateFromStringAndFormat(config) { - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; + if (config._f === moment.ISO_8601) { + parseISO(config); + return; + } - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + config._a = []; + config._pf.empty = true; - 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'; - }; + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var lang = getLangDefinition(config._l), + string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; - /** - * Hide the tooltip when displayed - * @private - */ - Graph3d.prototype._hideTooltip = function () { - if (this.tooltip) { - this.tooltip.dataPoint = null; + tokens = expandFormat(config._f, lang).match(formattingTokens) || []; - 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); + 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) { + config._pf.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) { + config._pf.empty = false; + } + else { + config._pf.unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + config._pf.unusedTokens.push(token); + } } - } + + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); + } + + // handle am pm + if (config._isPm && config._a[HOUR] < 12) { + config._a[HOUR] += 12; + } + // if is 12 am, change hours to 0 + if (config._isPm === false && config._a[HOUR] === 12) { + config._a[HOUR] = 0; + } + + dateFromConfig(config); + checkOverflow(config); } - } - }; - /**--------------------------------------------------------------------------**/ + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } - /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x - */ - getMouseX = function(event) { - if ('clientX' in event) return event.clientX; - return event.targetTouches[0] && event.targetTouches[0].clientX || 0; - }; + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + bestMoment, - /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y - */ - getMouseY = function(event) { - if ('clientY' in event) return event.clientY; - return event.targetTouches[0] && event.targetTouches[0].clientY || 0; - }; + scoreToBeat, + i, + currentScore; - module.exports = Graph3d; + if (config._f.length === 0) { + config._pf.invalidFormat = true; + config._d = new Date(NaN); + return; + } + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = extend({}, config); + tempConfig._pf = defaultParsingFlags(); + tempConfig._f = config._f[i]; + makeDateFromStringAndFormat(tempConfig); -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { + if (!isValid(tempConfig)) { + continue; + } - var Point3d = __webpack_require__(9); + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; - /** - * @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 - */ - Camera = function () { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0); + tempConfig._pf.score = currentScore; - this.calculateCameraOrientation(); - }; + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } - /** - * 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; + extend(config, bestMoment || tempConfig); + } + + // date from iso format + function parseISO(config) { + var i, l, + string = config._i, + match = isoRegex.exec(string); - this.calculateCameraOrientation(); - }; + if (match) { + config._pf.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(parseTokenTimezone)) { + config._f += "Z"; + } + makeDateFromStringAndFormat(config); + } else { + config._isValid = false; + } + } - /** - * 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; - } + // date from iso format or fallback + function makeDateFromString(config) { + parseISO(config); + if (config._isValid === false) { + delete config._isValid; + moment.createFromInputFallback(config); + } + } - 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; - } + function makeDateFromInput(config) { + var input = config._i, + matched = aspNetJsonRegex.exec(input); - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); - } - }; + if (input === undefined) { + config._d = new Date(); + } else if (matched) { + config._d = new Date(+matched[1]); + } else if (typeof input === 'string') { + makeDateFromString(config); + } else if (isArray(input)) { + config._a = input.slice(0); + dateFromConfig(config); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof(input) === 'object') { + dateFromObject(config); + } else if (typeof(input) === 'number') { + // from milliseconds + config._d = new Date(input); + } else { + moment.createFromInputFallback(config); + } + } - /** - * 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; + function makeDate(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 rot; - }; + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } - /** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 - */ - Camera.prototype.setArmLength = function(length) { - if (length === undefined) - return; + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } - this.armLength = length; + function parseWeekday(input, language) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = language.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; + } - // 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.0) this.armLength = 5.0; + /************************************ + Relative Time + ************************************/ - this.calculateCameraOrientation(); - }; - /** - * Retrieve the arm length - * @return {Number} length - */ - Camera.prototype.getArmLength = function() { - return this.armLength; - }; + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { + return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } - /** - * Retrieve the camera location - * @return {Point3d} cameraLocation - */ - Camera.prototype.getCameraLocation = function() { - return this.cameraLocation; - }; + function relativeTime(milliseconds, withoutSuffix, lang) { + var seconds = round(Math.abs(milliseconds) / 1000), + minutes = round(seconds / 60), + hours = round(minutes / 60), + days = round(hours / 24), + years = round(days / 365), + args = seconds < relativeTimeThresholds.s && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < relativeTimeThresholds.m && ['mm', minutes] || + hours === 1 && ['h'] || + hours < relativeTimeThresholds.h && ['hh', hours] || + days === 1 && ['d'] || + days <= relativeTimeThresholds.dd && ['dd', days] || + days <= relativeTimeThresholds.dm && ['M'] || + days < relativeTimeThresholds.dy && ['MM', round(days / 30)] || + years === 1 && ['y'] || ['yy', years]; + args[2] = withoutSuffix; + args[3] = milliseconds > 0; + args[4] = lang; + return substituteTimeAgo.apply({}, args); + } - /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation - */ - Camera.prototype.getCameraRotation = function() { - return this.cameraRotation; - }; - /** - * 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); + /************************************ + Week of Year + ************************************/ - // calculate rotation of the camera - this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; - this.cameraRotation.y = 0; - this.cameraRotation.z = -this.armRotation.horizontal; - }; - module.exports = Camera; + // 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; -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { - var DataView = __webpack_require__(4); + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } - /** - * @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 - */ - function Filter (data, column, graph) { - this.data = data; - this.column = column; - this.graph = graph; // the parent graph + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } - this.index = undefined; - this.value = undefined; + adjustedMoment = moment(mom).add('d', daysToDayOfWeek); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; + } - // read all distinct values and select the first one - this.values = graph.getDistinctValues(data.get(), this.column); + //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 = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; - // sort both numeric and string values correctly - this.values.sort(function (a, b) { - return a > b ? 1 : a < b ? -1 : 0; - }); + 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 (this.values.length > 0) { - this.selectValue(0); - } + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } - // create an array with the filtered datapoints. this will be loaded afterwards - this.dataPoints = []; + /************************************ + Top Level Functions + ************************************/ - this.loaded = false; - this.onLoadCallback = undefined; + function makeMoment(config) { + var input = config._i, + format = config._f; - if (graph.animationPreload) { - this.loaded = false; - this.loadInBackground(); - } - else { - this.loaded = true; - } - }; + if (input === null || (format === undefined && input === '')) { + return moment.invalid({nullInput: true}); + } + if (typeof input === 'string') { + config._i = input = getLangDefinition().preparse(input); + } - /** - * Return the label - * @return {string} label - */ - Filter.prototype.isLoaded = function() { - return this.loaded; - }; + if (moment.isMoment(input)) { + config = cloneMoment(input); + config._d = new Date(+input._d); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } + } else { + makeDateFromInput(config); + } - /** - * Return the loaded progress - * @return {Number} percentage between 0 and 100 - */ - Filter.prototype.getLoadedProgress = function() { - var len = this.values.length; + return new Moment(config); + } - var i = 0; - while (this.dataPoints[i]) { - i++; - } + moment = function (input, format, lang, strict) { + var c; - return Math.round(i / len * 100); - }; + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._i = input; + c._f = format; + c._l = lang; + c._strict = strict; + c._isUTC = false; + c._pf = defaultParsingFlags(); + return makeMoment(c); + }; - /** - * Return the label - * @return {string} label - */ - Filter.prototype.getLabel = function() { - return this.graph.filterLabel; - }; + moment.suppressDeprecationWarnings = false; + moment.createFromInputFallback = deprecate( + "moment construction falls back to js Date. This is " + + "discouraged and will be removed in upcoming major " + + "release. Please refer to " + + "https://github.com/moment/moment/issues/1407 for more info.", + function (config) { + config._d = new Date(config._i); + }); - /** - * Return the columnIndex of the filter - * @return {Number} columnIndex - */ - Filter.prototype.getColumn = function() { - return this.column; - }; + // 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 moment(); + } + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (moments[i][fn](res)) { + res = moments[i]; + } + } + return res; + } - /** - * Return the currently selected value. Returns undefined if there is no selection - * @return {*} value - */ - Filter.prototype.getSelectedValue = function() { - if (this.index === undefined) - return undefined; + moment.min = function () { + var args = [].slice.call(arguments, 0); - return this.values[this.index]; - }; + return pickBy('isBefore', args); + }; - /** - * Retrieve all values of the filter - * @return {Array} values - */ - Filter.prototype.getValues = function() { - return this.values; - }; + moment.max = function () { + var args = [].slice.call(arguments, 0); - /** - * 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'; + return pickBy('isAfter', args); + }; - return this.values[index]; - }; + // creating with utc + moment.utc = function (input, format, lang, strict) { + var c; + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._useUTC = true; + c._isUTC = true; + c._l = lang; + c._i = input; + c._f = format; + c._strict = strict; + c._pf = defaultParsingFlags(); - /** - * Retrieve the (filtered) dataPoints for the currently selected filter index - * @param {Number} [index] (optional) - * @return {Array} dataPoints - */ - Filter.prototype._getDataPoints = function(index) { - if (index === undefined) - index = this.index; + return makeMoment(c).utc(); + }; - if (index === undefined) - return []; + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; - var dataPoints; - if (this.dataPoints[index]) { - dataPoints = this.dataPoints[index]; - } - else { - var f = {}; - f.column = this.column; - f.value = this.values[index]; + // duration + moment.duration = function (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso; - var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get(); - dataPoints = this.graph._getDataPoints(dataView); + if (moment.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 = aspNetTimeSpanJsonRegex.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 = isoDurationRegex.exec(input))) { + sign = (match[1] === "-") ? -1 : 1; + parseIso = function (inp) { + // 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; + }; + duration = { + y: parseIso(match[2]), + M: parseIso(match[3]), + d: parseIso(match[4]), + h: parseIso(match[5]), + m: parseIso(match[6]), + s: parseIso(match[7]), + w: parseIso(match[8]) + }; + } - this.dataPoints[index] = dataPoints; - } + ret = new Duration(duration); - return dataPoints; - }; + if (moment.isDuration(input) && input.hasOwnProperty('_lang')) { + ret._lang = input._lang; + } + return ret; + }; + // version number + moment.version = VERSION; - /** - * Set a callback function when the filter is fully loaded. - */ - Filter.prototype.setOnLoadCallback = function(callback) { - this.onLoadCallback = callback; - }; + // default format + moment.defaultFormat = isoFormat; + // constant that refers to the ISO standard + moment.ISO_8601 = function () {}; - /** - * Add a value to the list with available values for this filter - * No double entries will be created. - * @param {Number} index - */ - Filter.prototype.selectValue = function(index) { - if (index >= this.values.length) - throw 'Error: index out of range'; + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + moment.momentProperties = momentProperties; - this.index = index; - this.value = this.values[index]; - }; + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + moment.updateOffset = function () {}; - /** - * Load all filtered rows in the background one by one - * Start this method without providing an index! - */ - Filter.prototype.loadInBackground = function(index) { - if (index === undefined) - index = 0; + // This function allows you to set a threshold for relative time strings + moment.relativeTimeThreshold = function(threshold, limit) { + if (relativeTimeThresholds[threshold] === undefined) { + return false; + } + relativeTimeThresholds[threshold] = limit; + return true; + }; - var frame = this.graph.frame; + // This function will load languages and then set the global language. If + // no arguments are passed in, it will simply return the current global + // language key. + moment.lang = function (key, values) { + var r; + if (!key) { + return moment.fn._lang._abbr; + } + if (values) { + loadLang(normalizeLanguage(key), values); + } else if (values === null) { + unloadLang(key); + key = 'en'; + } else if (!languages[key]) { + getLangDefinition(key); + } + r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); + return r._abbr; + }; - if (index < this.values.length) { - var dataPointsTemp = this._getDataPoints(index); - //this.graph.redrawInfo(); // TODO: not neat + // returns language data + moment.langData = function (key) { + if (key && key._lang && key._lang._abbr) { + key = key._lang._abbr; + } + return getLangDefinition(key); + }; - // 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'; + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment || + (obj != null && obj.hasOwnProperty('_isAMomentObject')); + }; - var me = this; - setTimeout(function() {me.loadInBackground(index+1);}, 10); - this.loaded = false; - } - else { - this.loaded = true; + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; - // remove the progress box - if (frame.progress !== undefined) { - frame.removeChild(frame.progress); - frame.progress = undefined; + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); } - if (this.onLoadCallback) - this.onLoadCallback(); - } - }; + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; - module.exports = Filter; + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } + return m; + }; -/***/ }, -/* 8 */ -/***/ function(module, exports, __webpack_require__) { + moment.parseZone = function () { + return moment.apply(null, arguments).parseZone(); + }; - /** - * @prototype Point2d - * @param {Number} [x] - * @param {Number} [y] - */ - Point2d = function (x, y) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - }; + moment.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; - module.exports = Point2d; + /************************************ + Moment Prototype + ************************************/ -/***/ }, -/* 9 */ -/***/ function(module, exports, __webpack_require__) { + extend(moment.fn = Moment.prototype, { - /** - * @prototype Point3d - * @param {Number} [x] - * @param {Number} [y] - * @param {Number} [z] - */ - function Point3d(x, y, z) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - this.z = z !== undefined ? z : 0; - }; + clone : function () { + return moment(this); + }, - /** - * 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; - }; + valueOf : function () { + return +this._d + ((this._offset || 0) * 60000); + }, - /** - * 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; - }; + unix : function () { + return Math.floor(+this / 1000); + }, - /** - * 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 - ); - }; + toString : function () { + return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + }, - /** - * 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(); + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, - 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; + toISOString : function () { + var m = moment(this).utc(); + if (0 < m.year() && m.year() <= 9999) { + 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]'); + } + }, - return crossproduct; - }; + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, + isValid : function () { + return isValid(this); + }, - /** - * Rtrieve the length of the vector (or the distance from this point to the origin - * @return {Number} length - */ - Point3d.prototype.length = function() { - return Math.sqrt( - this.x * this.x + - this.y * this.y + - this.z * this.z - ); - }; + isDSTShifted : function () { - module.exports = Point3d; + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } + return false; + }, -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { + parsingFlags : function () { + return extend({}, this._pf); + }, - var util = __webpack_require__(1); + invalidAt: function () { + return this._pf.overflow; + }, - /** - * @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; + utc : function () { + return this.zone(0); + }, - 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); + local : function () { + this.zone(0); + this._isUTC = false; + return this; + }, - this.frame.prev = document.createElement('INPUT'); - this.frame.prev.type = 'BUTTON'; - this.frame.prev.value = 'Prev'; - this.frame.appendChild(this.frame.prev); + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.lang().postformat(output); + }, - this.frame.play = document.createElement('INPUT'); - this.frame.play.type = 'BUTTON'; - this.frame.play.value = 'Play'; - this.frame.appendChild(this.frame.play); + add : function (input, val) { + var dur; + // switch args to support add('s', 1) and add(1, 's') + if (typeof input === 'string' && typeof val === 'string') { + dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input); + } else if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, 1); + return this; + }, - this.frame.next = document.createElement('INPUT'); - this.frame.next.type = 'BUTTON'; - this.frame.next.value = 'Next'; - this.frame.appendChild(this.frame.next); + subtract : function (input, val) { + var dur; + // switch args to support subtract('s', 1) and subtract(1, 's') + if (typeof input === 'string' && typeof val === 'string') { + dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input); + } else if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, -1); + return this; + }, - 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); + diff : function (input, units, asFloat) { + var that = makeAs(input, this), + zoneDiff = (this.zone() - that.zone()) * 6e4, + diff, output; - 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); + units = normalizeUnits(units); - // 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);}; - } + if (units === 'year' || units === 'month') { + // average number of days in the months in the given dates + diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 + // difference in months + output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); + // adjust by taking difference in days, average number of days + // and dst in the given months. + output += ((this - moment(this).startOf('month')) - + (that - moment(that).startOf('month'))) / diff; + // same as above but with zones, to negate all dst + output -= ((this.zone() - moment(this).startOf('month').zone()) - + (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff; + if (units === 'year') { + output = output / 12; + } + } else { + diff = (this - that); + output = units === 'second' ? diff / 1e3 : // 1000 + units === 'minute' ? diff / 6e4 : // 1000 * 60 + units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + diff; + } + return asFloat ? output : absRound(output); + }, - this.onChangeCallback = undefined; + from : function (time, withoutSuffix) { + return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix); + }, - this.values = []; - this.index = undefined; + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; - } + calendar : function (time) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're zone'd or not. + var now = time || moment(), + sod = makeAs(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.lang().calendar(format, this)); + }, - /** - * Select the previous index - */ - Slider.prototype.prev = function() { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); - } - }; + isLeapYear : function () { + return isLeapYear(this.year()); + }, - /** - * Select the next index - */ - Slider.prototype.next = function() { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } - }; + isDST : function () { + return (this.zone() < this.clone().month(0).zone() || + this.zone() < this.clone().month(5).zone()); + }, - /** - * Select the next index - */ - Slider.prototype.playNext = function() { - var start = new Date(); + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.lang()); + return this.add({ d : input - day }); + } else { + return day; + } + }, - 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); - } + month : makeAccessor('Month', true), - var end = new Date(); - var diff = (end - start); + startOf: function (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case '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); + /* falls through */ + } - // 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 + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } - var me = this; - this.playTimeout = setTimeout(function() {me.playNext();}, interval); - }; + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } - /** - * Toggle start or stop playing - */ - Slider.prototype.togglePlay = function() { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); - } - }; + return this; + }, - /** - * Start playing - */ - Slider.prototype.play = function() { - // Test whether already playing - if (this.playTimeout) return; + endOf: function (units) { + units = normalizeUnits(units); + return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1); + }, - this.playNext(); + isAfter: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) > +moment(input).startOf(units); + }, - if (this.frame) { - this.frame.play.value = 'Stop'; - } - }; + isBefore: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) < +moment(input).startOf(units); + }, - /** - * Stop playing - */ - Slider.prototype.stop = function() { - clearInterval(this.playTimeout); - this.playTimeout = undefined; + isSame: function (input, units) { + units = units || 'ms'; + return +this.clone().startOf(units) === +makeAs(input, this).startOf(units); + }, - if (this.frame) { - this.frame.play.value = 'Play'; - } - }; + min: deprecate( + "moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548", + function (other) { + other = moment.apply(null, arguments); + return other < this ? this : other; + } + ), - /** - * 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; - }; + max: deprecate( + "moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548", + function (other) { + other = moment.apply(null, arguments); + return other > this ? this : other; + } + ), - /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds - */ - Slider.prototype.setPlayInterval = function(interval) { - this.playInterval = interval; - }; + // keepTime = true means only change the timezone, without affecting + // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200 + // It is possible that 5:31:26 doesn't exist int zone +0200, so we + // adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + zone : function (input, keepTime) { + var offset = this._offset || 0; + if (input != null) { + if (typeof input === "string") { + input = timezoneMinutesFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + this._offset = input; + this._isUTC = true; + if (offset !== input) { + if (!keepTime || this._changeInProgress) { + addOrSubtractDurationFromMoment(this, + moment.duration(offset - input, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + moment.updateOffset(this, true); + this._changeInProgress = null; + } + } + } else { + return this._isUTC ? offset : this._d.getTimezoneOffset(); + } + return this; + }, - /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds - */ - Slider.prototype.getPlayInterval = function(interval) { - return this.playInterval; - }; + zoneAbbr : function () { + return this._isUTC ? "UTC" : ""; + }, - /** - * 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; - }; + zoneName : function () { + return this._isUTC ? "Coordinated Universal Time" : ""; + }, + parseZone : function () { + if (this._tzm) { + this.zone(this._tzm); + } else if (typeof this._i === 'string') { + this.zone(this._i); + } + return this; + }, - /** - * Execute the onchange callback function - */ - Slider.prototype.onChange = function() { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); - } - }; + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).zone(); + } - /** - * 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'; + return (this.zone() - input) % 60 === 0; + }, - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = (left) + 'px'; - } - }; + daysInMonth : function () { + return daysInMonth(this.year(), this.month()); + }, + dayOfYear : function (input) { + var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); + }, - /** - * 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; + quarter : function (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + }, - if (this.values.length > 0) - this.setIndex(0); - else - this.index = undefined; - }; + weekYear : function (input) { + var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year; + return input == null ? year : this.add("y", (input - year)); + }, - /** - * Select a value by its index - * @param {Number} index - */ - Slider.prototype.setIndex = function(index) { - if (index < this.values.length) { - this.index = index; + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add("y", (input - year)); + }, - this.redraw(); - this.onChange(); - } - else { - throw 'Error: index out of range'; - } - }; + week : function (input) { + var week = this.lang().week(this); + return input == null ? week : this.add("d", (input - week) * 7); + }, - /** - * retrieve the index of the currently selected vaue - * @return {Number} index - */ - Slider.prototype.getIndex = function() { - return this.index; - }; + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add("d", (input - week) * 7); + }, + weekday : function (input) { + var weekday = (this.day() + 7 - this.lang()._week.dow) % 7; + return input == null ? weekday : this.add("d", input - weekday); + }, - /** - * retrieve the currently selected value - * @return {*} value - */ - Slider.prototype.get = function() { - return this.values[this.index]; - }; + isoWeekday : function (input) { + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + }, + isoWeeksInYear : function () { + return weeksInYear(this.year(), 1, 4); + }, - 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; + weeksInYear : function () { + var weekInfo = this._lang._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + }, - this.startClientX = event.clientX; - this.startSlideX = parseFloat(this.frame.slide.style.left); + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, - this.frame.style.cursor = 'move'; + set : function (units, value) { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + this[units](value); + } + return this; + }, - // 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', this.onmousemove); - util.addEventListener(document, 'mouseup', this.onmouseup); - util.preventDefault(event); - }; + // If passed a language key, it will set the language for this + // instance. Otherwise, it will return the language configuration + // variables for this instance. + lang : function (key) { + if (key === undefined) { + return this._lang; + } else { + this._lang = getLangDefinition(key); + return this; + } + } + }); + function rawMonthSetter(mom, value) { + var dayOfMonth; - Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; - var x = left - 3; + // TODO: Move this out of here! + if (typeof value === 'string') { + value = mom.lang().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } + } - 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; + dayOfMonth = Math.min(mom.date(), + daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } - return index; - }; + function rawGetter(mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - - this.frame.slide.clientWidth - 10; + function rawSetter(mom, unit, value) { + if (unit === 'Month') { + return rawMonthSetter(mom, value); + } else { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } + } - var x = index / (this.values.length-1) * width; - var left = x + 3; + function makeAccessor(unit, keepTime) { + return function (value) { + if (value != null) { + rawSetter(this, unit, value); + moment.updateOffset(this, keepTime); + return this; + } else { + return rawGetter(this, unit); + } + }; + } - return left; - }; + moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false); + moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false); + moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false); + // Setting the hour should keep the time, because the user explicitly + // specified which hour he wants. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true); + // moment.fn.month is defined separately + moment.fn.date = makeAccessor('Date', true); + moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true)); + moment.fn.year = makeAccessor('FullYear', true); + moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true)); + // add plural methods + moment.fn.days = moment.fn.day; + moment.fn.months = moment.fn.month; + moment.fn.weeks = moment.fn.week; + moment.fn.isoWeeks = moment.fn.isoWeek; + moment.fn.quarters = moment.fn.quarter; + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; - Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; + /************************************ + Duration Prototype + ************************************/ - var index = this.leftToIndex(x); - this.setIndex(index); + extend(moment.duration.fn = Duration.prototype, { - util.preventDefault(); - }; + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years; + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = 'auto'; + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; - // remove event listeners - util.removeEventListener(document, 'mousemove', this.onmousemove); - util.removeEventListener(document, 'mouseup', this.onmouseup); + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; - util.preventDefault(); - }; + hours = absRound(minutes / 60); + data.hours = hours % 24; - module.exports = Slider; + days += absRound(hours / 24); + data.days = days % 30; + months += absRound(days / 30); + data.months = months % 12; -/***/ }, -/* 11 */ -/***/ function(module, exports, __webpack_require__) { + years = absRound(months / 12); + data.years = years; + }, - /** - * @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, ...) - */ - function StepNumber(start, end, step, prettyStep) { - // set default values - this._start = 0; - this._end = 0; - this._step = 1; - this.prettyStep = true; - this.precision = 5; + weeks : function () { + return absRound(this.days() / 7); + }, - this._current = 0; - this.setRange(start, end, step, prettyStep); - }; + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; + }, - /** - * 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, ...) - */ - StepNumber.prototype.setRange = function(start, end, step, prettyStep) { - this._start = start ? start : 0; - this._end = end ? end : 0; + humanize : function (withSuffix) { + var difference = +this, + output = relativeTime(difference, !withSuffix, this.lang()); - this.setStep(step, prettyStep); - }; + if (withSuffix) { + output = this.lang().pastFuture(difference, output); + } - /** - * 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, ...) - */ - StepNumber.prototype.setStep = function(step, prettyStep) { - if (step === undefined || step <= 0) - return; + return this.lang().postformat(output); + }, - if (prettyStep !== undefined) - this.prettyStep = prettyStep; + add : function (input, val) { + // supports only 2.0-style add(1, 's') or add(moment) + var dur = moment.duration(input, val); - if (this.prettyStep === true) - this._step = StepNumber.calculatePrettyStep(step); - else - this._step = step; - }; + this._milliseconds += dur._milliseconds; + this._days += dur._days; + this._months += dur._months; - /** - * 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 - */ - StepNumber.calculatePrettyStep = function (step) { - var log10 = function (x) {return Math.log(x) / Math.LN10;}; + this._bubble(); - // 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))); + return this; + }, - // 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; + subtract : function (input, val) { + var dur = moment.duration(input, val); - // for safety - if (prettyStep <= 0) { - prettyStep = 1; - } + this._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; - return prettyStep; - }; + this._bubble(); - /** - * returns the current value of the step - * @return {Number} current value - */ - StepNumber.prototype.getCurrent = function () { - return parseFloat(this._current.toPrecision(this.precision)); - }; + return this; + }, - /** - * returns the current step size - * @return {Number} current step size - */ - StepNumber.prototype.getStep = function () { - return this._step; - }; + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); + }, - /** - * 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; - }; + as : function (units) { + units = normalizeUnits(units); + return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's'](); + }, - /** - * Do a step, add the step size to the current value - */ - StepNumber.prototype.next = function () { - this._current += this._step; - }; + lang : moment.fn.lang, - /** - * Returns true whether the end is reached - * @return {boolean} True if the current value has passed the end value. - */ - StepNumber.prototype.end = function () { - return (this._current > this._end); - }; + toIsoString : function () { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var years = Math.abs(this.years()), + months = Math.abs(this.months()), + days = Math.abs(this.days()), + hours = Math.abs(this.hours()), + minutes = Math.abs(this.minutes()), + seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); - module.exports = StepNumber; + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + return (this.asSeconds() < 0 ? '-' : '') + + 'P' + + (years ? years + 'Y' : '') + + (months ? months + 'M' : '') + + (days ? days + 'D' : '') + + ((hours || minutes || seconds) ? 'T' : '') + + (hours ? hours + 'H' : '') + + (minutes ? minutes + 'M' : '') + + (seconds ? seconds + 'S' : ''); + } + }); -/***/ }, -/* 12 */ -/***/ function(module, exports, __webpack_require__) { + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } - var Emitter = __webpack_require__(48); - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(15); - var Core = __webpack_require__(43); - var TimeAxis = __webpack_require__(27); - var CurrentTime = __webpack_require__(19); - var CustomTime = __webpack_require__(20); - var ItemSet = __webpack_require__(24); + function makeDurationAsGetter(name, factor) { + moment.duration.fn['as' + name] = function () { + return +this / factor; + }; + } - /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | Array | google.visualization.DataTable} [items] - * @param {Object} [options] See Timeline.setOptions for the available options. - * @constructor - */ - function Timeline (container, items, options) { - // mix the core properties in here - for (var coreProp in Core.prototype) { - if (Core.prototype.hasOwnProperty(coreProp) && !Timeline.prototype.hasOwnProperty(coreProp)) { - Timeline.prototype[coreProp] = Core.prototype[coreProp]; + for (i in unitMillisecondFactors) { + if (unitMillisecondFactors.hasOwnProperty(i)) { + makeDurationAsGetter(i, unitMillisecondFactors[i]); + makeDurationGetter(i.toLowerCase()); + } } - } - if (!(this instanceof Timeline)) { - throw new SyntaxError('Constructor must be called with the new operator'); - } + makeDurationAsGetter('Weeks', 6048e5); + moment.duration.fn.asMonths = function () { + return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12; + }; - var me = this; - this.defaultOptions = { - start: null, - end: null, - autoResize: true, + /************************************ + Default Lang + ************************************/ - orientation: 'bottom', - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); - // Create the DOM, props, and emitter - this._create(container); + // Set default language, other languages will inherit from English. + moment.lang('en', { + 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; + } + }); - // all components listed here will be repainted automatically - this.components = []; + /* EMBED_LANGUAGES */ - this.body = { - dom: this.dom, - domProps: this.props, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this) - }, - util: { - snap: null, // will be specified after TimeAxis is created - toScreen: me._toScreen.bind(me), - toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width - toTime: me._toTime.bind(me), - toGlobalTime : me._toGlobalTime.bind(me) - } - }; + /************************************ + Exposing Moment + ************************************/ - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; + function makeGlobal(shouldDeprecate) { + /*global ender:false */ + if (typeof ender !== 'undefined') { + return; + } + oldGlobalMoment = globalScope.moment; + if (shouldDeprecate) { + globalScope.moment = deprecate( + "Accessing Moment through the global scope is " + + "deprecated, and will be removed in an upcoming " + + "release.", + moment); + } else { + globalScope.moment = moment; + } + } - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); - this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); + // CommonJS module is defined + if (hasModule) { + module.exports = moment; + } else if (true) { + !(__WEBPACK_AMD_DEFINE_RESULT__ = (function (require, exports, module) { + if (module.config && module.config() && module.config().noGlobal === true) { + // release the global variable + globalScope.moment = oldGlobalMoment; + } - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + return moment; + }.call(exports, __webpack_require__, exports, module)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + makeGlobal(true); + } else { + makeGlobal(); + } + }).call(this); + + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(5)(module))) - // custom time bar - // Note: time bar will be attached in this.setOptions when selected - this.customTime = new CustomTime(this.body); - this.components.push(this.customTime); +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { - // item set - this.itemSet = new ItemSet(this.body); - this.components.push(this.itemSet); + var map = {}; + function webpackContext(req) { + return __webpack_require__(webpackContextResolve(req)); + }; + function webpackContextResolve(req) { + return map[req] || (function() { throw new Error("Cannot find module '" + req + "'.") }()); + }; + webpackContext.keys = function webpackContextKeys() { + return Object.keys(map); + }; + webpackContext.resolve = webpackContextResolve; + module.exports = webpackContext; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - // apply options - if (options) { - this.setOptions(options); - } +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { - // create itemset - if (items) { - this.setItems(items); - } - else { - this.redraw(); - } + module.exports = function(module) { + if(!module.webpackPolyfill) { + module.deprecate = function() {}; + module.paths = []; + // module.parent = undefined by default + module.children = []; + module.webpackPolyfill = 1; + } + return module; } - /** - * 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 - */ - Timeline.prototype.setOptions = function (options) { - if (options) { - // copy the known options - var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; - util.selectiveExtend(fields, this.options, options); - // enable/disable autoResize - this._initAutoResize(); - } +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { - // propagate options to all components - this.components.forEach(function (component) { - component.setOptions(options); - }); + // DOM utility methods - // TODO: remove deprecation error one day (deprecated since version 0.8.0) - if (options && options.order) { - throw new Error('Option order is deprecated. There is no replacement for this feature.'); + /** + * this prepares the JSON container for allocating SVG elements + * @param JSONcontainer + * @private + */ + 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 = []; + } } - - // redraw everything - this.redraw(); }; /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + * 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 */ - Timeline.prototype.setItems = function(items) { - var initialLoad = (this.itemsData == null); - - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } - else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; - } - else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: 'Date', - end: 'Date' + 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 = []; } - }); - } - - // set items - this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); - - if (initialLoad && ('start' in this.options || 'end' in this.options)) { - this.fit(); - - var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; - var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; - - this.setWindow(start, end); + } } }; /** - * Set groups - * @param {vis.DataSet | Array | google.visualization.DataTable} groups + * 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 */ - 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; + 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 { - // turn an array into a dataset - newDataSet = new DataSet(groups); + // 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); } - - this.groupsData = newDataSet; - this.itemSet.setGroups(newDataSet); - }; - - /** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {Array} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. - */ - Timeline.prototype.setSelection = function(ids) { - this.itemSet && this.itemSet.setSelection(ids); - }; - - /** - * 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() || []; + JSONcontainer[elementType].used.push(element); + return element; }; /** - * 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 + * 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 */ - Timeline.prototype.getItemRange = function() { - // calculate min from start filed - var dataset = this.itemsData.getDataSet(), - min = null, - max = null; - - if (dataset) { - // calculate the minimum value of the field 'start' - var minItem = dataset.min('start'); - min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; - // Note: we convert first to Date and then to number because else - // a conversion from ISODate to Number will fail - - // calculate maximum value of fields 'start' and 'end' - var maxStartItem = dataset.max('start'); - if (maxStartItem) { - max = util.convert(maxStartItem.start, 'Date').valueOf(); + exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer) { + 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(); } - var maxEndItem = dataset.max('end'); - if (maxEndItem) { - if (max == null) { - max = util.convert(maxEndItem.end, 'Date').valueOf(); - } - else { - max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); - } + else { + // create a new element and add it to the SVG + element = document.createElement(elementType); + DOMContainer.appendChild(element); } } - - return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null - }; + 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: []}; + DOMContainer.appendChild(element); + } + JSONcontainer[elementType].used.push(element); + return element; }; - module.exports = Timeline; - - -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { - var Emitter = __webpack_require__(48); - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(15); - var Core = __webpack_require__(43); - var TimeAxis = __webpack_require__(27); - var CurrentTime = __webpack_require__(19); - var CustomTime = __webpack_require__(20); - var LineGraph = __webpack_require__(26); /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | Array | google.visualization.DataTable} [items] - * @param {Object} [options] See Graph2d.setOptions for the available options. - * @constructor + * draw a point object. this is a seperate 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 group + * @param JSONcontainer + * @param svgContainer + * @returns {*} */ - function Graph2d (container, items, options, groups) { - for (var coreProp in Core.prototype) { - if (Core.prototype.hasOwnProperty(coreProp) && !Graph2d.prototype.hasOwnProperty(coreProp)) { - Graph2d.prototype[coreProp] = Core.prototype[coreProp]; - } + exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) { + var point; + if (group.options.drawPoints.style == 'circle') { + point = exports.getSVGElement('circle',JSONcontainer,svgContainer); + point.setAttributeNS(null, "cx", x); + point.setAttributeNS(null, "cy", y); + point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size); + point.setAttributeNS(null, "class", group.className + " point"); } - - var me = this; - this.defaultOptions = { - start: null, - end: null, - - autoResize: true, - - orientation: 'bottom', - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); - - // 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) - }, - util: { - snap: null, // will be specified after TimeAxis is created - toScreen: me._toScreen.bind(me), - toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width - toTime: me._toTime.bind(me), - toGlobalTime : me._toGlobalTime.bind(me) - } - }; - - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; - - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); - this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); - - // custom time bar - // Note: time bar will be attached in this.setOptions when selected - this.customTime = new CustomTime(this.body); - this.components.push(this.customTime); - - // item set - this.linegraph = new LineGraph(this.body); - this.components.push(this.linegraph); - - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet - - // apply options - if (options) { - this.setOptions(options); + else { + point = exports.getSVGElement('rect',JSONcontainer,svgContainer); + point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size); + point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size); + point.setAttributeNS(null, "width", group.options.drawPoints.size); + point.setAttributeNS(null, "height", group.options.drawPoints.size); + point.setAttributeNS(null, "class", group.className + " point"); } + return point; + }; - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + /** + * 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) { + // if (height != 0) { + 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); + // } + }; - // create itemset - if (items) { - this.setItems(items); - } - else { - this.redraw(); - } - } +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + var util = __webpack_require__(1); /** - * Set options. Options will be passed to all components loaded in the Graph2d. - * @param {Object} [options] - * {String} orientation - * Vertical orientation for the Graph2d, - * 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 Graph2d, a number in pixels or - * a css string like '400px' or '75%'. If undefined, - * The Graph2d will automatically size such that - * its contents fit. - * {String | Number} minHeight - * Minimum height for the Graph2d, a number in pixels or - * a css string like '400px' or '75%'. - * {String | Number} maxHeight - * Maximum height for the Graph2d, 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 + * 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 | DataTable} [data] Optional array with initial data + * @param {Object} [options] Available options: + * {String} fieldId Field name of the id in the + * items, 'id' by default. + * {Object. value ? value : min; - max = max == null ? value : max < value ? value : max; - } - } + var addOrUpdate = function (item) { + var id = item[fieldId]; + if (me._data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); + } + else { + // add new item + id = me._addItem(item); + addedIds.push(id); } - } - - return { - min: (min != null) ? new Date(min) : null, - max: (max != null) ? new Date(max) : null }; - }; - + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + addOrUpdate(data[i]); + } + } + else if (util.isDataTable(data)) { + // Google DataTable + var columns = this._getColumnNames(data); + for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { + var item = {}; + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + item[field] = data.getValue(row, col); + } - module.exports = Graph2d; + addOrUpdate(item); + } + } + else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } + else { + throw new Error('Unknown dataType'); + } + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); + } + if (updatedIds.length) { + this._trigger('update', {items: updatedIds}, senderId); + } -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { + return addedIds.concat(updatedIds); + }; /** - * @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. + * Get a data item or multiple items. * - * 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 + * Usage: * - * 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. + * get() + * get(options: Object) + * get(options: Object, data: Array | DataTable) * - * Version: 1.2 + * get(id: Number | String) + * get(id: Number | String, options: Object) + * get(id: Number | String, options: Object, data: Array | DataTable) * - * @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 + * get(ids: Number[] | String[]) + * get(ids: Number[] | String[], options: Object) + * get(ids: Number[] | String[], options: Object, data: Array | DataTable) + * + * Where: + * + * {Number | String} id The id of an item + * {Number[] | String{}} ids An array with ids of items + * {Object} options An Object with options. Available options: + * {String} [returnType] Type of data to be + * returned. Can be 'DataTable' or 'Array' (default) + * {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. + * {Array | DataTable} [data] If provided, items will be appended to this + * array or table. Required in case of Google + * DataTable. + * + * @throws Error */ - function DataStep(start, end, minimumStep, containerHeight, customRange) { - // variables - this.current = 0; - - this.autoScale = true; - this.stepIndex = 0; - this.step = 1; - this.scale = 1; - - this.marginStart; - this.marginEnd; - this.deadSpace = 0; - - this.majorSteps = [1, 2, 5, 10]; - this.minorSteps = [0.25, 0.5, 1, 2]; - - this.setRange(start, end, minimumStep, containerHeight, customRange); - } - + DataSet.prototype.get = function (args) { + var me = this; + // parse the arguments + var id, ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == 'String' || firstType == 'Number') { + // get(id [, options] [, data]) + id = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else if (firstType == 'Array') { + // get(ids [, options] [, data]) + ids = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } + else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } - /** - * 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; + // determine the return type + var returnType; + if (options && options.returnType) { + var allowedValues = ["DataTable", "Array", "Object"]; + returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; - if (start == end) { - this._start = start - 0.75; - this._end = end + 1; + if (data && (returnType != util.getType(data))) { + throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' + + 'does not correspond with specified options.type (' + options.type + ')'); + } + if (returnType == 'DataTable' && !util.isDataTable(data)) { + throw new Error('Parameter "data" must be a DataTable ' + + 'when options.type is "DataTable"'); + } } - - if (this.autoScale) { - this.setMinimumStep(minimumStep, containerHeight); + else if (data) { + returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; + } + else { + returnType = 'Array'; } - this.setFirst(customRange); - }; - - /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds - */ - DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) { - // round to floor - var size = this._end - this._start; - var safeSize = size * 1.2; - var minimumStepValue = minimumStep * (safeSize / containerHeight); - var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10); - var minorStepIdx = -1; - var magnitudefactor = Math.pow(10,orderOfMagnitude); + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], item, itemId, i, len; - var start = 0; - if (orderOfMagnitude < 0) { - start = orderOfMagnitude; + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, type); + if (filter && !filter(item)) { + item = null; + } } - - 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; + 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); } } - if (solutionFound == true) { - break; + } + 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); + } + } } } - 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 = {}; + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); } - var niceStart = customRange.min === undefined ? this._start - (this.scale * 2 * this.minorSteps[this.stepIndex]) : customRange.min; - var niceEnd = customRange.max === undefined ? this._end + (this.scale * this.minorSteps[this.stepIndex]) : customRange.max; - - this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; - this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; - this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; - this.marginRange = this.marginEnd - this.marginStart; - - this.current = this.marginEnd; - }; + // 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); + } + } + } - 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]); + // return the results + if (returnType == 'DataTable') { + var columns = this._getColumnNames(data); + if (id != undefined) { + // append a single item to the data table + me._appendRow(data, columns, item); + } + else { + // copy the items to the provided data table + for (i = 0; i < items.length; i++) { + me._appendRow(data, columns, items[i]); + } + } + return data; + } + else if (returnType == "Object") { + var result = {}; + for (i = 0; i < items.length; i++) { + result[items[i].id] = items[i]; + } + return result; } else { - return rounded; - } - } - - - /** - * 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); - }; - - /** - * Do the next step - */ - DataStep.prototype.next = function() { - var prev = this.current; - this.current -= this.step; - - // safety mechanism: if current time is still unchanged, move to the end - if (this.current == prev) { - this.current = this._end; + // return an array + if (id != undefined) { + // a single item + return item; + } + else { + // multiple items + if (data) { + // copy the items to the provided array + for (i = 0, len = items.length; i < len; i++) { + data.push(items[i]); + } + return data; + } + else { + // just return our array + return items; + } + } } }; /** - * Do the next step + * 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 */ - DataStep.prototype.previous = function() { - this.current += this.step; - this.marginEnd += this.step; - this.marginRange = this.marginEnd - this.marginStart; - }; + 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 = []; + 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); + } + } + } + this._sort(items, order); - /** - * Get the current datetime - * @return {String} current The current date - */ - DataStep.prototype.getCurrent = function() { - var toPrecision = '' + Number(this.current).toPrecision(5); - for (var i = toPrecision.length-1; i > 0; i--) { - if (toPrecision[i] == "0") { - toPrecision = toPrecision.slice(0,i); - } - else if (toPrecision[i] == "." || toPrecision[i] == ",") { - toPrecision = toPrecision.slice(0,i); - break; + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this._fieldId]; + } } - else{ - break; + 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]); + } + } - return toPrecision; - }; - - + this._sort(items, order); - /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate - */ - DataStep.prototype.snap = function(date) { + for (i = 0, len = items.length; i < len; i++) { + ids[i] = items[i][this._fieldId]; + } + } + else { + // create unordered list + for (id in data) { + if (data.hasOwnProperty(id)) { + item = data[id]; + ids.push(item[this._fieldId]); + } + } + } + } + return ids; }; /** - * 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. + * Returns the DataSet itself. Is overwritten for example by the DataView, + * which returns the DataSet it is connected to instead. */ - DataStep.prototype.isMajor = function() { - return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0); + DataSet.prototype.getDataSet = function () { + return this; }; - module.exports = DataStep; - - -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var hammerUtil = __webpack_require__(44); - var moment = __webpack_require__(40); - var Component = __webpack_require__(18); - /** - * @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 + * 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. */ - function Range(body, options) { - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add('days', -3).valueOf(); // Number - this.end = now.clone().add('days', 4).valueOf(); // Number - - this.body = body; - - // 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); - - this.props = { - touch: {} - }; - - // drag listeners for dragging - this.body.emitter.on('dragstart', this._onDragStart.bind(this)); - this.body.emitter.on('drag', this._onDrag.bind(this)); - this.body.emitter.on('dragend', this._onDragEnd.bind(this)); - - // ignore dragging when holding - this.body.emitter.on('hold', this._onHold.bind(this)); - - // mouse wheel for zooming - this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); - this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF - - // pinch to zoom - this.body.emitter.on('touch', this._onTouch.bind(this)); - this.body.emitter.on('pinch', this._onPinch.bind(this)); - - this.setOptions(options); - } - - Range.prototype = new Component(); + DataSet.prototype.forEach = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + data = this._data, + item, + id; - /** - * 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']; - util.selectiveExtend(fields, this.options, options); + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); - if ('start' in options || 'end' in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); + 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); + } + } } } }; /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' + * 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 */ - function validateDirection (direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + - 'Choose "horizontal" or "vertical".'); + DataSet.prototype.map = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + mappedItems = [], + data = this._data, + item; + + // 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)); + } + } } - } - /** - * Set a new start and end range - * @param {Number} [start] - * @param {Number} [end] - */ - Range.prototype.setRange = function(start, end) { - var changed = this._applyRange(start, end); - if (changed) { - var params = { - start: new Date(this.start), - end: new Date(this.end) - }; - this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); } + + return mappedItems; }; /** - * 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 + * Filter the fields of an item + * @param {Object} item + * @param {String[]} fields Field names + * @return {Object} filteredItem * @private */ - Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start, - newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end, - max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, - min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, - diff; - - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); - } + DataSet.prototype._filterFields = function (item, fields) { + var filteredItem = {}; - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; + for (var field in item) { + if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { + filteredItem[field] = item[field]; + } } - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = (min - newStart); - newStart += diff; - newEnd += diff; + return filteredItem; + }; - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } + /** + * 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'); + } + }; - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = (newEnd - max); - newStart -= diff; - newEnd -= diff; + /** + * 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; - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } + if (Array.isArray(id)) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); } } } - - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) === zoomMin) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the minimum - diff = (zoomMin - (newEnd - newStart)); - newStart -= diff / 2; - newEnd += diff / 2; - } + else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); } } - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } - if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) === zoomMax) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the maximum - diff = ((newEnd - newStart) - zoomMax); - newStart += diff / 2; - newEnd -= diff / 2; - } - } + if (removedIds.length) { + this._trigger('remove', {items: removedIds}, senderId); } - var changed = (this.start != newStart || this.end != newEnd); - - this.start = newStart; - this.end = newEnd; - - return changed; + return removedIds; }; /** - * Retrieve the current range. - * @return {Object} An object with start and end properties + * Remove an item by its id + * @param {Number | String | Object} id id or item + * @returns {Number | String | null} id + * @private */ - Range.prototype.getRange = function() { - return { - start: this.start, - end: this.end - }; + DataSet.prototype._remove = function (id) { + if (util.isNumber(id) || util.isString(id)) { + if (this._data[id]) { + delete this._data[id]; + return id; + } + } + else if (id instanceof Object) { + var itemId = id[this._fieldId]; + if (itemId && this._data[itemId]) { + delete this._data[itemId]; + return itemId; + } + } + return null; }; /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion + * Clear the data + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds The ids of all removed items */ - Range.prototype.conversion = function (width) { - return Range.conversion(this.start, this.end, width); + DataSet.prototype.clear = function (senderId) { + var ids = Object.keys(this._data); + + this._data = {}; + + this._trigger('remove', {items: ids}, senderId); + + return ids; }; /** - * 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 + * 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 */ - Range.conversion = function (start, end, width) { - if (width != 0 && (end - start != 0)) { - return { - offset: start, - scale: width / (end - start) + DataSet.prototype.max = function (field) { + var data = this._data, + max = null, + maxField = null; + + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!max || itemField > maxField)) { + max = item; + maxField = itemField; + } } } - else { - return { - offset: 0, - scale: 1 - }; - } + + return max; }; /** - * Start dragging horizontally or vertically - * @param {Event} event - * @private + * 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 */ - Range.prototype._onDragStart = function(event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; - - // 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; - - this.props.touch.start = this.start; - this.props.touch.end = this.end; + DataSet.prototype.min = function (field) { + var data = this._data, + min = null, + minField = null; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'move'; + for (var id in data) { + if (data.hasOwnProperty(id)) { + var item = data[id]; + var itemField = item[field]; + if (itemField != null && (!min || itemField < minField)) { + min = item; + minField = itemField; + } + } } + + return min; }; /** - * Perform dragging operation - * @param {Event} event - * @private + * 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. */ - Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; - var direction = this.options.direction; - validateDirection(direction); - // 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 delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, - interval = (this.props.touch.end - this.props.touch.start), - width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height, - diffRange = -delta / width * interval; - this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange); - this.body.emitter.emit('rangechange', { - start: new Date(this.start), - end: new Date(this.end) - }); + 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; + + 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 (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); + } + } + + return values; }; /** - * Stop dragging operation - * @param {event} event + * Add a single item. Will fail when an item with the same id already exists. + * @param {Object} item + * @return {String} id * @private */ - Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + DataSet.prototype._addItem = function (item) { + var id = item[this._fieldId]; - // 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 (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; + } - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'auto'; + 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; - // fire a rangechanged event - this.body.emitter.emit('rangechanged', { - start: new Date(this.start), - end: new Date(this.end) - }); + return id; }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event + * 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 */ - Range.prototype._onMouseWheel = function(event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + DataSet.prototype._getItem = function (id, types) { + var field, value; - // 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; + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; } - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - // perform the zoom action. Delta is normally 1 or -1 - - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - (delta / 5); + // 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 { - scale = 1 / (1 + (delta / 5)) ; + } + else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = value; + } } - - // calculate center, the date to zoom around - var gesture = hammerUtil.fakeGesture(this, event), - pointer = getPointer(gesture.center, this.body.dom.center), - pointerDate = this._pointerToDate(pointer); - - this.zoom(scale, pointerDate); } - - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); + return converted; }; /** - * Start of a touch gesture + * 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 */ - 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; + DataSet.prototype._updateItem = function (item) { + var id = item[this._fieldId]; + if (id == undefined) { + throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')'); + } + var d = this._data[id]; + if (!d) { + // item doesn't exist + throw new Error('Cannot update item: no item with id ' + id + ' found'); + } + + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + + return id; }; /** - * On start of a hold gesture + * Get an array with the column names of a Google DataTable + * @param {DataTable} dataTable + * @return {String[]} columnNames * @private */ - Range.prototype._onHold = function () { - this.props.touch.allowDragging = false; + DataSet.prototype._getColumnNames = function (dataTable) { + var columns = []; + for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { + columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); + } + return columns; }; /** - * Handle pinch event - * @param {Event} event + * Append an item as a row to the dataTable + * @param dataTable + * @param columns + * @param item * @private */ - Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; - - this.props.touch.allowDragging = false; - - if (event.gesture.touches.length > 1) { - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center); - } - - var scale = 1 / event.gesture.scale, - initDate = this._pointerToDate(this.props.touch.center); - - // calculate new start and end - var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale); - var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale); + DataSet.prototype._appendRow = function (dataTable, columns, item) { + var row = dataTable.addRow(); - // apply new range - this.setRange(newStart, newEnd); + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + dataTable.setValue(row, col, item[field]); } }; - /** - * 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; + module.exports = DataSet; - validateDirection(direction); - if (direction == 'horizontal') { - var width = this.body.domProps.center.width; - conversion = this.conversion(width); - return pointer.x / conversion.scale + conversion.offset; - } - else { - var height = this.body.domProps.center.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; - } - }; +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); /** - * Get the pointer location relative to the location of the dom element - * @param {{pageX: Number, pageY: Number}} touch - * @param {Element} element HTML DOM element - * @return {{x: Number, y: Number}} pointer - * @private + * 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 getPointer (touch, element) { - return { - x: touch.pageX - util.getAbsoluteLeft(element), - y: touch.pageY - util.getAbsoluteTop(element) + function DataView (data, options) { + this._data = null; + this._ids = {}; // ids of the items currently in memory (just contains a boolean true) + this._options = options || {}; + this._fieldId = 'id'; // name of the field containing id + this._subscribers = {}; // event subscribers + + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); }; + + this.setData(data); } + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly + /** - * 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. + * Set a data source for the view + * @param {DataSet | DataView} data */ - Range.prototype.zoom = function(scale, center) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } + DataView.prototype.setData = function (data) { + var ids, i, len; - // calculate new start and end - var newStart = center + (this.start - center) * scale; - var newEnd = center + (this.end - center) * scale; + if (this._data) { + // unsubscribe from current dataset + if (this._data.unsubscribe) { + this._data.unsubscribe('*', this.listener); + } - this.setRange(newStart, newEnd); - }; + // trigger a remove of all items in memory + ids = []; + for (var id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + ids.push(id); + } + } + this._ids = {}; + this._trigger('remove', {items: ids}); + } - /** - * Move the range with a given delta to the left or right. Start and end - * value will be adjusted. For example, try delta = 0.1 or -0.1 - * @param {Number} delta Moving amount. Positive value will move right, - * negative value will move left - */ - Range.prototype.move = function(delta) { - // zoom start Date and end Date relative to the centerDate - var diff = (this.end - this.start); + this._data = data; - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || + (this._data && this._data.options && this._data.options.fieldId) || + 'id'; - // TODO: reckon with min and max range + // trigger an add of all added items + ids = this._data.getIds({filter: this._options && this._options.filter}); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this._ids[id] = true; + } + this._trigger('add', {items: ids}); - this.start = newStart; - this.end = newEnd; + // subscribe to new dataset + if (this._data.on) { + this._data.on('*', this.listener); + } + } }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * 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 */ - Range.prototype.moveTo = function(moveTo) { - var center = (this.start + this.end) / 2; + DataView.prototype.get = function (args) { + var me = this; - var diff = center - moveTo; + // 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]; + } - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); - this.setRange(newStart, newEnd); + // 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); }; - module.exports = Range; + /** + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids + */ + DataView.prototype.getIds = function (options) { + var ids; + if (this._data) { + var defaultFilter = this._options.filter; + var filter; -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + } + } + else { + filter = options.filter; + } + } + else { + filter = defaultFilter; + } - // Utility functions for ordering and stacking of items - var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors + ids = this._data.getIds({ + filter: filter, + order: options && options.order + }); + } + else { + ids = []; + } - /** - * 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; - }); + return ids; }; /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items + * 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 */ - 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; - - return aTime - bTime; - }); + DataView.prototype.getDataSet = function () { + var dataSet = this; + while (dataSet instanceof DataView) { + dataSet = dataSet._data; + } + return dataSet || null; }; /** - * 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 + * 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 */ - exports.stack = function(items, margin, force) { - var i, iMax; + DataView.prototype._onEvent = function (event, params, senderId) { + var i, len, id, item, + ids = params && params.items, + data = this._data, + added = [], + updated = [], + removed = []; - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } - } + 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); + } + } - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.top === null) { - // initialize top position - item.top = margin.axis; + break; - 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 && exports.collision(item, other, margin.item)) { - collidingItem = other; - break; + case 'update': + // determine the event from the views viewpoint: an updated + // item can be added, updated, or removed from this view. + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + + if (item) { + if (this._ids[id]) { + updated.push(id); + } + else { + this._ids[id] = true; + added.push(id); + } + } + else { + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } + else { + // nothing interesting for me :-( + } } } - if (collidingItem != null) { - // There is a collision. Reposition the items above the colliding element - item.top = collidingItem.top + collidingItem.height + margin.item.vertical; + break; + + case 'remove': + // filter the ids of the removed items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } } - } while (collidingItem); + + break; + } + + if (added.length) { + this._trigger('add', {items: added}, senderId); + } + if (updated.length) { + this._trigger('update', {items: updated}, senderId); + } + if (removed.length) { + this._trigger('remove', {items: removed}, senderId); } } }; - /** - * 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) { - var i, iMax; + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = margin.axis; - } - }; + // 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; + + module.exports = DataView; + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + var Emitter = __webpack_require__(10); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(8); + var util = __webpack_require__(1); + var Point3d = __webpack_require__(11); + var Point2d = __webpack_require__(12); + var Camera = __webpack_require__(13); + var Filter = __webpack_require__(14); + var Slider = __webpack_require__(15); + var StepNumber = __webpack_require__(16); /** - * 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 + * @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] */ - 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); - }; + function Graph3d(container, data, options) { + if (!(this instanceof Graph3d)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } + // 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%'; -/***/ }, -/* 17 */ -/***/ function(module, exports, __webpack_require__) { + this.xLabel = 'x'; + this.yLabel = 'y'; + this.zLabel = 'z'; + this.filterLabel = 'time'; + this.legendLabel = 'value'; + + 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' - var moment = __webpack_require__(40); + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - /** - * @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) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); + this.camera = new Camera(); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - this.autoScale = true; - this.scale = TimeStep.SCALE.DAY; - this.step = 1; + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects - // initialize the range - this.setRange(start, end, minimumStep); - } + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; - /// enum scale - TimeStep.SCALE = { - MILLISECOND: 1, - SECOND: 2, - MINUTE: 3, - HOUR: 4, - DAY: 5, - WEEKDAY: 6, - MONTH: 7, - YEAR: 8 - }; + 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 + // constants + this.colorAxis = '#4D4D4D'; + this.colorGrid = '#D3D3D3'; + this.colorDot = '#7DC1FF'; + this.colorDotBorder = '#3267D2'; - /** - * 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"; - } + // create a frame and canvas + this.create(); - this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); - this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + // apply options (also when undefined) + this.setOptions(options); - if (this.autoScale) { - this.setMinimumStep(minimumStep); + // apply data + if (data) { + this.setData(data); } - }; + } - /** - * Set the range iterator to the start date. - */ - TimeStep.prototype.first = function() { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); - }; + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); /** - * Round the current date to the first minor date value - * This must be executed once when the current date is set to start Date + * Calculate the scaling values, dependent on the range in x, y, and z direction */ - TimeStep.prototype.roundToMinor = function() { - // round to floor - // IMPORTANT: we have no breaks in this switch! (this is no bug) - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.YEAR: - this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); - this.current.setMonth(0); - case TimeStep.SCALE.MONTH: this.current.setDate(1); - case TimeStep.SCALE.DAY: // intentional fall through - case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); - case TimeStep.SCALE.HOUR: this.current.setMinutes(0); - case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); - case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); - //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds - } + Graph3d.prototype._setScale = function() { + this.scale = new Point3d(1 / (this.xMax - this.xMin), + 1 / (this.yMax - this.yMin), + 1 / (this.zMax - this.zMin)); - if (this.step != 1) { - // round down to the first minor value that is a multiple of the current step size - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; - default: break; + // 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; } } + + // 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); }; + /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date + * 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 */ - TimeStep.prototype.hasNext = function () { - return (this.current.valueOf() <= this._end.valueOf()); + Graph3d.prototype._convert3Dto2D = function(point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); }; /** - * Do the next step + * 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 */ - TimeStep.prototype.next = function() { - var prev = this.current.valueOf(); - - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - - this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; - case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; - case TimeStep.SCALE.HOUR: - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - (h % this.step)); - break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; - } - } - else { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; - case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; - case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; - case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; - case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; - default: break; - } - } + Graph3d.prototype._convertPointToTranslation = function(point3d) { + var ax = point3d.x * this.scale.x, + ay = point3d.y * this.scale.y, + az = point3d.z * this.scale.z, - if (this.step != 1) { - // round down to the correct major value - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; - case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; - case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; - case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; - case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; - case TimeStep.SCALE.YEAR: break; // nothing to do for year - default: break; - } - } + cx = this.camera.getCameraLocation().x, + cy = this.camera.getCameraLocation().y, + cz = this.camera.getCameraLocation().z, - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); - } - }; + // 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)); - /** - * Get the current datetime - * @return {Date} current The current date - */ - TimeStep.prototype.getCurrent = function() { - return this.current; + return new Point3d(dx, dy, dz); }; /** - * Set a custom scale. Autoscaling will be disabled. - * For example setScale(SCALE.MINUTES, 5) will result - * in minor steps of 5 minutes, and major steps of an hour. - * - * @param {TimeStep.SCALE} newScale - * A scale. Choose from SCALE.MILLISECOND, - * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, - * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, - * SCALE.YEAR. - * @param {Number} newStep A step size, by default 1. Choose for - * example 1, 2, 5, or 10. + * 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 */ - TimeStep.prototype.setScale = function(newScale, newStep) { - this.scale = newScale; + 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; - if (newStep > 0) { - this.step = newStep; + // 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()); } - this.autoScale = false; + // 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); }; /** - * Enable or disable autoscaling - * @param {boolean} enable If true, autoascaling is set true + * Set the background styling for the graph + * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ - TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; + Graph3d.prototype._setBackgroundColor = function(backgroundColor) { + var fill = 'white'; + var stroke = 'gray'; + var strokeWidth = 1; + + 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) { + // use use defaults + } + 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'; }; + /// 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 + }; + /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds + * 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 */ - TimeStep.prototype.setMinimumStep = function(minimumStep) { - if (minimumStep == undefined) { - return; + 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; } - var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); - var stepMonth = (1000 * 60 * 60 * 24 * 30); - var stepDay = (1000 * 60 * 60 * 24); - var stepHour = (1000 * 60 * 60); - var stepMinute = (1000 * 60); - var stepSecond = (1000); - var stepMillisecond= (1); - - // find the smallest step that is larger than the provided minimumStep - if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} - if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} - if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} - if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} - if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} - if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} - if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} - if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} - if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} - if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} - if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} - if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} - if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} - if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} - if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} - if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} - if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} - if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} - if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} - if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} - if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} - if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} - if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} - if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} - if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} - if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} - if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} - if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} - if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} + return -1; }; /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ - TimeStep.prototype.snap = function(date) { - var clone = new Date(date.valueOf()); - - if (this.scale == TimeStep.SCALE.YEAR) { - var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); - clone.setFullYear(Math.round(year / this.step) * this.step); - clone.setMonth(0); - clone.setDate(0); - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.MONTH) { - if (clone.getDate() > 15) { - clone.setDate(1); - clone.setMonth(clone.getMonth() + 1); - // important: first set Date to 1, after that change the month. - } - else { - clone.setDate(1); - } - - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.DAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 24) * 24); break; - default: - clone.setHours(Math.round(clone.getHours() / 12) * 12); break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.WEEKDAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.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 (this.scale == TimeStep.SCALE.HOUR) { - switch (this.step) { - case 4: - clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; - default: - clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; - } - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (this.scale == TimeStep.SCALE.MINUTE) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); - clone.setSeconds(0); - break; - case 5: - clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; - default: - clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; + 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; + + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; } - clone.setMilliseconds(0); } - else if (this.scale == TimeStep.SCALE.SECOND) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); - clone.setMilliseconds(0); - break; - case 5: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; - default: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; + else if (this.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; + + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; } } - else if (this.scale == TimeStep.SCALE.MILLISECOND) { - var step = this.step > 5 ? this.step / 2 : 1; - clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step); + else { + throw 'Unknown style "' + this.style + '"'; } - - return clone; }; - /** - * Check if the current value is a major value (for example when the step - * is DAY, a major value is each first day of the MONTH) - * @return {boolean} true if current date is major, else false. - */ - TimeStep.prototype.isMajor = function() { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - return (this.current.getMilliseconds() == 0); - case TimeStep.SCALE.SECOND: - return (this.current.getSeconds() == 0); - case TimeStep.SCALE.MINUTE: - return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); - // Note: this is no bug. Major label is equal for both minute and hour scale - case TimeStep.SCALE.HOUR: - return (this.current.getHours() == 0); - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: - return (this.current.getDate() == 1); - case TimeStep.SCALE.MONTH: - return (this.current.getMonth() == 0); - case TimeStep.SCALE.YEAR: - return false; - default: - return false; + Graph3d.prototype.getNumberOfRows = function(data) { + return data.length; + } + + + Graph3d.prototype.getNumberOfColumns = function(data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; + } } - }; + return counter; + } - /** - * 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; + 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; + } - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); - case TimeStep.SCALE.SECOND: return moment(date).format('s'); - case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); - case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); - case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); - case TimeStep.SCALE.DAY: return moment(date).format('D'); - case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); - case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); - default: return ''; + + 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; }; - /** - * 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 + * 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 */ - TimeStep.prototype.getLabelMajor = function(date) { - if (date == undefined) { - date = this.current; + Graph3d.prototype._dataInitialize = function (rawData, style) { + var me = this; + + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off('*', this._onChange); } - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); - case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); - case TimeStep.SCALE.MINUTE: - case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); - case TimeStep.SCALE.WEEKDAY: - case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); - case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); - case TimeStep.SCALE.YEAR: return ''; - default: return ''; + if (rawData === undefined) + return; + + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); } - }; - module.exports = TimeStep; + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } + else { + throw new Error('Array, DataSet, or DataView expected'); + } + if (data.length == 0) + return; -/***/ }, -/* 18 */ -/***/ function(module, exports, __webpack_require__) { + this.dataSet = rawData; + this.dataTable = data; - /** - * Prototype for visual components - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] - * @param {Object} [options] - */ - function Component (body, options) { - this.options = null; - this.props = null; - } + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on('*', this._onChange); - /** - * 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); + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange + + // 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'; + + + + // 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();}); + } + } + + + var withBars = this.style == Graph3d.STYLE.BAR || + this.style == Graph3d.STYLE.BARCOLOR || + this.style == Graph3d.STYLE.BARSIZE; + + // 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; + } + + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } + else { + var dataY = this.getDistinctValues(data,this.colY); + this.yBarWidth = (dataY[1] - dataY[0]) || 1; + } + } + + // 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 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; + + 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; + + 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; } - }; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - Component.prototype.redraw = function() { - // should be implemented by the component - return false; + // set the scale dependent on the ranges. + this._setScale(); }; - /** - * Destroy the component. Cleanup DOM and event listeners - */ - Component.prototype.destroy = function() { - // should be implemented by the component - }; + /** - * Test whether the component is resized since the last time _isResized() was - * called. - * @return {Boolean} Returns true if the component is resized - * @protected + * Filter the data based on the current filter + * @param {Array} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen */ - Component.prototype._isResized = function() { - var resized = (this.props._previousWidth !== this.props.width || - this.props._previousHeight !== this.props.height); + 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; - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; + var dataPoints = []; - return resized; - }; + 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 - module.exports = Component; + // 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; + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } + } -/***/ }, -/* 19 */ -/***/ function(module, exports, __webpack_require__) { + function sortNumber(a, b) { + return a - b; + } + dataX.sort(sortNumber); + dataY.sort(sortNumber); - var util = __webpack_require__(1); - var Component = __webpack_require__(18); - var moment = __webpack_require__(40); - var locales = __webpack_require__(42); + // 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; - /** - * 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; + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - // default options - this.defaultOptions = { - showCurrentTime: true, + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - locales: locales, - locale: 'en' - }; - this.options = util.extend({}, this.defaultOptions); + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - this._create(); + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); - this.setOptions(options); - } + dataMatrix[xIndex][yIndex] = obj; - CurrentTime.prototype = new Component(); + 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 (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; + + dataPoints.push(obj); + } + } + + return dataPoints; + }; /** - * Create the HTML DOM for the current time bar - * @private + * 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. */ - CurrentTime.prototype._create = function() { - var bar = document.createElement('div'); - bar.className = 'currenttime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } - this.bar = bar; + 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 (event) {me._onMouseDown(event);}; + var ontouchstart = function (event) {me._onTouchStart(event);}; + var onmousewheel = function (event) {me._onWheel(event);}; + var ontooltip = function (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); }; + /** - * Destroy the CurrentTime bar + * 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%') */ - CurrentTime.prototype.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing + Graph3d.prototype.setSize = function(width, height) { + this.frame.style.width = width; + this.frame.style.height = height; - this.body = null; + this._resizeCanvas(); }; /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] + * Resize the canvas to the current size of the frame */ - CurrentTime.prototype.setOptions = function(options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); - } + Graph3d.prototype._resizeCanvas = function() { + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; + + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; + + // adjust with for margin + this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px'; }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Start animation */ - 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.animationStart = function() { + if (!this.frame.filter || !this.frame.filter.slider) + throw 'No animation available'; - this.start(); - } + this.frame.filter.slider.play(); + }; - var now = new Date(); - var x = this.body.util.toScreen(now); - var locale = this.options.locales[this.options.locale]; - var title = locale.current + ' ' + locale.time + ': ' + moment(now).format('dddd, MMMM Do YYYY, H:mm:ss'); - title = title.charAt(0).toUpperCase() + title.substring(1); + /** + * Stop animation + */ + Graph3d.prototype.animationStop = function() { + if (!this.frame.filter || !this.frame.filter.slider) return; - this.bar.style.left = x + 'px'; - this.bar.title = title; + this.frame.filter.slider.stop(); + }; + + + /** + * 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 { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - this.stop(); + this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px } - return false; + // 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 + } }; /** - * Start auto refreshing the current time bar + * 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. */ - CurrentTime.prototype.start = function() { - var me = this; - - 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; + Graph3d.prototype.setCameraPosition = function(pos) { + if (pos === undefined) { + return; + } - me.redraw(); + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); + } - // start a timer to adjust for the new time - me.currentTimeTimer = setTimeout(update, interval); + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); } - update(); + this.redraw(); }; + /** - * Stop auto refreshing the current time bar + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance */ - CurrentTime.prototype.stop = function() { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; - } + Graph3d.prototype.getCameraPosition = function() { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; }; - module.exports = CurrentTime; + /** + * Load data into the 3D Graph + */ + Graph3d.prototype._readData = function(data) { + // read the data + this._dataInitialize(data, this.style); -/***/ }, -/* 20 */ -/***/ function(module, exports, __webpack_require__) { + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } + else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); + } - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); - var Component = __webpack_require__(18); - var moment = __webpack_require__(40); - var locales = __webpack_require__(42); + // draw the filter + this._redrawFilter(); + }; /** - * A custom time bar - * @param {{range: Range, dom: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCustomTime] - * @constructor CustomTime - * @extends Component + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data */ + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); - function CustomTime (body, options) { - this.body = body; + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + }; - // default options - this.defaultOptions = { - showCustomTime: false, - locales: locales, - locale: 'en' - }; - this.options = util.extend({}, this.defaultOptions); + /** + * Update the options. Options will be merged with current options + * @param {Object} options + */ + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - this.customTime = new Date(); - this.eventParams = {}; // stores state parameters while dragging the bar + this.animationStop(); - // create the DOM - this._create(); + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; - this.setOptions(options); - } + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - CustomTime.prototype = new Component(); + 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; - /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCustomTime] - */ - CustomTime.prototype.setOptions = function(options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['showCustomTime', 'locale', 'locales'], this.options, options); - } - }; + 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; - /** - * Create the DOM for the custom time - * @private - */ - CustomTime.prototype._create = function() { - var bar = document.createElement('div'); - bar.className = 'customtime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; - this.bar = bar; + 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 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 (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - // attach event listeners - this.hammer = Hammer(bar, { - prevent_default: true - }); - this.hammer.on('dragstart', this._onDragStart.bind(this)); - this.hammer.on('drag', this._onDrag.bind(this)); - this.hammer.on('dragend', this._onDragEnd.bind(this)); - }; + if (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; - /** - * Destroy the CustomTime bar - */ - CustomTime.prototype.destroy = function () { - this.options.showCustomTime = false; - this.redraw(); // will remove the bar from the DOM + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + + if (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); + } + else { + this.camera.setArmRotation(1.0, 0.5); + this.camera.setArmLength(1.7); + } + } - this.hammer.enable(false); - this.hammer = null; + this._setBackgroundColor(options && options.backgroundColor); - this.body = null; + this.setSize(this.width, this.height); + + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); + } + + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Redraw the Graph. */ - CustomTime.prototype.redraw = function () { - if (this.options.showCustomTime) { - 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 x = this.body.util.toScreen(this.customTime); + Graph3d.prototype.redraw = function() { + if (this.dataPoints === undefined) { + throw 'Error: graph data not initialized'; + } - var locale = this.options.locales[this.options.locale]; - var title = locale.time + ': ' + moment(this.customTime).format('dddd, MMMM Do YYYY, H:mm:ss'); - title = title.charAt(0).toUpperCase() + title.substring(1); + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); - this.bar.style.left = x + 'px'; - this.bar.title = title; + 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 { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); } - return false; + this._redrawInfo(); + this._redrawLegend(); }; /** - * Set custom time. - * @param {Date} time + * Clear the canvas before redrawing */ - CustomTime.prototype.setCustomTime = function(time) { - this.customTime = new Date(time.valueOf()); - this.redraw(); - }; + Graph3d.prototype._redrawClear = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); - /** - * Retrieve the current custom time. - * @return {Date} customTime - */ - CustomTime.prototype.getCustomTime = function() { - return new Date(this.customTime.valueOf()); + ctx.clearRect(0, 0, canvas.width, canvas.height); }; - /** - * Start moving horizontally - * @param {Event} event - * @private - */ - CustomTime.prototype._onDragStart = function(event) { - this.eventParams.dragging = true; - this.eventParams.customTime = this.customTime; - - event.stopPropagation(); - event.preventDefault(); - }; /** - * Perform moving operating. - * @param {Event} event - * @private + * Redraw the legend showing the colors */ - CustomTime.prototype._onDrag = function (event) { - if (!this.eventParams.dragging) return; - - var deltaX = event.gesture.deltaX, - x = this.body.util.toScreen(this.eventParams.customTime) + deltaX, - time = this.body.util.toTime(x); - - this.setCustomTime(time); + Graph3d.prototype._redrawLegend = function() { + var y; - // fire a timechange event - this.body.emitter.emit('timechange', { - time: new Date(this.customTime.valueOf()) - }); + if (this.style === Graph3d.STYLE.DOTCOLOR || + this.style === Graph3d.STYLE.DOTSIZE) { - event.stopPropagation(); - event.preventDefault(); - }; + var dotSize = this.frame.clientWidth * 0.02; - /** - * Stop moving operating. - * @param {event} event - * @private - */ - CustomTime.prototype._onDragEnd = function (event) { - if (!this.eventParams.dragging) return; + 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 + } - // fire a timechanged event - this.body.emitter.emit('timechanged', { - time: new Date(this.customTime.valueOf()) - }); + 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; + } - event.stopPropagation(); - event.preventDefault(); - }; + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + ctx.lineWidth = 1; + ctx.font = '14px arial'; // TODO: put in options - module.exports = CustomTime; + 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); + //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); -/***/ }, -/* 21 */ -/***/ function(module, exports, __webpack_require__) { + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var Component = __webpack_require__(18); - var DataStep = __webpack_require__(14); + ctx.strokeStyle = this.colorAxis; + ctx.strokeRect(left, top, widthMax, height); + } - /** - * 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) { - this.id = util.randomUUID(); - this.body = body; + if (this.style === Graph3d.STYLE.DOTSIZE) { + // draw border around color bar + ctx.strokeStyle = this.colorAxis; + ctx.fillStyle = this.colorDot; + 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(); + } - this.defaultOptions = { - orientation: 'left', // supported: 'left', 'right' - showMinorLabels: true, - showMajorLabels: true, - icons: true, - majorLinesOffset: 7, - minorLinesOffset: 4, - labelOffsetX: 10, - labelOffsetY: 2, - iconWidth: 20, - width: '40px', - visible: true, - customRange: { - left: {min:undefined, max:undefined}, - right: {min:undefined, max:undefined} + 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(); } - }; - - this.linegraphSVG = svg; - this.props = {}; - this.DOMelements = { // dynamic elements - lines: {}, - labels: {} - }; - - this.dom = {}; + while (!step.end()) { + y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height; - this.range = {start:0, end:0}; + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); - this.options = util.extend({}, this.defaultOptions); - this.conversionFactor = 1; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - this.setOptions(options); - this.width = Number(('' + this.options.width).replace("px","")); - this.minWidth = this.width; - this.height = this.linegraphSVG.offsetHeight; + step.next(); + } - this.stepPixels = 25; - this.stepPixelsForced = 25; - this.lineOffset = 0; - this.master = true; - this.svgElements = {}; + ctx.textAlign = 'right'; + ctx.textBaseline = 'top'; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); + } + }; + /** + * Redraw the filter + */ + Graph3d.prototype._redrawFilter = function() { + this.frame.filter.innerHTML = ''; - this.groups = {}; - this.amountOfGroups = 0; + if (this.dataFilter) { + var options = { + 'visible': this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; - // create the HTML DOM - this._create(); - } + // TODO: css here is not nice here... + this.frame.filter.style.padding = '10px'; + //this.frame.filter.style.backgroundColor = '#EFEFEF'; - DataAxis.prototype = new Component(); + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); + // create an event handler + var me = this; + var onchange = function () { + var index = slider.getIndex(); + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); - DataAxis.prototype.addGroup = function(label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } + else { + this.frame.filter.slider = undefined; } - this.amountOfGroups += 1; }; - DataAxis.prototype.updateGroup = function(label, graphOptions) { - this.groups[label] = graphOptions; + /** + * Redraw the slider + */ + Graph3d.prototype._redrawSlider = function() { + if ( this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); + } }; - DataAxis.prototype.removeGroup = function(label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; + + /** + * Redraw common information + */ + Graph3d.prototype._redrawInfo = function() { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + + ctx.font = '14px arial'; // TODO: put in options + ctx.lineStyle = 'gray'; + ctx.fillStyle = 'gray'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y); } }; - DataAxis.prototype.setOptions = function (options) { - if (options) { - var redraw = false; - if (this.options.orientation != options.orientation && options.orientation !== undefined) { - redraw = true; + /** + * 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; + + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + 'px arial'; + + // 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; + + // 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(); + + 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.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); } - var fields = [ - 'orientation', - 'showMinorLabels', - 'showMajorLabels', - 'icons', - 'majorLinesOffset', - 'minorLinesOffset', - 'labelOffsetX', - 'labelOffsetY', - 'iconWidth', - 'width', - 'visible', - 'customRange' - ]; - util.selectiveExtend(fields, this.options, options); + 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.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - this.minWidth = Number(('' + this.options.width).replace("px","")); + from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - if (redraw == true && this.dom.frame) { - this.hide(); - this.show(); + 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.colorAxis; + ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); + + step.next(); } - }; + // 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.colorGrid; + 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.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + + 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.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - /** - * 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; + 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.colorAxis; + ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y); - this.dom.lineContainer = document.createElement('div'); - this.dom.lineContainer.style.width = '100%'; - this.dom.lineContainer.style.height = this.height; + step.next(); + } + + // 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.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(from.x - textMargin, from.y); + ctx.stroke(); - // 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); - }; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y); - DataAxis.prototype._redrawGroupIcons = function () { - DOMutil.prepareElements(this.svgElements); + 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.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - var x; - var iconWidth = this.options.iconWidth; - var iconHeight = 15; - var iconOffset = 4; - var y = iconOffset + 0.5 * iconHeight; + // 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.colorAxis; + 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.colorAxis; + ctx.beginPath(); + ctx.moveTo(xMin2d.x, xMin2d.y); + ctx.lineTo(xMax2d.x, xMax2d.y); + ctx.stroke(); - if (this.options.orientation == 'left') { - x = iconOffset; - } - else { - x = this.width - iconWidth - iconOffset; - } + // 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.colorAxis; + 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.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true) { - this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); - y += iconHeight + iconOffset; - } + // 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.colorAxis; + ctx.fillText(xLabel, text.x, text.y); } - DOMutil.cleanupElements(this.svgElements); - }; - - /** - * Create the HTML DOM for the DataAxis - */ - DataAxis.prototype.show = function() { - if (!this.dom.frame.parentNode) { - if (this.options.orientation == 'left') { - this.body.dom.left.appendChild(this.dom.frame); + // 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 { - this.body.dom.right.appendChild(this.dom.frame); + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; } + ctx.fillStyle = this.colorAxis; + ctx.fillText(yLabel, text.x, text.y); } - if (!this.dom.lineContainer.parentNode) { - this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); + // 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.colorAxis; + ctx.fillText(zLabel, text.x - offset, text.y); } }; /** - * Create the HTML DOM for the DataAxis + * 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 */ - DataAxis.prototype.hide = function() { - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + Graph3d.prototype._hsv2rgb = function(H, S, V) { + var R, G, B, C, Hi, X; - if (this.dom.lineContainer.parentNode) { - this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); - } - }; + C = V * S; + Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(((H/60) % 2) - 1)); - /** - * Set a range (start and end) - * @param end - * @param start - * @param end - */ - DataAxis.prototype.setRange = function (start, end) { - this.range.start = start; - this.range.end = end; - }; + 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; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - DataAxis.prototype.redraw = function () { - var changeCalled = false; - var activeGroups = 0; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true) { - activeGroups++; - } - } - } - if (this.amountOfGroups == 0 || activeGroups == 0) { - this.hide(); + default: R = 0; G = 0; B = 0; break; } - 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 = 'dataaxis'; - - // 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 elemens for the axis - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')'; + }; - 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"; - } - else { // right - frame.style.top = ''; - frame.style.bottom = '0'; - frame.style.left = '0'; - frame.style.width = this.width + 'px'; - frame.style.height = this.height + "px"; - } - changeCalled = this._redrawLabels(); - if (this.options.icons == true) { - this._redrawGroupIcons(); - } - } - return changeCalled; - }; + /** + * 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; - /** - * Repaint major and minor text labels and vertical grid lines - * @private - */ - DataAxis.prototype._redrawLabels = function () { - DOMutil.prepareElements(this.DOMelements.lines); - DOMutil.prepareElements(this.DOMelements.labels); - var orientation = this.options['orientation']; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - // calculate range and step (step such that we have space for 7 characters per label) - var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced; - var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation]); - this.step = step; - // get the distance in pixels for a step - // dead space is space that is "left over" after a step - var stepPixels = (this.dom.frame.offsetHeight - (step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange))) / (((step.marginRange - step.deadSpace) / step.step)); - this.stepPixels = stepPixels; + // 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 amountOfSteps = this.height / stepPixels; - var stepDifference = 0; + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - if (this.master == false) { - stepPixels = this.stepPixelsForced; - stepDifference = Math.round((this.dom.frame.offsetHeight / stepPixels) - amountOfSteps); - for (var i = 0; i < 0.5 * stepDifference; i++) { - step.previous(); - } - amountOfSteps = this.height / stepPixels; - } - else { - amountOfSteps += 0.25; + // 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; } + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - this.valueAtZero = step.marginEnd; - var marginStartPos = 0; + 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; - // do not draw the first label - var max = 1; + if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { - this.maxLabelSize = 0; - var y = 0; - while (max < Math.round(amountOfSteps)) { - step.next(); - y = Math.round(max * stepPixels); - marginStartPos = max * stepPixels; - var isMajor = step.isMajor(); + if (this.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) - if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) { - this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight); - } + topSideVisible = (crossproduct.z > 0); + } + else { + topSideVisible = true; + } - if (isMajor && this.options['showMajorLabels'] && this.master == true || - this.options['showMinorLabels'] == false && this.master == false && isMajor == true) { - if (y >= 0) { - this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight); + 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.colorAxis; + } + } + else { + fillStyle = 'gray'; + strokeStyle = this.colorAxis; + } + lineWidth = 0.5; + + ctx.lineWidth = lineWidth; + 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(); } - this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth); - } - else { - this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth); } - - max++; } + else { // 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 (this.master == false) { - this.conversionFactor = y / (this.valueAtZero - step.current); - } - else { - this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; - } + if (point !== undefined) { + if (this.showPerspective) { + lineWidth = 2 / -point.trans.z; + } + else { + lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); + } + } - var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15; - // this will resize the yAxis to accomodate the labels. - if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) { - this.width = this.maxLabelSize + offset; - this.options.width = this.width + "px"; - DOMutil.cleanupElements(this.DOMelements.lines); - DOMutil.cleanupElements(this.DOMelements.labels); - this.redraw(); - return true; - } - // this will resize the yAxis if it is too big for the labels. - else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) { - this.width = Math.max(this.minWidth,this.maxLabelSize + offset); - this.options.width = this.width + "px"; - DOMutil.cleanupElements(this.DOMelements.lines); - DOMutil.cleanupElements(this.DOMelements.labels); - this.redraw(); - return true; - } - else { - DOMutil.cleanupElements(this.DOMelements.lines); - DOMutil.cleanupElements(this.DOMelements.labels); - return false; + 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 = lineWidth; + 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(); + } + + 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; + + ctx.lineWidth = lineWidth; + 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(); + } + } } }; - DataAxis.prototype.convertValue = function (value) { - var invertedValue = this.valueAtZero - value; - var convertedValue = invertedValue * this.conversionFactor; - return convertedValue; - }; /** - * Create a label for the axis at position x - * @private - * @param y - * @param text - * @param orientation - * @param className - * @param characterHeight + * Draw all datapoints as dots. + * This function can be used when the style is 'dot' or 'dot-line' */ - 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"; + Graph3d.prototype._redrawDataDot = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i; + + 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; } - label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px'; + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - text += ''; + // 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]; + + 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.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(point.screen.x, point.screen.y); + ctx.stroke(); + } - var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth); - if (this.maxLabelSize < text.length * largestWidth) { - this.maxLabelSize = text.length * largestWidth; - } - }; + // 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; + } - /** - * 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 = ''; + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } + else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; + } - if (orientation == 'left') { - line.style.left = (this.width - offset) + 'px'; + 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.colorDot; + borderColor = this.colorDotBorder; } else { - line.style.right = (this.width - offset) + 'px'; + // 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); } - line.style.width = width + 'px'; - line.style.top = y + 'px'; + // draw the circle + ctx.lineWidth = 1.0; + 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(); } }; - - - - /** - * 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 + * Draw all datapoints as bars. + * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - DataAxis.prototype._calculateCharSize = function () { - // determine the char width and height on the minor axis - if (!('minorCharHeight' in this.props)) { - var textMinor = document.createTextNode('0'); - var measureCharMinor = document.createElement('DIV'); - measureCharMinor.className = 'yAxis minor measure'; - measureCharMinor.appendChild(textMinor); - this.dom.frame.appendChild(measureCharMinor); + Graph3d.prototype._redrawDataBar = function() { + var canvas = this.frame.canvas; + var ctx = canvas.getContext('2d'); + var i, j, surface, corners; - this.props.minorCharHeight = measureCharMinor.clientHeight; - this.props.minorCharWidth = measureCharMinor.clientWidth; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - this.dom.frame.removeChild(measureCharMinor); + // 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 (!('majorCharHeight' in this.props)) { - var textMajor = document.createTextNode('0'); - var measureCharMajor = document.createElement('DIV'); - measureCharMajor.className = 'yAxis major measure'; - measureCharMajor.appendChild(textMajor); - this.dom.frame.appendChild(measureCharMajor); + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - this.props.majorCharHeight = measureCharMajor.clientHeight; - this.props.majorCharWidth = measureCharMajor.clientWidth; + // 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]; - this.dom.frame.removeChild(measureCharMajor); - } - }; + // 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.colorDot; + borderColor = this.colorDotBorder; + } + 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); + } - /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate - */ - DataAxis.prototype.snap = function(date) { - return this.step.snap(date); - }; + // 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); + } - module.exports = DataAxis; + // 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)} + ]; + // 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); + }); -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { + // 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 util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); + // 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}) + } - /** - * @constructor Group - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet - */ - function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) { - this.id = groupId; - var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom'] - this.options = util.selectiveBridgeObject(fields,options); - this.usingDefaultStyle = group.className === undefined; - this.groupsUsingDefaultStyles = groupsUsingDefaultStyles; - this.zeroPosition = 0; - this.update(group); - if (this.usingDefaultStyle == true) { - this.groupsUsingDefaultStyles[0] += 1; - } - this.itemsData = []; - this.visible = group.visible === undefined ? true : group.visible; - } + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; - GraphGroup.prototype.setItems = function(items) { - if (items != null) { - this.itemsData = items; - if (this.options.sort == true) { - this.itemsData.sort(function (a,b) {return a.x - b.x;}) - } - } - else { - this.itemsData = []; + // 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 = 1; + 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(); + } } }; - GraphGroup.prototype.setZeroPosition = function(pos) { - this.zeroPosition = pos; - }; - - GraphGroup.prototype.setOptions = function(options) { - if (options !== undefined) { - var fields = ['sampling','style','sort','yAxisOrientation','barChart']; - util.selectiveDeepExtend(fields, this.options, options); - - util.mergeOptions(this.options, options,'catmullRom'); - util.mergeOptions(this.options, options,'drawPoints'); - util.mergeOptions(this.options, options,'shaded'); - if (options.catmullRom) { - if (typeof options.catmullRom == 'object') { - if (options.catmullRom.parametrization) { - if (options.catmullRom.parametrization == 'uniform') { - this.options.catmullRom.alpha = 0; - } - else if (options.catmullRom.parametrization == 'chordal') { - this.options.catmullRom.alpha = 1.0; - } - else { - this.options.catmullRom.parametrization = 'centripetal'; - this.options.catmullRom.alpha = 0.5; - } - } - } - } - } - }; + /** + * 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; - GraphGroup.prototype.update = function(group) { - this.group = group; - this.content = group.content || 'graph'; - this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10; - this.visible = group.visible === undefined ? true : group.visible; - this.setOptions(group.options); - }; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) + return; // TODO: throw exception? - GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { - var fillHeight = iconHeight * 0.5; - var path, fillPath; + // 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 outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer); - outline.setAttributeNS(null, "x", x); - outline.setAttributeNS(null, "y", y - fillHeight); - outline.setAttributeNS(null, "width", iconWidth); - outline.setAttributeNS(null, "height", 2*fillHeight); - outline.setAttributeNS(null, "class", "outline"); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + } - if (this.options.style == 'line') { - path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer); - path.setAttributeNS(null, "class", this.className); - path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+""); - if (this.options.shaded.enabled == true) { - fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer); - if (this.options.shaded.orientation == 'top') { - fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) + - "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight)); - } - else { - fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " + - "L"+x+"," + (y + fillHeight) + " " + - "L"+ (x + iconWidth) + "," + (y + fillHeight) + - "L"+ (x + iconWidth) + ","+y); - } - fillPath.setAttributeNS(null, "class", this.className + " iconFill"); - } + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; - if (this.options.drawPoints.enabled == true) { - DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer); - } + ctx.lineWidth = 1; // TODO: make customizable + ctx.strokeStyle = 'blue'; // TODO: make customizable + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); } - 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); + // 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); + } - DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer); - DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer); + // finish the line + if (this.dataPoints.length > 0) { + ctx.stroke(); } }; /** - * - * @param iconWidth - * @param iconHeight - * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) */ - 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}; - } + Graph3d.prototype._onMouseDown = function(event) { + event = event || window.event; - module.exports = GraphGroup; + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); + } + // only react on left mouse button down + this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); + if (!this.leftButtonDown && !this.touchDown) return; -/***/ }, -/* 23 */ -/***/ function(module, exports, __webpack_require__) { + // 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); + }; - var util = __webpack_require__(1); - var stack = __webpack_require__(16); - var ItemRange = __webpack_require__(31); /** - * @constructor Group - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the event */ - function Group (groupId, data, itemSet) { - this.groupId = groupId; + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; - this.itemSet = itemSet; + // calculate change in mouse position + var diffX = parseFloat(getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - this.dom = {}; - this.props = { - label: { - width: 0, - height: 0 - } - }; - this.className = null; + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; - this.items = {}; // items filtered by groupId of this group - this.visibleItems = []; // items currently visible in window - this.orderedItems = { // items sorted by start and by end - byStart: [], - byEnd: [] - }; + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); - this._create(); + // 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; + } - this.setData(data); - } + // 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; + } - /** - * Create DOM elements for the group - * @private - */ - Group.prototype._create = function() { - var label = document.createElement('div'); - label.className = 'vlabel'; - this.dom.label = label; + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); - var inner = document.createElement('div'); - inner.className = 'inner'; - label.appendChild(inner); - this.dom.inner = inner; + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); - var foreground = document.createElement('div'); - foreground.className = 'group'; - foreground['timeline-group'] = this; - this.dom.foreground = foreground; + util.preventDefault(event); + }; - this.dom.background = document.createElement('div'); - this.dom.background.className = 'group'; - this.dom.axis = document.createElement('div'); - this.dom.axis.className = 'group'; + /** + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event + */ + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; + this.leftButtonDown = 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); + // remove event listeners here + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); }; /** - * Set the group data for this group - * @param {Object} data Group data, can contain properties content and className + * 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 */ - 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; + Graph3d.prototype._onTooltip = function (event) { + var delay = 300; // ms + var mouseX = getMouseX(event) - util.getAbsoluteLeft(this.frame); + var mouseY = getMouseY(event) - util.getAbsoluteTop(this.frame); + + if (!this.showTooltip) { + return; } - else { - this.dom.inner.innerHTML = this.groupId || ''; // groupId can be null + + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); } - // update title - this.dom.label.title = data && data.title || ''; + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; + } - if (!this.dom.inner.firstChild) { - util.addClassName(this.dom.inner, 'hidden'); + 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 { - util.removeClassName(this.dom.inner, 'hidden'); - } + // tooltip is currently not visible + var me = this; + this.tooltipTimeout = setTimeout(function () { + me.tooltipTimeout = null; - // update className - var className = data && data.className || null; - if (className != this.className) { - if (this.className) { - util.removeClassName(this.dom.label, className); - util.removeClassName(this.dom.foreground, className); - util.removeClassName(this.dom.background, className); - util.removeClassName(this.dom.axis, 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); + // show a tooltip if we have a data point + var dataPoint = me._dataPointFromXY(mouseX, mouseY); + if (dataPoint) { + me._showTooltip(dataPoint); + } + }, delay); } }; /** - * Get the width of the group label - * @return {number} width + * Event handler for touchstart event on mobile devices */ - Group.prototype.getLabelWidth = function() { - return this.props.label.width; + Graph3d.prototype._onTouchStart = function(event) { + this.touchDown = true; + + 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); + + this._onMouseDown(event); }; + /** + * Event handler for touchmove event on mobile devices + */ + Graph3d.prototype._onTouchMove = function(event) { + this._onMouseMove(event); + }; /** - * 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 + * Event handler for touchend event on mobile devices */ - Group.prototype.redraw = function(range, margin, restack) { - var resized = false; + Graph3d.prototype._onTouchEnd = function(event) { + this.touchDown = false; - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + util.removeEventListener(document, 'touchmove', this.ontouchmove); + util.removeEventListener(document, 'touchend', this.ontouchend); - // 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; + this._onMouseUp(event); + }; - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); - restack = true; - } + /** + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event + */ + Graph3d.prototype._onWheel = function(event) { + if (!event) /* For IE. */ + event = window.event; - // reposition visible items vertically - if (this.itemSet.options.stack) { // TODO: ugly way to access options... - stack.stack(this.visibleItems, margin, restack); - } - else { // no stacking - stack.nostack(this.visibleItems, margin); + // 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; } - // recalculate the height of the group - var height; - var visibleItems = this.visibleItems; - if (visibleItems.length) { - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, (item.top + item.height)); - }); - 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; + // 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); + + this.camera.setArmLength(newLength); + this.redraw(); + + this._hideTooltip(); } - else { - height = margin.axis + margin.item.vertical; + + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit('cameraPositionChange', parameters); + + // 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); + }; + + /** + * 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 + */ + Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; + + function sign (x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; } - height = Math.max(height, this.props.label.height); - // calculate actual size and position - var foreground = this.dom.foreground; - this.top = foreground.offsetTop; - this.left = foreground.offsetLeft; - this.width = foreground.offsetWidth; - resized = util.updateProperty(this, 'height', height) || resized; + 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)); - // 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; + // 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); + }; - // apply new height - this.dom.background.style.height = height + 'px'; - this.dom.foreground.style.height = height + 'px'; - this.dom.label.style.height = height + 'px'; + /** + * 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 + */ + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); + + 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); - // 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(); + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } + } + } } - return resized; + + return closestDataPoint; }; /** - * Show this group: attach to the DOM + * Display a tooltip for given data point + * @param {Object} dataPoint + * @private */ - Group.prototype.show = function() { - if (!this.dom.label.parentNode) { - this.itemSet.dom.labelSet.appendChild(this.dom.label); - } + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - if (!this.dom.foreground.parentNode) { - this.itemSet.dom.foreground.appendChild(this.dom.foreground); - } + 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)'; - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } + line = document.createElement('div'); + line.style.position = 'absolute'; + line.style.height = '40px'; + line.style.width = '0'; + line.style.borderLeft = '1px solid #4d4d4d'; - if (!this.dom.axis.parentNode) { - this.itemSet.dom.axis.appendChild(this.dom.axis); - } - }; + 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'; - /** - * Hide this group: remove from the DOM - */ - Group.prototype.hide = function() { - var label = this.dom.label; - if (label.parentNode) { - label.parentNode.removeChild(label); + this.tooltip = { + dataPoint: null, + dom: { + content: content, + line: line, + dot: dot + } + }; } - - var foreground = this.dom.foreground; - if (foreground.parentNode) { - foreground.parentNode.removeChild(foreground); + else { + content = this.tooltip.dom.content; + line = this.tooltip.dom.line; + dot = this.tooltip.dom.dot; } - var background = this.dom.background; - if (background.parentNode) { - background.parentNode.removeChild(background); - } + this._hideTooltip(); - var axis = this.dom.axis; - if (axis.parentNode) { - axis.parentNode.removeChild(axis); + this.tooltip.dataPoint = dataPoint; + if (typeof this.showTooltip === 'function') { + content.innerHTML = this.showTooltip(dataPoint.point); } - }; - - /** - * Add an item to the group - * @param {Item} item - */ - Group.prototype.add = function(item) { - this.items[item.id] = item; - item.setParent(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); + else { + content.innerHTML = '' + + '' + + '' + + '' + + '
x:' + dataPoint.point.x + '
y:' + dataPoint.point.y + '
z:' + dataPoint.point.z + '
'; } - }; - /** - * Remove an item from the group - * @param {Item} item - */ - Group.prototype.remove = function(item) { - delete this.items[item.id]; - item.setParent(this.itemSet); - - // remove from visible items - var index = this.visibleItems.indexOf(item); - if (index != -1) this.visibleItems.splice(index, 1); - - // TODO: also remove from ordered items? - }; + content.style.left = '0'; + content.style.top = '0'; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); - /** - * Remove an item from the corresponding DataSet - * @param {Item} item - */ - Group.prototype.removeFromDataSet = function(item) { - this.itemSet.removeItem(item.id); - }; + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; - /** - * Reorder the items - */ - Group.prototype.order = function() { - var array = util.toArray(this.items); - this.orderedItems.byStart = array; - this.orderedItems.byEnd = this._constructByEndArray(array); + var left = dataPoint.screen.x - contentWidth / 2; + left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); - stack.orderByStart(this.orderedItems.byStart); - stack.orderByEnd(this.orderedItems.byEnd); + 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'; }; /** - * Create an array containing all items being a range (having an end date) - * @param {Item[]} array - * @returns {ItemRange[]} + * Hide the tooltip when displayed * @private */ - Group.prototype._constructByEndArray = function(array) { - var endArray = []; + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof ItemRange) { - endArray.push(array[i]); + 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 endArray; }; - /** - * Update the visible items - * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date - * @param {Item[]} visibleItems The previously visible items. - * @param {{start: number, end: number}} range Visible range - * @return {Item[]} visibleItems The new visible items. - * @private - */ - Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) { - var initialPosByStart, - newVisibleItems = [], - i; + /**--------------------------------------------------------------------------**/ - // first check if the items that were in view previously are still in view. - // this handles the case for the ItemRange that is both before and after the current one. - if (visibleItems.length > 0) { - for (i = 0; i < visibleItems.length; i++) { - this._checkIfVisible(visibleItems[i], newVisibleItems, range); - } - } - // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime) - if (newVisibleItems.length == 0) { - initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start'); - } - else { - initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); - } + /** + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x + */ + getMouseX = function(event) { + if ('clientX' in event) return event.clientX; + return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + }; - // use visible search to find a visible ItemRange (only based on endTime) - var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end'); + /** + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse y + */ + getMouseY = function(event) { + if ('clientY' in event) return event.clientY; + return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + }; - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByStart != -1) { - for (i = initialPosByStart; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} - } - for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) { - if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} - } - } + module.exports = Graph3d; - // if we found a initial ID to use, trace it up and down until we meet an invisible item. - if (initialPosByEnd != -1) { - for (i = initialPosByEnd; i >= 0; i--) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} - } - for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) { - if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} - } - } - return newVisibleItems; - }; +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Expose `Emitter`. + */ + module.exports = Emitter; /** - * this function checks if an item is invisible. If it is NOT we make it visible - * and add it to the global visible items. If it is, return true. + * Initialize a new `Emitter`. * - * @param {Item} item - * @param {Item[]} visibleItems - * @param {{start:number, end:number}} range - * @returns {boolean} - * @private + * @api public */ - Group.prototype._checkIfInvisible = function(item, visibleItems, range) { - if (item.isVisible(range)) { - if (!item.displayed) item.show(); - item.repositionX(); - if (visibleItems.indexOf(item) == -1) { - visibleItems.push(item); - } - return false; - } - else { - if (item.displayed) item.hide(); - return true; - } + + function Emitter(obj) { + if (obj) return mixin(obj); }; /** - * 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. + * Mixin the emitter properties. * - * @param {Item} item - * @param {Array} visibleItems - * @param {{start:number, end:number}} range - * @private + * @param {Object} obj + * @return {Object} + * @api 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(); + + function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; } - }; + return obj; + } - module.exports = Group; + /** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + Emitter.prototype.on = + Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; + }; -/***/ }, -/* 24 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Component = __webpack_require__(18); - var Group = __webpack_require__(23); - var ItemBox = __webpack_require__(29); - var ItemPoint = __webpack_require__(30); - var ItemRange = __webpack_require__(31); + Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; + function on() { + self.off(event, on); + fn.apply(this, arguments); + } - var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + on.fn = fn; + this.on(event, on); + return this; + }; /** - * 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 + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - function ItemSet(body, options) { - this.body = body; - - this.defaultOptions = { - type: null, // 'box', 'point', 'range' - orientation: 'bottom', // 'top' or 'bottom' - align: 'center', // alignment of box items - stack: true, - groupOrder: null, - selectable: true, - editable: { - updateTime: false, - updateGroup: false, - add: false, - remove: false - }, + Emitter.prototype.off = + Emitter.prototype.removeListener = + Emitter.prototype.removeAllListeners = + Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; - onAdd: function (item, callback) { - callback(item); - }, - onUpdate: function (item, callback) { - callback(item); - }, - onMove: function (item, callback) { - callback(item); - }, - onRemove: function (item, callback) { - callback(item); - }, + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } - margin: { - item: { - horizontal: 10, - vertical: 10 - }, - axis: 20 - }, - padding: 5 - }; + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; - // options is shared by this ItemSet and all its items - this.options = util.extend({}, this.defaultOptions); + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } - // options for getting items from the DataSet with the correct type - this.itemOptions = { - type: {start: 'Date', end: 'Date'} - }; + // 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; + } + } + return this; + }; - this.conversion = { - toScreen: body.util.toScreen, - toTime: body.util.toTime - }; - this.dom = {}; - this.props = {}; - this.hammer = null; + /** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; - // listeners for the DataSet of the items - this.itemListeners = { - 'add': function (event, params, senderId) { - me._onAdd(params.items); - }, - 'update': function (event, params, senderId) { - me._onUpdate(params.items); - }, - 'remove': function (event, params, senderId) { - me._onRemove(params.items); + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); } - }; + } - // listeners for the DataSet of the groups - this.groupListeners = { - 'add': function (event, params, senderId) { - me._onAddGroups(params.items); - }, - 'update': function (event, params, senderId) { - me._onUpdateGroups(params.items); - }, - 'remove': function (event, params, senderId) { - me._onRemoveGroups(params.items); - } - }; + return this; + }; - this.items = {}; // object with an Item for every data item - this.groups = {}; // Group object for every group - this.groupIds = []; + /** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ - this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw + Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; + }; - this.touchParams = {}; // stores properties while dragging - // create the HTML DOM + /** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ - this._create(); + Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; + }; - this.setOptions(options); - } - ItemSet.prototype = new Component(); +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { - // available item types will be registered here - ItemSet.types = { - box: ItemBox, - range: ItemRange, - point: ItemPoint + /** + * @prototype Point3d + * @param {Number} [x] + * @param {Number} [y] + * @param {Number} [z] + */ + function Point3d(x, y, z) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + this.z = z !== undefined ? z : 0; }; /** - * Create the HTML DOM for the ItemSet + * Subtract the two provided points, returns a-b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a-b */ - ItemSet.prototype._create = function(){ - var frame = document.createElement('div'); - frame.className = 'itemset'; - frame['timeline-itemset'] = this; - this.dom.frame = frame; - - // create background panel - var background = document.createElement('div'); - background.className = 'background'; - frame.appendChild(background); - this.dom.background = background; + 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; + }; - // create foreground panel - var foreground = document.createElement('div'); - foreground.className = 'foreground'; - frame.appendChild(foreground); - this.dom.foreground = foreground; + /** + * 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; + }; - // create axis panel - var axis = document.createElement('div'); - axis.className = 'axis'; - this.dom.axis = axis; + /** + * 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 + ); + }; - // create labelset - var labelSet = document.createElement('div'); - labelSet.className = 'labelset'; - this.dom.labelSet = labelSet; + /** + * 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(); - // create ungrouped Group - this._updateUngrouped(); + 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; - // 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 = Hammer(this.body.dom.centerContainer, { - prevent_default: true - }); + return crossproduct; + }; - // drag items when selected - this.hammer.on('touch', this._onTouch.bind(this)); - this.hammer.on('dragstart', this._onDragStart.bind(this)); - this.hammer.on('drag', this._onDrag.bind(this)); - this.hammer.on('dragend', this._onDragEnd.bind(this)); - // single select (or unselect) when tapping an item - this.hammer.on('tap', this._onSelectItem.bind(this)); + /** + * Rtrieve the length of the vector (or the distance from this point to the origin + * @return {Number} length + */ + Point3d.prototype.length = function() { + return Math.sqrt( + this.x * this.x + + this.y * this.y + + this.z * this.z + ); + }; - // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on('hold', this._onMultiSelectItem.bind(this)); + module.exports = Point3d; - // add item on doubletap - this.hammer.on('doubletap', this._onAddItem.bind(this)); - // attach to the DOM - this.show(); - }; +/***/ }, +/* 12 */ +/***/ 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', or 'range'. The default - * Style can be overwritten by individual items. - * {String} align - * Alignment for the items, only applicable for - * ItemBox. Choose 'center' (default), 'left', or - * 'right'. - * {String} orientation - * Orientation of the item set. Choose 'top' or - * 'bottom' (default). - * {Function} groupOrder - * A sorting function for ordering groups - * {Boolean} stack - * If true (deafult), 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. - * {Number} padding - * Padding of the contents of an item in pixels. - * Must correspond with the items css. Default is 5. - * {Boolean} selectable - * If true (default), items can be selected. - * {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 Point2d + * @param {Number} [x] + * @param {Number} [y] */ - ItemSet.prototype.setOptions = function(options) { - if (options) { - // copy all options that we know - var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder']; - util.selectiveExtend(fields, this.options, options); + Point2d = function (x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + }; - 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); - } - } - } - } + module.exports = Point2d; - 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); - } - } - // callback functions - var addCallback = (function (name) { - if (name in options) { - var fn = options[name]; - 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'].forEach(addCallback); +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { - // force the itemSet to refresh: options like orientation and margins may be changed - this.markDirty(); - } - }; + var Point3d = __webpack_require__(11); /** - * Mark the ItemSet dirty so it will refresh everything with next redraw + * @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 */ - ItemSet.prototype.markDirty = function() { - this.groupIds = []; - this.stackDirty = true; + Camera = function () { + 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(); }; /** - * Destroy the ItemSet + * 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 */ - ItemSet.prototype.destroy = function() { - this.hide(); - this.setItems(null); - this.setGroups(null); - - this.hammer = null; + Camera.prototype.setArmLocation = function(x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; - this.body = null; - this.conversion = null; + this.calculateCameraOrientation(); }; /** - * Hide the component from the DOM + * 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. */ - ItemSet.prototype.hide = function() { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); + Camera.prototype.setArmRotation = function(horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; } - // remove the axis with dots - if (this.dom.axis.parentNode) { - this.dom.axis.parentNode.removeChild(this.dom.axis); + 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; } - // remove the labelset containing all group labels - if (this.dom.labelSet.parentNode) { - this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); } }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical */ - 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); - } + Camera.prototype.getArmRotation = function() { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; - // show labelset containing labels - if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); - } + return rot; }; /** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {Array} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. + * Set the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 */ - ItemSet.prototype.setSelection = function(ids) { - var i, ii, id, item; + Camera.prototype.setArmLength = function(length) { + if (length === undefined) + return; - if (ids) { - if (!Array.isArray(ids)) { - throw new TypeError('Array expected'); - } + this.armLength = length; - // 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(); - } + // 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.0) this.armLength = 5.0; - // 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(); - } - } - } + this.calculateCameraOrientation(); }; /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * Retrieve the arm length + * @return {Number} length */ - ItemSet.prototype.getSelection = function() { - return this.selection.concat([]); + Camera.prototype.getArmLength = function() { + return this.armLength; }; /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * Retrieve the camera location + * @return {Point3d} cameraLocation */ - 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); + Camera.prototype.getCameraLocation = function() { + return this.cameraLocation; + }; - var ids = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - var group = this.groups[groupId]; - var rawVisibleItems = group.visibleItems; + /** + * Retrieve the camera rotation + * @return {Point3d} cameraRotation + */ + Camera.prototype.getCameraRotation = function() { + return this.cameraRotation; + }; - // 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); - } - } - } - } + /** + * 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); - return ids; + // calculate rotation of the camera + this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; + this.cameraRotation.y = 0; + this.cameraRotation.z = -this.armRotation.horizontal; }; + module.exports = Camera; + +/***/ }, +/* 14 */ +/***/ function(module, exports, __webpack_require__) { + + var DataView = __webpack_require__(8); + /** - * Deselect a selected item - * @param {String | Number} id - * @private + * @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 */ - 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; - } + 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); + } + + // 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; } }; + + /** + * Return the label + * @return {string} label + */ + Filter.prototype.isLoaded = function() { + return this.loaded; + }; + + /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Return the loaded progress + * @return {Number} percentage between 0 and 100 */ - ItemSet.prototype.redraw = function() { - var margin = this.options.margin, - range = this.body.range, - asSize = util.option.asSize, - options = this.options, - orientation = options.orientation, - resized = false, - frame = this.dom.frame, - editable = options.editable.updateTime || options.editable.updateGroup; + Filter.prototype.getLoadedProgress = function() { + var len = this.values.length; - // update class name - frame.className = 'itemset' + (editable ? ' editable' : ''); + var i = 0; + while (this.dataPoints[i]) { + i++; + } - // reorder the groups (if needed) - resized = this._orderGroups() || resized; + return Math.round(i / len * 100); + }; - // 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; - // redraw all groups - var restack = this.stackDirty, - firstGroup = this._firstGroup(), - firstMargin = { - item: margin.item, - axis: margin.axis - }, - nonFirstMargin = { - item: margin.item, - axis: margin.item.vertical / 2 - }, - height = 0, - minHeight = margin.axis + margin.item.vertical; - 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; + /** + * Return the label + * @return {string} label + */ + Filter.prototype.getLabel = function() { + return this.graph.filterLabel; + }; - // update frame height - frame.style.height = asSize(height); - // calculate actual size and position - this.props.top = frame.offsetTop; - this.props.left = frame.offsetLeft; - this.props.width = frame.offsetWidth; - this.props.height = height; + /** + * Return the columnIndex of the filter + * @return {Number} columnIndex + */ + Filter.prototype.getColumn = function() { + return this.column; + }; - // 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 = this.body.domProps.border.left + 'px'; + /** + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value + */ + Filter.prototype.getSelectedValue = function() { + if (this.index === undefined) + return undefined; - // check if this component is resized - resized = this._isResized() || resized; + return this.values[this.index]; + }; - return resized; + /** + * Retrieve all values of the filter + * @return {Array} values + */ + Filter.prototype.getValues = function() { + return this.values; }; /** - * Get the first group, aligned with the axis - * @return {Group | null} firstGroup - * @private + * Retrieve one value of the filter + * @param {Number} index + * @return {*} value */ - ItemSet.prototype._firstGroup = function() { - var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1); - var firstGroupId = this.groupIds[firstGroupIndex]; - var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; + Filter.prototype.getValue = function(index) { + if (index >= this.values.length) + throw 'Error: index out of range'; - return firstGroup || null; + return this.values[index]; }; + /** - * Create or delete the group holding all ungrouped items. This group is used when - * there are no groups specified. - * @protected + * Retrieve the (filtered) dataPoints for the currently selected filter index + * @param {Number} [index] (optional) + * @return {Array} dataPoints */ - ItemSet.prototype._updateUngrouped = function() { - var ungrouped = this.groups[UNGROUPED]; + Filter.prototype._getDataPoints = function(index) { + if (index === undefined) + index = this.index; - if (this.groupsData) { - // remove the group holding all ungrouped items - if (ungrouped) { - ungrouped.hide(); - delete this.groups[UNGROUPED]; - } + if (index === undefined) + return []; + + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; } 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; + var f = {}; + f.column = this.column; + f.value = this.values[index]; - for (var itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - ungrouped.add(this.items[itemId]); - } - } + var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get(); + dataPoints = this.graph._getDataPoints(dataView); - ungrouped.show(); - } + this.dataPoints[index] = dataPoints; } + + return dataPoints; }; + + /** - * Get the element for the labelset - * @return {HTMLElement} labelSet + * Set a callback function when the filter is fully loaded. */ - ItemSet.prototype.getLabelSet = function() { - return this.dom.labelSet; + Filter.prototype.setOnLoadCallback = function(callback) { + this.onLoadCallback = callback; }; + /** - * Set items - * @param {vis.DataSet | null} items + * Add a value to the list with available values for this filter + * No double entries will be created. + * @param {Number} index */ - ItemSet.prototype.setItems = function(items) { - var me = this, - ids, - oldItemsData = this.itemsData; + Filter.prototype.selectValue = function(index) { + if (index >= this.values.length) + throw 'Error: index out of range'; - // 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'); - } + this.index = index; + this.value = this.values[index]; + }; - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); - }); + /** + * Load all filtered rows in the background one by one + * Start this method without providing an index! + */ + Filter.prototype.loadInBackground = function(index) { + if (index === undefined) + index = 0; - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + var frame = this.graph.frame; - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); - }); + if (index < this.values.length) { + var dataPointsTemp = this._getDataPoints(index); + //this.graph.redrawInfo(); // TODO: not neat - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + // 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 the group holding all ungrouped items - this._updateUngrouped(); + var me = this; + setTimeout(function() {me.loadInBackground(index+1);}, 10); + this.loaded = false; } - }; + else { + this.loaded = true; - /** - * Get the current items - * @returns {vis.DataSet | null} - */ - ItemSet.prototype.getItems = function() { - return this.itemsData; + // remove the progress box + if (frame.progress !== undefined) { + frame.removeChild(frame.progress); + frame.progress = undefined; + } + + if (this.onLoadCallback) + this.onLoadCallback(); + } }; + module.exports = Filter; + + +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { + + var util = __webpack_require__(1); + /** - * Set groups - * @param {vis.DataSet} groups + * @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. */ - ItemSet.prototype.setGroups = function(groups) { - var me = this, - ids; + 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; - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + 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); - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + this.frame.prev = document.createElement('INPUT'); + this.frame.prev.type = 'BUTTON'; + this.frame.prev.value = 'Prev'; + this.frame.appendChild(this.frame.prev); - // 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'); - } + this.frame.play = document.createElement('INPUT'); + this.frame.play.type = 'BUTTON'; + this.frame.play.value = 'Play'; + this.frame.appendChild(this.frame.play); - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + this.frame.next = document.createElement('INPUT'); + this.frame.next.type = 'BUTTON'; + this.frame.next.value = 'Next'; + this.frame.appendChild(this.frame.next); - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); + 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); + + 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); + + // 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);}; } - // update the group holding all ungrouped items - this._updateUngrouped(); + this.onChangeCallback = undefined; - // update the order of all items in each group - this._order(); + this.values = []; + this.index = undefined; - this.body.emitter.emit('change'); - }; + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } /** - * Get the current groups - * @returns {vis.DataSet | null} groups + * Select the previous index */ - ItemSet.prototype.getGroups = function() { - return this.groupsData; + Slider.prototype.prev = function() { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); + } }; /** - * Remove an item by its id - * @param {String | Number} id + * Select the next index */ - ItemSet.prototype.removeItem = function(id) { - var item = this.itemsData.get(id), - dataset = this.itemsData.getDataSet(); - - 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); - } - }); + Slider.prototype.next = function() { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); } }; /** - * Handle updated items - * @param {Number[]} ids - * @protected + * Select the next index */ - ItemSet.prototype._onUpdate = function(ids) { - var me = this; + Slider.prototype.playNext = function() { + var start = new Date(); - ids.forEach(function (id) { - var itemData = me.itemsData.get(id, me.itemOptions), - item = me.items[id], - type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box'); + 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); + } - var constructor = ItemSet.types[type]; + var end = new Date(); + var diff = (end - start); - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, delete the item and recreate it - me._removeItem(item); - item = null; - } - else { - me._updateItem(item, itemData); - } - } + // 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 - 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); - } - 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.timeline .item.range .content {overflow: visible;}'); - } - else { - throw new TypeError('Unknown item type "' + type + '"'); - } - } - }); + var me = this; + this.playTimeout = setTimeout(function() {me.playNext();}, interval); + }; - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); + /** + * Toggle start or stop playing + */ + Slider.prototype.togglePlay = function() { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } }; /** - * Handle added items - * @param {Number[]} ids - * @protected + * Start playing */ - ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; + Slider.prototype.play = function() { + // Test whether already playing + if (this.playTimeout) return; + + this.playNext(); + + if (this.frame) { + this.frame.play.value = 'Stop'; + } + }; /** - * Handle removed items - * @param {Number[]} ids - * @protected + * Stop playing */ - 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); - } - }); + Slider.prototype.stop = function() { + clearInterval(this.playTimeout); + this.playTimeout = undefined; - if (count) { - // update order - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); + if (this.frame) { + this.frame.play.value = 'Play'; } }; /** - * Update the order of item in all groups - * @private + * Set a callback function which will be triggered when the value of the + * slider bar has changed. */ - 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(); - }); + Slider.prototype.setOnChangeCallback = function(callback) { + this.onChangeCallback = callback; }; /** - * Handle updated groups - * @param {Number[]} ids - * @private + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds */ - ItemSet.prototype._onUpdateGroups = function(ids) { - this._onAddGroups(ids); + Slider.prototype.setPlayInterval = function(interval) { + this.playInterval = interval; }; /** - * Handle changed groups - * @param {Number[]} ids - * @private + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds */ - ItemSet.prototype._onAddGroups = function(ids) { - var me = this; - - ids.forEach(function (id) { - var groupData = me.groupsData.get(id); - var group = me.groups[id]; - - if (!group) { - // check for reserved ids - if (id == UNGROUPED) { - throw new Error('Illegal group id. ' + id + ' is a reserved id.'); - } - - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null - }); - - group = new Group(id, groupData, me); - me.groups[id] = group; + Slider.prototype.getPlayInterval = function(interval) { + return this.playInterval; + }; - // 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); - } - } - } + /** + * 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; + }; - group.order(); - group.show(); - } - else { - // update group - group.setData(groupData); - } - }); - this.body.emitter.emit('change'); + /** + * Execute the onchange callback function + */ + Slider.prototype.onChange = function() { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } }; /** - * Handle removed groups - * @param {Number[]} ids - * @private + * redraw the slider on the correct place */ - ItemSet.prototype._onRemoveGroups = function(ids) { - var groups = this.groups; - ids.forEach(function (id) { - var group = groups[id]; - - if (group) { - group.hide(); - delete groups[id]; - } - }); - - this.markDirty(); + 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'; - this.body.emitter.emit('change'); + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = (left) + 'px'; + } }; + /** - * Reorder the groups if needed - * @return {boolean} changed - * @private + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) */ - ItemSet.prototype._orderGroups = function () { - if (this.groupsData) { - // reorder the groups - var groupIds = this.groupsData.getIds({ - order: this.options.groupOrder - }); - - 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(); - }); + Slider.prototype.setValues = function(values) { + this.values = values; - // show the groups again, attach them to the DOM in correct order - groupIds.forEach(function (groupId) { - groups[groupId].show(); - }); + if (this.values.length > 0) + this.setIndex(0); + else + this.index = undefined; + }; - this.groupIds = groupIds; - } + /** + * Select a value by its index + * @param {Number} index + */ + Slider.prototype.setIndex = function(index) { + if (index < this.values.length) { + this.index = index; - return changed; + this.redraw(); + this.onChange(); } else { - return false; + throw 'Error: index out of range'; } }; /** - * Add a new item - * @param {Item} item - * @private + * retrieve the index of the currently selected vaue + * @return {Number} index */ - ItemSet.prototype._addItem = function(item) { - this.items[item.id] = item; - - // add to group - var groupId = this.groupsData ? item.data.group : UNGROUPED; - var group = this.groups[groupId]; - if (group) group.add(item); + Slider.prototype.getIndex = function() { + return this.index; }; + /** - * Update an existing item - * @param {Item} item - * @param {Object} itemData - * @private + * retrieve the currently selected value + * @return {*} value */ - ItemSet.prototype._updateItem = function(item, itemData) { - var oldGroupId = item.data.group; + Slider.prototype.get = function() { + return this.values[this.index]; + }; - item.data = itemData; - if (item.displayed) { - item.redraw(); - } - // update group - if (oldGroupId != item.data.group) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); + 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; - var groupId = this.groupsData ? item.data.group : UNGROUPED; - var group = this.groups[groupId]; - if (group) group.add(item); - } + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); + + 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', this.onmousemove); + util.addEventListener(document, 'mouseup', this.onmouseup); + util.preventDefault(event); }; - /** - * 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(); - // remove from items - delete this.items[item.id]; + 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; + }; + + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - + this.frame.slide.clientWidth - 10; + + var x = index / (this.values.length-1) * width; + var left = x + 3; + + return left; + }; + + + + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var index = this.leftToIndex(x); + + this.setIndex(index); + + util.preventDefault(); + }; + - // remove from selection - var index = this.selection.indexOf(item.id); - if (index != -1) this.selection.splice(index, 1); + Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = 'auto'; - // remove from group - var groupId = this.groupsData ? item.data.group : UNGROUPED; - var group = this.groups[groupId]; - if (group) group.remove(item); + // remove event listeners + util.removeEventListener(document, 'mousemove', this.onmousemove); + util.removeEventListener(document, 'mouseup', this.onmouseup); + + util.preventDefault(); }; + module.exports = Slider; + + +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { + /** - * Create an array containing all items being a range (having an end date) - * @param array - * @returns {Array} - * @private + * @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._constructByEndArray = function(array) { - var endArray = []; + function StepNumber(start, end, step, prettyStep) { + // set default values + this._start = 0; + this._end = 0; + this._step = 1; + this.prettyStep = true; + this.precision = 5; - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof ItemRange) { - endArray.push(array[i]); - } - } - return endArray; + this._current = 0; + this.setRange(start, end, step, prettyStep); }; /** - * Register the clicked item on touch, before dragStart is initiated. - * - * dragStart is initiated from a mousemove event, which can have left the item - * already resulting in an item == null + * Set a new range: start, end and step. * - * @param {Event} event - * @private + * @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._onTouch = function (event) { - // store the touched item, used in _onDragStart - this.touchParams.item = ItemSet.itemFromTarget(event); + StepNumber.prototype.setRange = function(start, end, step, prettyStep) { + this._start = start ? start : 0; + this._end = end ? end : 0; + + this.setStep(step, prettyStep); }; /** - * Start dragging the selected events - * @param {Event} event - * @private + * 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._onDragStart = function (event) { - if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { + StepNumber.prototype.setStep = function(step, prettyStep) { + if (step === undefined || step <= 0) return; - } - - var item = this.touchParams.item || null, - me = this, - props; - - if (item && item.selected) { - var dragLeftItem = event.target.dragLeftItem; - var dragRightItem = event.target.dragRightItem; - - if (dragLeftItem) { - props = { - item: dragLeftItem - }; - - if (me.options.editable.updateTime) { - props.start = item.data.start.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } - - this.touchParams.itemProps = [props]; - } - else if (dragRightItem) { - props = { - item: dragRightItem - }; - if (me.options.editable.updateTime) { - props.end = item.data.end.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } - - this.touchParams.itemProps = [props]; - } - else { - this.touchParams.itemProps = this.getSelection().map(function (id) { - var item = me.items[id]; - var props = { - item: item - }; - - if (me.options.editable.updateTime) { - if ('start' in item.data) props.start = item.data.start.valueOf(); - if ('end' in item.data) props.end = item.data.end.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } - - return props; - }); - } + if (prettyStep !== undefined) + this.prettyStep = prettyStep; - event.stopPropagation(); - } + if (this.prettyStep === true) + this._step = StepNumber.calculatePrettyStep(step); + else + this._step = step; }; /** - * Drag selected items - * @param {Event} event - * @private + * 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._onDrag = function (event) { - if (this.touchParams.itemProps) { - var range = this.body.range, - snap = this.body.util.snap || null, - deltaX = event.gesture.deltaX, - scale = (this.props.width / (range.end - range.start)), - offset = deltaX / scale; - - // move - this.touchParams.itemProps.forEach(function (props) { - if ('start' in props) { - var start = new Date(props.start + offset); - props.item.data.start = snap ? snap(start) : start; - } - - if ('end' in props) { - var end = new Date(props.end + offset); - props.item.data.end = snap ? snap(end) : end; - } - - if ('group' in props) { - // drag from one group to another - var group = ItemSet.groupFromTarget(event); - _moveToGroup(props.item, group); - } - }); + StepNumber.calculatePrettyStep = function (step) { + var log10 = function (x) {return Math.log(x) / Math.LN10;}; - // TODO: implement onMoving handler + // 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))); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); + // 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; - event.stopPropagation(); + // for safety + if (prettyStep <= 0) { + prettyStep = 1; } + + return prettyStep; }; /** - * Move an item to another group - * @param {Item} item - * @param {Group} group - * @private + * returns the current value of the step + * @return {Number} current value */ - function _moveToGroup (item, group) { - if (group && group.groupId != item.data.group) { - var oldGroup = item.parent; - oldGroup.remove(item); - oldGroup.order(); - group.add(item); - group.order(); + StepNumber.prototype.getCurrent = function () { + return parseFloat(this._current.toPrecision(this.precision)); + }; - item.data.group = group.groupId; - } - } + /** + * returns the current step size + * @return {Number} current step size + */ + StepNumber.prototype.getStep = function () { + return this._step; + }; /** - * End of dragging selected items - * @param {Event} event - * @private + * Set the current value to the largest value smaller than start, which + * is a multiple of the step size */ - ItemSet.prototype._onDragEnd = function (event) { - if (this.touchParams.itemProps) { - // prepare a change set for the changed items - var changes = [], - me = this, - dataset = this.itemsData.getDataSet(); + StepNumber.prototype.start = function() { + this._current = this._start - this._start % this._step; + }; - var itemProps = this.touchParams.itemProps ; - this.touchParams.itemProps = null; - itemProps.forEach(function (props) { - var id = props.item.id, - itemData = me.itemsData.get(id, me.itemOptions); + /** + * Do a step, add the step size to the current value + */ + StepNumber.prototype.next = function () { + this._current += this._step; + }; - var changed = false; - if ('start' in props.item.data) { - changed = (props.start != props.item.data.start.valueOf()); - itemData.start = util.convert(props.item.data.start, - dataset._options.type && dataset._options.type.start || 'Date'); - } - if ('end' in props.item.data) { - changed = changed || (props.end != props.item.data.end.valueOf()); - itemData.end = util.convert(props.item.data.end, - dataset._options.type && dataset._options.type.end || 'Date'); - } - if ('group' in props.item.data) { - changed = changed || (props.group != props.item.data.group); - itemData.group = props.item.data.group; - } + /** + * Returns true whether the end is reached + * @return {boolean} True if the current value has passed the end value. + */ + StepNumber.prototype.end = function () { + return (this._current > this._end); + }; - // only apply changes when start or end is actually changed - if (changed) { - 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 - if ('start' in props) props.item.data.start = props.start; - if ('end' in props) props.item.data.end = props.end; - if ('group' in props && props.item.data.group != props.group) { - var group = me.groups[props.group]; - _moveToGroup(props.item, group); - } + module.exports = StepNumber; - me.stackDirty = true; // force re-stacking of all items next redraw - me.body.emitter.emit('change'); - } - }); - } - }); - // apply the changes to the data (if there are changes) - if (changes.length) { - dataset.update(changes); - } +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { - event.stopPropagation(); - } - }; + var Emitter = __webpack_require__(10); + var Hammer = __webpack_require__(18); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(8); + var Range = __webpack_require__(20); + var Core = __webpack_require__(23); + var TimeAxis = __webpack_require__(24); + var CurrentTime = __webpack_require__(26); + var CustomTime = __webpack_require__(28); + var ItemSet = __webpack_require__(29); /** - * Handle selecting/deselecting an item when tapping it - * @param {Event} event - * @private + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Timeline.setOptions for the available options. + * @constructor */ - ItemSet.prototype._onSelectItem = function (event) { - if (!this.options.selectable) return; + function Timeline (container, items, options) { + // mix the core properties in here + for (var coreProp in Core.prototype) { + if (Core.prototype.hasOwnProperty(coreProp) && !Timeline.prototype.hasOwnProperty(coreProp)) { + Timeline.prototype[coreProp] = Core.prototype[coreProp]; + } + } - var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey; - var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey; - if (ctrlKey || shiftKey) { - this._onMultiSelectItem(event); - return; + if (!(this instanceof Timeline)) { + throw new SyntaxError('Constructor must be called with the new operator'); } - var oldSelection = this.getSelection(); + var me = this; + this.defaultOptions = { + start: null, + end: null, - var item = ItemSet.itemFromTarget(event); - var selection = item ? [item.id] : []; - this.setSelection(selection); + autoResize: true, - var newSelection = this.getSelection(); + orientation: 'bottom', + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - // 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: this.getSelection() - }); - } + // Create the DOM, props, and emitter + this._create(container); - event.stopPropagation(); - }; + // all components listed here will be repainted automatically + this.components = []; - /** - * Handle creation and updates of an item on double tap - * @param event - * @private - */ - ItemSet.prototype._onAddItem = function (event) { - if (!this.options.selectable) return; - if (!this.options.editable.add) 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) + }, + util: { + snap: null, // will be specified after TimeAxis is created + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime : me._toGlobalTime.bind(me) + } + }; - var me = this, - snap = this.body.util.snap || null, - item = ItemSet.itemFromTarget(event); + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - if (item) { - // update item + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - // execute async handler to update the item (or cancel it) - var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset - this.options.onUpdate(itemData, function (itemData) { - if (itemData) { - me.itemsData.update(itemData); - } - }); + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); + + // custom time bar + // Note: time bar will be attached in this.setOptions when selected + this.customTime = new CustomTime(this.body); + this.components.push(this.customTime); + + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // apply options + if (options) { + this.setOptions(options); + } + + // create itemset + if (items) { + this.setItems(items); } else { - // add item - var xAbs = util.getAbsoluteLeft(this.dom.frame); - var x = event.gesture.center.pageX - xAbs; - var start = this.body.util.toTime(x); - var newItem = { - start: snap ? snap(start) : start, - content: 'new item' - }; + this.redraw(); + } + } - // 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) : end; - } + /** + * 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 + */ + Timeline.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; + util.selectiveExtend(fields, this.options, options); - newItem[this.itemsData.fieldId] = util.randomUUID(); + // enable/disable autoResize + this._initAutoResize(); + } - var group = ItemSet.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; - } + // propagate options to all components + this.components.forEach(function (component) { + component.setOptions(options); + }); - // execute async handler to customize (or cancel) adding an item - this.options.onAdd(newItem, function (item) { - if (item) { - me.itemsData.add(newItem); - // TODO: need to trigger a redraw? - } - }); + // TODO: remove deprecation error one day (deprecated since version 0.8.0) + if (options && options.order) { + throw new Error('Option order is deprecated. There is no replacement for this feature.'); } + + // redraw everything + this.redraw(); }; /** - * Handle selecting/deselecting multiple items when holding an item - * @param {Event} event - * @private + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - ItemSet.prototype._onMultiSelectItem = function (event) { - if (!this.options.selectable) return; + Timeline.prototype.setItems = function(items) { + var initialLoad = (this.itemsData == null); - var selection, - item = ItemSet.itemFromTarget(event); + // 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' + } + }); + } - if (item) { - // multi select items - selection = this.getSelection(); // current selection - var index = selection.indexOf(item.id); - if (index == -1) { - // item is not yet selected -> select it - selection.push(item.id); - } - else { - // item is already selected -> deselect it - selection.splice(index, 1); - } - this.setSelection(selection); + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - this.body.emitter.emit('select', { - items: this.getSelection() - }); + if (initialLoad && ('start' in this.options || 'end' in this.options)) { + this.fit(); - event.stopPropagation(); + var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; + var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; + + this.setWindow(start, end); } }; /** - * 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 + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - ItemSet.itemFromTarget = function(event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; - } - target = target.parentNode; + 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 null; + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); }; /** - * 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 + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. */ - ItemSet.groupFromTarget = function(event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-group')) { - return target['timeline-group']; - } - target = target.parentNode; - } + Timeline.prototype.setSelection = function(ids) { + this.itemSet && this.itemSet.setSelection(ids); + }; - return null; + /** + * 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() || []; }; + /** - * 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 + * 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 */ - ItemSet.itemSetFromTarget = function(event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-itemset')) { - return target['timeline-itemset']; + Timeline.prototype.getItemRange = function() { + // calculate min from start filed + var dataset = this.itemsData.getDataSet(), + min = null, + max = null; + + if (dataset) { + // calculate the minimum value of the field 'start' + var minItem = dataset.min('start'); + min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null; + // Note: we convert first to Date and then to number because else + // a conversion from ISODate to Number will fail + + // calculate maximum value of fields 'start' and 'end' + var maxStartItem = dataset.max('start'); + if (maxStartItem) { + max = util.convert(maxStartItem.start, 'Date').valueOf(); + } + var maxEndItem = dataset.max('end'); + if (maxEndItem) { + if (max == null) { + max = util.convert(maxEndItem.end, 'Date').valueOf(); + } + else { + max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf()); + } } - target = target.parentNode; } - return null; + return { + min: (min != null) ? new Date(min) : null, + max: (max != null) ? new Date(max) : null + }; }; - module.exports = ItemSet; + + module.exports = Timeline; /***/ }, -/* 25 */ +/* 18 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var Component = __webpack_require__(18); + // Only load hammer.js when in a browser environment + // (loading hammer.js in a node.js environment gives errors) + if (typeof window !== 'undefined') { + module.exports = window['Hammer'] || __webpack_require__(19); + } + else { + module.exports = function () { + throw Error('hammer.js is only available in a browser, not in node.js.'); + } + } + + +/***/ }, +/* 19 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v1.1.3 - 2014-05-20 + * http://eightmedia.github.io/hammer.js + * + * Copyright (c) 2014 Jorik Tangelder ; + * Licensed under the MIT license */ + + (function(window, undefined) { + 'use strict'; /** - * Legend for Graph2d + * @main + * @module hammer + * + * @class Hammer + * @static */ - function Legend(body, options, side) { - this.body = body; - this.defaultOptions = { - enabled: true, - icons: true, - iconSize: 20, - iconSpacing: 6, - left: { - visible: true, - position: 'top-left' // top/bottom - left,center,right - }, - right: { - visible: true, - position: 'top-left' // top/bottom - left,center,right - } - } - this.side = side; - this.options = util.extend({},this.defaultOptions); - this.svgElements = {}; - this.dom = {}; - this.groups = {}; - this.amountOfGroups = 0; - this._create(); + /** + * Hammer, use this to create instances + * ```` + * var hammertime = new Hammer(myElement); + * ```` + * + * @method Hammer + * @param {HTMLElement} element + * @param {Object} [options={}] + * @return {Hammer.Instance} + */ + var Hammer = function Hammer(element, options) { + return new Hammer.Instance(element, options || {}); + }; - this.setOptions(options); - } + /** + * version, as defined in package.json + * the value will be set at each build + * @property VERSION + * @final + * @type {String} + */ + Hammer.VERSION = '1.1.3'; + + /** + * default settings. + * more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled + * by setting it's name (like `swipe`) to false. + * You can set the defaults for all instances by changing this object before creating an instance. + * @example + * ```` + * Hammer.defaults.drag = false; + * Hammer.defaults.behavior.touchAction = 'pan-y'; + * delete Hammer.defaults.behavior.userSelect; + * ```` + * @property defaults + * @type {Object} + */ + Hammer.defaults = { + /** + * this setting object adds styles and attributes to the element to prevent the browser from doing + * its native behavior. The css properties are auto prefixed for the browsers when needed. + * @property defaults.behavior + * @type {Object} + */ + behavior: { + /** + * Disables text selection to improve the dragging gesture. When the value is `none` it also sets + * `onselectstart=false` for IE on the element. Mainly for desktop browsers. + * @property defaults.behavior.userSelect + * @type {String} + * @default 'none' + */ + userSelect: 'none', - Legend.prototype = new Component(); + /** + * Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming). + * Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event. + * @property defaults.behavior.touchAction + * @type {String} + * @default: 'pan-y' + */ + touchAction: 'pan-y', + /** + * 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. + * @property defaults.behavior.touchCallout + * @type {String} + * @default 'none' + */ + touchCallout: 'none', - Legend.prototype.addGroup = function(label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; - } - this.amountOfGroups += 1; - }; + /** + * Specifies whether zooming is enabled. Used by IE10> + * @property defaults.behavior.contentZooming + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - Legend.prototype.updateGroup = function(label, graphOptions) { - this.groups[label] = graphOptions; - }; + /** + * Specifies that an entire element should be draggable instead of its contents. + * Mainly for desktop browsers. + * @property defaults.behavior.userDrag + * @type {String} + * @default 'none' + */ + userDrag: 'none', - Legend.prototype.removeGroup = function(label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; - } + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in Safari on iPhone. This property obeys the alpha value, if specified. + * + * If you don't specify an alpha value, Safari on iPhone applies a default alpha value + * to the color. To disable tap highlighting, set the alpha value to 0 (invisible). + * If you set the alpha value to 1.0 (opaque), the element is not visible when tapped. + * @property defaults.behavior.tapHighlightColor + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' + } }; - Legend.prototype._create = function() { - this.dom.frame = document.createElement('div'); - this.dom.frame.className = 'legend'; - this.dom.frame.style.position = "absolute"; - this.dom.frame.style.top = "10px"; - this.dom.frame.style.display = "block"; - - this.dom.textArea = document.createElement('div'); - this.dom.textArea.className = 'legendText'; - this.dom.textArea.style.position = "relative"; - this.dom.textArea.style.top = "0px"; - - this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg"); - this.svg.style.position = 'absolute'; - this.svg.style.top = 0 +'px'; - this.svg.style.width = this.options.iconSize + 5 + 'px'; - - this.dom.frame.appendChild(this.svg); - this.dom.frame.appendChild(this.dom.textArea); - }; + /** + * hammer document where the base events are added at + * @property DOCUMENT + * @type {HTMLElement} + * @default window.document + */ + Hammer.DOCUMENT = document; /** - * Hide the component from the DOM + * detect support for pointer events + * @property HAS_POINTEREVENTS + * @type {Boolean} */ - Legend.prototype.hide = function() { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } - }; + Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * detect support for touch events + * @property HAS_TOUCHEVENTS + * @type {Boolean} */ - Legend.prototype.show = function() { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } - }; + Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); - Legend.prototype.setOptions = function(options) { - var fields = ['enabled','orientation','icons','left','right']; - util.selectiveDeepExtend(fields, this.options, options); - }; + /** + * detect mobile browsers + * @property IS_MOBILE + * @type {Boolean} + */ + Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent); - Legend.prototype.redraw = function() { - var activeGroups = 0; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true) { - activeGroups++; - } - } - } + /** + * detect if we want to support mouseevents at all + * @property NO_MOUSEEVENTS + * @type {Boolean} + */ + Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS; - 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 = ''; - } + /** + * interval in which Hammer recalculates current velocity/direction/angle in ms + * @property CALCULATE_INTERVAL + * @type {Number} + * @default 25 + */ + Hammer.CALCULATE_INTERVAL = 25; - if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') { - this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px'; - this.dom.frame.style.bottom = ''; - } - else { - this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px'; - this.dom.frame.style.top = ''; - } + /** + * eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup` + * the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`) + * @property EVENT_TYPES + * @private + * @writeOnce + * @type {Object} + */ + var EVENT_TYPES = {}; - 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(); - } + /** + * direction strings, for safe comparisons + * @property DIRECTION_DOWN|LEFT|UP|RIGHT + * @final + * @type {String} + * @default 'down' 'left' 'up' 'right' + */ + var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down'; + var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left'; + var DIRECTION_UP = Hammer.DIRECTION_UP = 'up'; + var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right'; - var content = ''; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true) { - content += this.groups[groupId].content + '
'; - } - } - } - this.dom.textArea.innerHTML = content; - this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px'; - } - }; + /** + * pointertype strings, for safe comparisons + * @property POINTER_MOUSE|TOUCH|PEN + * @final + * @type {String} + * @default 'mouse' 'touch' 'pen' + */ + var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse'; + var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch'; + var POINTER_PEN = Hammer.POINTER_PEN = 'pen'; - Legend.prototype.drawLegendIcons = function() { - if (this.dom.frame.parentNode) { - DOMutil.prepareElements(this.svgElements); - var padding = window.getComputedStyle(this.dom.frame).paddingTop; - var iconOffset = Number(padding.replace('px','')); - var x = iconOffset; - var iconWidth = this.options.iconSize; - var iconHeight = 0.75 * this.options.iconSize; - var y = iconOffset + 0.5 * iconHeight + 3; + /** + * eventtypes + * @property EVENT_START|MOVE|END|RELEASE|TOUCH + * @final + * @type {String} + * @default 'start' 'change' 'move' 'end' 'release' 'touch' + */ + var EVENT_START = Hammer.EVENT_START = 'start'; + var EVENT_MOVE = Hammer.EVENT_MOVE = 'move'; + var EVENT_END = Hammer.EVENT_END = 'end'; + var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release'; + var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch'; - this.svg.style.width = iconWidth + 5 + iconOffset + 'px'; + /** + * if the window events are set... + * @property READY + * @writeOnce + * @type {Boolean} + * @default false + */ + Hammer.READY = false; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true) { - this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); - y += iconHeight + this.options.iconSpacing; - } - } - } + /** + * plugins namespace + * @property plugins + * @type {Object} + */ + Hammer.plugins = Hammer.plugins || {}; - DOMutil.cleanupElements(this.svgElements); - } - }; + /** + * gestures namespace + * see `/gestures` for the definitions + * @property gestures + * @type {Object} + */ + Hammer.gestures = Hammer.gestures || {}; - module.exports = Legend; + /** + * setup events to detect gestures on the document + * this function is called when creating an new instance + * @private + */ + function setup() { + if(Hammer.READY) { + return; + } + // find what eventtypes we add listeners to + Event.determineEventTypes(); -/***/ }, -/* 26 */ -/***/ function(module, exports, __webpack_require__) { + // Register all gestures inside Hammer.gestures + Utils.each(Hammer.gestures, function(gesture) { + Detection.register(gesture); + }); - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Component = __webpack_require__(18); - var DataAxis = __webpack_require__(21); - var GraphGroup = __webpack_require__(22); - var Legend = __webpack_require__(25); + // Add touch events on the document + Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect); + Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect); - var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + // Hammer is ready...! + Hammer.READY = true; + } /** - * This is the constructor of the LineGraph. It requires a Timeline body and options. + * @module hammer * - * @param body - * @param options - * @constructor + * @class Utils + * @static */ - function LineGraph(body, options) { - this.id = util.randomUUID(); - this.body = body; + var Utils = Hammer.utils = { + /** + * extend method, could also be used for cloning when `dest` is an empty object. + * changes the dest object + * @method extend + * @param {Object} dest + * @param {Object} src + * @param {Boolean} [merge=false] do a merge + * @return {Object} dest + */ + extend: function extend(dest, src, merge) { + for(var key in src) { + if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, - this.defaultOptions = { - yAxisOrientation: 'left', - defaultGroup: 'default', - sort: true, - sampling: true, - graphHeight: '400px', - shaded: { - enabled: false, - orientation: 'bottom' // top, bottom + /** + * simple addEventListener wrapper + * @method on + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + on: function on(element, type, handler) { + element.addEventListener(type, handler, false); }, - style: 'line', // line, bar - barChart: { - width: 50, - handleOverlap: 'overlap', - align: 'center' // left, center, right + + /** + * simple removeEventListener wrapper + * @method off + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + */ + off: function off(element, type, handler) { + element.removeEventListener(type, handler, false); }, - catmullRom: { - enabled: true, - parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) - alpha: 0.5 + + /** + * forEach over arrays and objects + * @method each + * @param {Object|Array} obj + * @param {Function} iterator + * @param {any} iterator.item + * @param {Number} iterator.index + * @param {Object|Array} iterator.obj the source object + * @param {Object} context value to use as `this` in the iterator + */ + each: function each(obj, iterator, context) { + var i, len; + + // native forEach on arrays + if('forEach' in obj) { + obj.forEach(iterator, context); + // arrays + } else if(obj.length !== undefined) { + for(i = 0, len = obj.length; i < len; i++) { + if(iterator.call(context, obj[i], i, obj) === false) { + return; + } + } + // objects + } else { + for(i in obj) { + if(obj.hasOwnProperty(i) && + iterator.call(context, obj[i], i, obj) === false) { + return; + } + } + } }, - drawPoints: { - enabled: true, - size: 6, - style: 'square' // square, circle + + /** + * find if a string contains the string using indexOf + * @method inStr + * @param {String} src + * @param {String} find + * @return {Boolean} found + */ + inStr: function inStr(src, find) { + return src.indexOf(find) > -1; }, - dataAxis: { - showMinorLabels: true, - showMajorLabels: true, - icons: false, - width: '40px', - visible: true, - customRange: { - left: {min:undefined, max:undefined}, - right: {min:undefined, max:undefined} - } + + /** + * find if a array contains the object using indexOf or a simple polyfill + * @method inArray + * @param {String} src + * @param {String} find + * @return {Boolean|Number} false when not found, or the index + */ + inArray: function inArray(src, find) { + if(src.indexOf) { + var index = src.indexOf(find); + return (index === -1) ? false : index; + } else { + for(var i = 0, len = src.length; i < len; i++) { + if(src[i] === find) { + return i; + } + } + return false; + } }, - 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 - } - } - }; - // 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 = {}; + /** + * convert an array-like object (`arguments`, `touchlist`) to an array + * @method toArray + * @param {Object} obj + * @return {Array} + */ + toArray: function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + }, + + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + hasParent: function hasParent(node, parent) { + while(node) { + if(node == parent) { + return true; + } + node = node.parentNode; + } + return false; + }, + + /** + * get the center of all the touches + * @method getCenter + * @param {Array} touches + * @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties + */ + getCenter: function getCenter(touches) { + var pageX = [], + pageY = [], + clientX = [], + clientY = [], + min = Math.min, + max = Math.max; + + // no need to loop when only one touch + if(touches.length === 1) { + return { + pageX: touches[0].pageX, + pageY: touches[0].pageY, + clientX: touches[0].clientX, + clientY: touches[0].clientY + }; + } - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + Utils.each(touches, function(touch) { + pageX.push(touch.pageX); + pageY.push(touch.pageY); + clientX.push(touch.clientX); + clientY.push(touch.clientY); + }); - // listeners for the DataSet of the items - this.itemListeners = { - 'add': function (event, params, senderId) { - me._onAdd(params.items); - }, - 'update': function (event, params, senderId) { - me._onUpdate(params.items); + return { + pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2, + pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2, + clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2, + clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2 + }; }, - 'remove': function (event, params, senderId) { - me._onRemove(params.items); - } - }; - // listeners for the DataSet of the groups - this.groupListeners = { - 'add': function (event, params, senderId) { - me._onAddGroups(params.items); - }, - 'update': function (event, params, senderId) { - me._onUpdateGroups(params.items); + /** + * calculate the velocity between two points. unit is in px per ms. + * @method getVelocity + * @param {Number} deltaTime + * @param {Number} deltaX + * @param {Number} deltaY + * @return {Object} velocity `x` and `y` + */ + getVelocity: function getVelocity(deltaTime, deltaX, deltaY) { + return { + x: Math.abs(deltaX / deltaTime) || 0, + y: Math.abs(deltaY / deltaTime) || 0 + }; }, - 'remove': function (event, params, senderId) { - me._onRemoveGroups(params.items); - } - }; - 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 + /** + * calculate the angle between two coordinates + * @method getAngle + * @param {Touch} touch1 + * @param {Touch} touch2 + * @return {Number} angle + */ + getAngle: function getAngle(touch1, touch2) { + var x = touch2.clientX - touch1.clientX, + y = touch2.clientY - touch1.clientY; - this.svgElements = {}; - this.setOptions(options); - this.groupsUsingDefaultStyles = [0]; + return Math.atan2(y, x) * 180 / Math.PI; + }, - this.body.emitter.on("rangechange",function() { - if (me.lastStart != 0) { - var offset = me.body.range.start - me.lastStart; - var range = me.body.range.end - me.body.range.start; - if (me.width != 0) { - var rangePerPixelInv = me.width/range; - var xOffset = offset * rangePerPixelInv; - me.svg.style.left = (-me.width - xOffset) + "px"; - } - } - }); - this.body.emitter.on("rangechanged", function() { - me.lastStart = me.body.range.start; - me.svg.style.left = util.option.asSize(-me.width); - me._updateGraph.apply(me); - }); + /** + * do a small comparision to get the direction between two touches. + * @method getDirection + * @param {Touch} touch1 + * @param {Touch} touch2 + * @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN` + */ + getDirection: function getDirection(touch1, touch2) { + var x = Math.abs(touch1.clientX - touch2.clientX), + y = Math.abs(touch1.clientY - touch2.clientY); - // create the HTML DOM - this._create(); - this.body.emitter.emit("change"); - } + if(x >= y) { + return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN; + }, - LineGraph.prototype = new Component(); + /** + * calculate the distance between two touches + * @method getDistance + * @param {Touch}touch1 + * @param {Touch} touch2 + * @return {Number} distance + */ + getDistance: function getDistance(touch1, touch2) { + var x = touch2.clientX - touch1.clientX, + y = touch2.clientY - touch1.clientY; - /** - * Create the HTML DOM for the ItemSet - */ - LineGraph.prototype._create = function(){ - var frame = document.createElement('div'); - frame.className = 'LineGraph'; - this.dom.frame = frame; + return Math.sqrt((x * x) + (y * y)); + }, - // 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); + /** + * calculate the scale factor between two touchLists + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @method getScale + * @param {Array} start array of touches + * @param {Array} end array of touches + * @return {Number} scale + */ + getScale: function getScale(start, end) { + // need two fingers... + if(start.length >= 2 && end.length >= 2) { + return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]); + } + return 1; + }, - // data axis - this.options.dataAxis.orientation = 'left'; - this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg); + /** + * calculate the rotation degrees between two touchLists + * @method getRotation + * @param {Array} start array of touches + * @param {Array} end array of touches + * @return {Number} rotation + */ + getRotation: function getRotation(start, end) { + // need two fingers + if(start.length >= 2 && end.length >= 2) { + return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]); + } + return 0; + }, - this.options.dataAxis.orientation = 'right'; - this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg); - delete this.options.dataAxis.orientation; + /** + * find out if the direction is vertical * + * @method isVertical + * @param {String} direction matches `DIRECTION_UP|DOWN` + * @return {Boolean} is_vertical + */ + isVertical: function isVertical(direction) { + return direction == DIRECTION_UP || direction == DIRECTION_DOWN; + }, - // legends - this.legendLeft = new Legend(this.body, this.options.legend, 'left'); - this.legendRight = new Legend(this.body, this.options.legend, 'right'); + /** + * set css properties with their prefixes + * @param {HTMLElement} element + * @param {String} prop + * @param {String} value + * @param {Boolean} [toggle=true] + * @return {Boolean} + */ + setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) { + var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms']; + prop = Utils.toCamelCase(prop); - this.show(); - }; + for(var i = 0; i < prefixes.length; i++) { + var p = prop; + // prefixes + if(prefixes[i]) { + p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1); + } - /** - * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. - * @param options - */ - LineGraph.prototype.setOptions = function(options) { - if (options) { - var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort']; - util.selectiveDeepExtend(fields, this.options, options); - util.mergeOptions(this.options, options,'catmullRom'); - util.mergeOptions(this.options, options,'drawPoints'); - util.mergeOptions(this.options, options,'shaded'); - util.mergeOptions(this.options, options,'legend'); + // test the style + if(p in element.style) { + element.style[p] = (toggle == null || toggle) && value || ''; + break; + } + } + }, - if (options.catmullRom) { - if (typeof options.catmullRom == 'object') { - if (options.catmullRom.parametrization) { - if (options.catmullRom.parametrization == 'uniform') { - this.options.catmullRom.alpha = 0; - } - else if (options.catmullRom.parametrization == 'chordal') { - this.options.catmullRom.alpha = 1.0; - } - else { - this.options.catmullRom.parametrization = 'centripetal'; - this.options.catmullRom.alpha = 0.5; - } + /** + * toggle browser default behavior by setting css properties. + * `userSelect='none'` also sets `element.onselectstart` to false + * `userDrag='none'` also sets `element.ondragstart` to false + * + * @method toggleBehavior + * @param {HtmlElement} element + * @param {Object} props + * @param {Boolean} [toggle=true] + */ + toggleBehavior: function toggleBehavior(element, props, toggle) { + if(!props || !element || !element.style) { + return; } - } - } - if (this.yAxisLeft) { - if (options.dataAxis !== undefined) { - this.yAxisLeft.setOptions(this.options.dataAxis); - this.yAxisRight.setOptions(this.options.dataAxis); - } - } + // set the css properties + Utils.each(props, function(value, prop) { + Utils.setPrefixedCss(element, prop, value, toggle); + }); - if (this.legendLeft) { - if (options.legend !== undefined) { - this.legendLeft.setOptions(this.options.legend); - this.legendRight.setOptions(this.options.legend); - } - } + var falseFn = toggle && function() { + return false; + }; - if (this.groups.hasOwnProperty(UNGROUPED)) { - this.groups[UNGROUPED].setOptions(options); + // also the disable onselectstart + if(props.userSelect == 'none') { + element.onselectstart = falseFn; + } + // and disable ondragstart + if(props.userDrag == 'none') { + element.ondragstart = falseFn; + } + }, + + /** + * convert a string with underscores to camelCase + * so prevent_default becomes preventDefault + * @param {String} str + * @return {String} camelCaseStr + */ + toCamelCase: function toCamelCase(str) { + return str.replace(/[_-]([a-z])/g, function(s) { + return s[1].toUpperCase(); + }); } - } - if (this.dom.frame) { - this._updateGraph(); - } }; + /** - * Hide the component from the DOM + * @module hammer */ - LineGraph.prototype.hide = function() { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } - }; - /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * @class Event + * @static */ - LineGraph.prototype.show = function() { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } - }; + var Event = Hammer.event = { + /** + * when touch events have been fired, this is true + * this is used to stop mouse events + * @property prevent_mouseevents + * @private + * @type {Boolean} + */ + preventMouseEvents: false, + /** + * if EVENT_START has been fired + * @property started + * @private + * @type {Boolean} + */ + started: false, - /** - * Set items - * @param {vis.DataSet | null} items - */ - LineGraph.prototype.setItems = function(items) { - var me = this, - ids, - oldItemsData = this.itemsData; + /** + * when the mouse is hold down, this is true + * @property should_detect + * @private + * @type {Boolean} + */ + shouldDetect: false, - // 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'); - } + /** + * simple event binder with a hook and support for multiple types + * @method on + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + * @param {Function} [hook] + * @param {Object} hook.type + */ + on: function on(element, type, handler, hook) { + var types = type.split(' '); + Utils.each(types, function(type) { + Utils.on(element, type, handler); + hook && hook(type); + }); + }, - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); - }); + /** + * simple event unbinder with a hook and support for multiple types + * @method off + * @param {HTMLElement} element + * @param {String} type + * @param {Function} handler + * @param {Function} [hook] + * @param {Object} hook.type + */ + off: function off(element, type, handler, hook) { + var types = type.split(' '); + Utils.each(types, function(type) { + Utils.off(element, type, handler); + hook && hook(type); + }); + }, - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + /** + * the core touch event handler. + * this finds out if we should to detect gestures + * @method onTouch + * @param {HTMLElement} element + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Function} handler + * @return onTouchHandler {Function} the core event handler + */ + onTouch: function onTouch(element, eventType, handler) { + var self = this; - 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 onTouchHandler = function onTouchHandler(ev) { + var srcType = ev.type.toLowerCase(), + isPointer = Hammer.HAS_POINTEREVENTS, + isMouse = Utils.inStr(srcType, 'mouse'), + triggerType; - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); - } - this._updateUngrouped(); - this._updateGraph(); - this.redraw(); - }; + // if we are in a mouseevent, but there has been a touchevent triggered in this session + // we want to do nothing. simply break out of the event. + if(isMouse && self.preventMouseEvents) { + return; + + // mousebutton must be down + } else if(isMouse && eventType == EVENT_START && ev.button === 0) { + self.preventMouseEvents = false; + self.shouldDetect = true; + } else if(isPointer && eventType == EVENT_START) { + self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev)); + // just a valid start event, but no mouse + } else if(!isMouse && eventType == EVENT_START) { + self.preventMouseEvents = true; + self.shouldDetect = true; + } + + // update the pointer event before entering the detection + if(isPointer && eventType != EVENT_END) { + PointerEvent.updatePointer(eventType, ev); + } + + // we are in a touch/down state, so allowed detection of gestures + if(self.shouldDetect) { + triggerType = self.doDetect.call(self, ev, eventType, element, handler); + } + + // ...and we are done with the detection + // so reset everything to start each detection totally fresh + if(triggerType == EVENT_END) { + self.preventMouseEvents = false; + self.shouldDetect = false; + PointerEvent.reset(); + // update the pointerevent object after the detection + } + + if(isPointer && eventType == EVENT_END) { + PointerEvent.updatePointer(eventType, ev); + } + }; + + this.on(element, EVENT_TYPES[eventType], onTouchHandler); + return onTouchHandler; + }, + + /** + * the core detection method + * this finds out what hammer-touch-events to trigger + * @method doDetect + * @param {Object} ev + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {HTMLElement} element + * @param {Function} handler + * @return {String} triggerType matches `EVENT_START|MOVE|END` + */ + doDetect: function doDetect(ev, eventType, element, handler) { + var touchList = this.getTouchList(ev, eventType); + var touchListLength = touchList.length; + var triggerType = eventType; + var triggerChange = touchList.trigger; // used by fakeMultitouch plugin + var changedLength = touchListLength; + + // at each touchstart-like event we want also want to trigger a TOUCH event... + if(eventType == EVENT_START) { + triggerChange = EVENT_TOUCH; + // ...the same for a touchend-like event + } else if(eventType == EVENT_END) { + triggerChange = EVENT_RELEASE; + + // keep track of how many touches have been removed + changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1); + } - /** - * Set groups - * @param {vis.DataSet} groups - */ - LineGraph.prototype.setGroups = function(groups) { - var me = this, - ids; + // after there are still touches on the screen, + // we just want to trigger a MOVE event. so change the START or END to a MOVE + // but only after detection has been started, the first time we actualy want a START + if(changedLength > 0 && this.started) { + triggerType = EVENT_MOVE; + } - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + // detection has been started, we keep track of this, see above + this.started = true; - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + // generate some event data, some basic information + var evData = this.collectEventData(element, triggerType, touchList, ev); - // 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'); - } + // trigger the triggerType event before the change (TOUCH, RELEASE) events + // but the END event should be at last + if(eventType != EVENT_END) { + handler.call(Detection, evData); + } - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + // trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed + if(triggerChange) { + evData.changedLength = changedLength; + evData.eventType = triggerChange; - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); - } - this._onUpdate(); - }; + handler.call(Detection, evData); + evData.eventType = triggerType; + delete evData.changedLength; + } + // trigger the END event + if(triggerType == EVENT_END) { + handler.call(Detection, evData); - LineGraph.prototype._onUpdate = function(ids) { - this._updateUngrouped(); - this._updateAllGroupData(); - this._updateGraph(); - this.redraw(); - }; - LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);}; - LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);}; - LineGraph.prototype._onUpdateGroups = function (groupIds) { - for (var i = 0; i < groupIds.length; i++) { - var group = this.groupsData.get(groupIds[i]); - this._updateGroup(group, groupIds[i]); - } + // ...and we are done with the detection + // so reset everything to start each detection totally fresh + this.started = false; + } - this._updateGraph(); - this.redraw(); - }; - LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);}; + return triggerType; + }, - 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(); - }; + /** + * we have different events for each device/browser + * determine what we need and set them in the EVENT_TYPES constant + * the `onTouch` method is bind to these properties. + * @method determineEventTypes + * @return {Object} events + */ + determineEventTypes: function determineEventTypes() { + var types; + if(Hammer.HAS_POINTEREVENTS) { + if(window.PointerEvent) { + types = [ + 'pointerdown', + 'pointermove', + 'pointerup pointercancel lostpointercapture' + ]; + } else { + types = [ + 'MSPointerDown', + 'MSPointerMove', + 'MSPointerUp MSPointerCancel MSLostPointerCapture' + ]; + } + } else if(Hammer.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel' + ]; + } else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup' + ]; + } - /** - * update a group object - * - * @param group - * @param groupId - * @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]); - } - } - 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(); - }; + EVENT_TYPES[EVENT_START] = types[0]; + EVENT_TYPES[EVENT_MOVE] = types[1]; + EVENT_TYPES[EVENT_END] = types[2]; + return EVENT_TYPES; + }, - LineGraph.prototype._updateAllGroupData = function () { - if (this.itemsData != null) { - var groupsContent = {}; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - groupsContent[groupId] = []; - } - } - for (var itemId in this.itemsData._data) { - if (this.itemsData._data.hasOwnProperty(itemId)) { - var item = this.itemsData._data[itemId]; - item.x = util.convert(item.x,"Date"); - groupsContent[item.group].push(item); - } - } - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - this.groups[groupId].setItems(groupsContent[groupId]); - } - } - } - }; + /** + * create touchList depending on the event + * @method getTouchList + * @param {Object} ev + * @param {String} eventType + * @return {Array} touches + */ + getTouchList: function getTouchList(ev, eventType) { + // get the fake pointerEvent touchlist + if(Hammer.HAS_POINTEREVENTS) { + return PointerEvent.getTouchList(); + } - /** - * 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 != null) { - // var t0 = new Date(); - var group = {id: UNGROUPED, content: this.options.defaultGroup}; - this._updateGroup(group, UNGROUPED); - var ungroupedCounter = 0; - if (this.itemsData) { - 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; + // get the touchlist + if(ev.touches) { + if(eventType == EVENT_MOVE) { + return ev.touches; } - ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter; - } + + var identifiers = []; + var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches)); + var touchList = []; + + Utils.each(concat, function(touch) { + if(Utils.inArray(identifiers, touch.identifier) === false) { + touchList.push(touch); + } + identifiers.push(touch.identifier); + }); + + return touchList; } - } - } - // much much slower - // var datapoints = this.itemsData.get({ - // filter: function (item) {return item.group === undefined;}, - // showInternalIds:true - // }); - // if (datapoints.length > 0) { - // var updateQuery = []; - // for (var i = 0; i < datapoints.length; i++) { - // updateQuery.push({id:datapoints[i].id, group: UNGROUPED}); - // } - // this.itemsData.update(updateQuery, true); - // } - // var t1 = new Date(); - // var pointInUNGROUPED = this.itemsData.get({filter: function (item) {return item.group == UNGROUPED;}}); - if (ungroupedCounter == 0) { - delete this.groups[UNGROUPED]; - this.legendLeft.removeGroup(UNGROUPED); - this.legendRight.removeGroup(UNGROUPED); - this.yAxisLeft.removeGroup(UNGROUPED); - this.yAxisRight.removeGroup(UNGROUPED); - } - // console.log("getting amount ungrouped",new Date() - t1); - // console.log("putting in ungrouped",new Date() - t0); - } - else { - delete this.groups[UNGROUPED]; - this.legendLeft.removeGroup(UNGROUPED); - this.legendRight.removeGroup(UNGROUPED); - this.yAxisLeft.removeGroup(UNGROUPED); - this.yAxisRight.removeGroup(UNGROUPED); - } + // make fake touchList from mouse position + ev.identifier = 1; + return [ev]; + }, - this.legendLeft.redraw(); - this.legendRight.redraw(); + /** + * collect basic event data + * @method collectEventData + * @param {HTMLElement} element + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Array} touches + * @param {Object} ev + * @return {Object} ev + */ + collectEventData: function collectEventData(element, eventType, touches, ev) { + // find out pointerType + var pointerType = POINTER_TOUCH; + if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) { + pointerType = POINTER_MOUSE; + } else if(PointerEvent.matchType(POINTER_PEN, ev)) { + pointerType = POINTER_PEN; + } + + return { + center: Utils.getCenter(touches), + timeStamp: Date.now(), + target: ev.target, + touches: touches, + eventType: eventType, + pointerType: pointerType, + srcEvent: ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + var srcEvent = this.srcEvent; + srcEvent.preventManipulation && srcEvent.preventManipulation(); + srcEvent.preventDefault && srcEvent.preventDefault(); + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return Detection.stopDetect(); + } + }; + } }; /** - * Redraw the component, mandatory function - * @return {boolean} Returns true if the component is resized + * @module hammer + * + * @class PointerEvent + * @static */ - LineGraph.prototype.redraw = function() { - var resized = false; + var PointerEvent = Hammer.PointerEvent = { + /** + * holds all pointers, by `identifier` + * @property pointers + * @type {Object} + */ + pointers: {}, - this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px'; - if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) { - resized = true; - } - // check if this component is resized - resized = this._isResized() || resized; - // check whether zoomed (in that case we need to re-stack everything) - var visibleInterval = this.body.range.end - this.body.range.start; - var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth); - this.lastVisibleInterval = visibleInterval; - this.lastWidth = this.width; + /** + * get the pointers as an array + * @method getTouchList + * @return {Array} touchlist + */ + getTouchList: function getTouchList() { + var touchlist = []; + // we can use forEach since pointerEvents only is in IE10 + Utils.each(this.pointers, function(pointer) { + touchlist.push(pointer); + }); + return touchlist; + }, - // calculate actual size and position - this.width = this.dom.frame.offsetWidth; + /** + * update the position of a pointer + * @method updatePointer + * @param {String} eventType matches `EVENT_START|MOVE|END` + * @param {Object} pointerEvent + */ + updatePointer: function updatePointer(eventType, pointerEvent) { + if(eventType == EVENT_END || (eventType != EVENT_END && pointerEvent.buttons !== 1)) { + delete this.pointers[pointerEvent.pointerId]; + } else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + }, - // the svg element is three times as big as the width, this allows for fully dragging left and right - // without reloading the graph. the controls for this are bound to events in the constructor - if (resized == true) { - this.svg.style.width = util.option.asSize(3*this.width); - this.svg.style.left = util.option.asSize(-this.width); - } - if (zoomed == true) { - this._updateGraph(); - } + /** + * check if ev matches pointertype + * @method matchType + * @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN` + * @param {PointerEvent} ev + */ + matchType: function matchType(pointerType, ev) { + if(!ev.pointerType) { + return false; + } - this.legendLeft.redraw(); - this.legendRight.redraw(); + var pt = ev.pointerType, + types = {}; - return resized; + types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE)); + types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH)); + types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN)); + return types[pointerType]; + }, + + /** + * reset the stored pointers + * @method reset + */ + reset: function resetList() { + this.pointers = {}; + } }; + /** - * Update and redraw the graph. + * @module hammer * + * @class Detection + * @static */ - LineGraph.prototype._updateGraph = function () { - // reset the svg elements - DOMutil.prepareElements(this.svgElements); - if (this.width != 0 && this.itemsData != null) { - var group, i; - var preprocessedGroupData = {}; - var processedGroupData = {}; - var groupRanges = {}; - var changeCalled = false; + var Detection = Hammer.detection = { + // contains all registred Hammer.gestures in the correct order + gestures: [], - // getting group Ids - var groupIds = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - group = this.groups[groupId]; - if (group.visible == true) { - groupIds.push(groupId); + // data of the current Hammer.gesture detection session + current: null, + + // the previous Hammer.gesture session data + // is a full clone of the previous gesture.current object + previous: null, + + // when this becomes true, no gestures are fired + stopped: false, + + /** + * start Hammer.gesture detection + * @method startDetect + * @param {Hammer.Instance} inst + * @param {Object} eventData + */ + startDetect: function startDetect(inst, eventData) { + // already busy with a Hammer.gesture detection on an element + if(this.current) { + return; } - } - } - if (groupIds.length > 0) { - // this is the range of the SVG canvas - var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width); - var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width); - var groupsData = {}; - // fill groups data - this._getRelevantData(groupIds, groupsData, minDate, maxDate); - // we transform the X coordinates to detect collisions - for (i = 0; i < groupIds.length; i++) { - preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); - } - // now all needed data has been collected we start the processing. - this._getYRanges(groupIds, preprocessedGroupData, groupRanges); - // update the Y axis first, we use this data to draw at the correct Y points - // changeCalled is required to clean the SVG on a change emit. - changeCalled = this._updateYAxis(groupIds, groupRanges); - if (changeCalled == true) { - DOMutil.cleanupElements(this.svgElements); - this.body.emitter.emit("change"); - return; - } + this.stopped = false; - // 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); - } + // holds current session + this.current = { + inst: inst, // reference to HammerInstance we're working for + startEvent: Utils.extend({}, eventData), // start eventData for distances, timing etc + lastEvent: false, // last eventData + lastCalcEvent: false, // last eventData for calculations. + futureCalcEvent: false, // last eventData for calculations. + lastCalcData: {}, // last lastCalcData + name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc + }; + this.detect(eventData); + }, - // draw the groups - for (i = 0; i < groupIds.length; i++) { - group = this.groups[groupIds[i]]; - if (group.options.style == 'line') { - this._drawLineGraph(processedGroupData[groupIds[i]], group); + /** + * Hammer.gesture detection + * @method detect + * @param {Object} eventData + * @return {any} + */ + detect: function detect(eventData) { + if(!this.current || this.stopped) { + return; } - } - this._drawBarGraphs(groupIds, processedGroupData); - } - } - // cleanup unused svg elements - DOMutil.cleanupElements(this.svgElements); - }; + // extend event data with calculations about scale, distance etc + eventData = this.extendEventData(eventData); + // hammer instance and instance options + var inst = this.current.inst, + instOptions = inst.options; - LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { - // first select and preprocess the data from the datasets. - // the groups have their preselection of data, we now loop over this data to see - // what data we need to draw. Sorted data is much faster. - // more optimization is possible by doing the sampling before and using the binary search - // to find the end date to determine the increment. - var group; - if (groupIds.length > 0) { - for (var i = 0; i < groupIds.length; i++) { - group = this.groups[groupIds[i]]; - groupsData[groupIds[i]] = []; - var dataContainer = groupsData[groupIds[i]]; - // optimization for sorted data - if (group.options.sort == true) { - var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before')); - for (var j = guess; j < group.itemsData.length; j++) { - var item = group.itemsData[j]; - if (item !== undefined) { - if (item.x > maxDate) { - dataContainer.push(item); - break; - } - else { - dataContainer.push(item); + // call Hammer.gesture handlers + Utils.each(this.gestures, function triggerGesture(gesture) { + // only when the instance options have enabled this gesture + if(!this.stopped && inst.enabled && instOptions[gesture.name]) { + gesture.handler.call(gesture, eventData, inst); } - } + }, this); + + // store as previous event event + if(this.current) { + this.current.lastEvent = eventData; } - } - else { - for (var j = 0; j < group.itemsData.length; j++) { - var item = group.itemsData[j]; - if (item !== undefined) { - if (item.x > minDate && item.x < maxDate) { - dataContainer.push(item); - } - } + + if(eventData.eventType == EVENT_END) { + this.stopDetect(); } - } - } - } - this._applySampling(groupIds, groupsData); - }; + return eventData; + }, - 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]]; - var increment = 1; - var amountOfPoints = dataContainer.length; + /** + * clear the Hammer.gesture vars + * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected + * to stop other Hammer.gestures from being fired + * @method stopDetect + */ + stopDetect: function stopDetect() { + // clone current data to the store as the previous gesture + // used for the double tap gesture, since this is an other gesture detect session + this.previous = Utils.extend({}, this.current); - // 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))); + // reset the current + this.current = null; + this.stopped = true; + }, - var sampledData = []; - for (var j = 0; j < amountOfPoints; j += increment) { - sampledData.push(dataContainer[j]); + /** + * calculate velocity, angle and direction + * @method getVelocityData + * @param {Object} ev + * @param {Object} center + * @param {Number} deltaTime + * @param {Number} deltaX + * @param {Number} deltaY + */ + getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) { + var cur = this.current, + recalc = false, + calcEv = cur.lastCalcEvent, + calcData = cur.lastCalcData; + if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) { + center = calcEv.center; + deltaTime = ev.timeStamp - calcEv.timeStamp; + deltaX = ev.center.clientX - calcEv.center.clientX; + deltaY = ev.center.clientY - calcEv.center.clientY; + recalc = true; } - groupsData[groupIds[i]] = sampledData; - } - } - } - }; - LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { - var groupData, group; - var barCombinedDataLeft = []; - var barCombinedDataRight = []; - var barCombinedData; - if (groupIds.length > 0) { - for (var i = 0; i < groupIds.length; i++) { - groupData = groupsData[groupIds[i]]; - group = this.groups[groupIds[i]]; - if (group.options.style == 'line' || group.options.barChart.handleOverlap != "stack") { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (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; - } - groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation}; - } - else if (group.options.style == 'bar') { - if (group.options.yAxisOrientation == 'left') { - barCombinedData = barCombinedDataLeft; - } - else { - barCombinedData = barCombinedDataRight; + if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { + cur.futureCalcEvent = ev; } - groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true}; + if(!cur.lastCalcEvent || recalc) { + calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY); + calcData.angle = Utils.getAngle(center, ev.center); + calcData.direction = Utils.getDirection(center, ev.center); - // combine data - for (var j = 0; j < groupData.length; j++) { - barCombinedData.push({ - x: groupData[j].x, - y: groupData[j].y, - groupId: groupIds[i] - }); - } - } - } - if (barCombinedDataLeft.length > 0) { - // sort by time and by group - barCombinedDataLeft.sort(function (a, b) { - if (a.x == b.x) { - return a.groupId - b.groupId; - } else { - return a.x - b.x; - } - }) - var intersections = {}; - this._getDataIntersections(intersections, barCombinedDataLeft); - groupRanges["__barchartLeft"] = this._getStackedBarYRange(intersections, barCombinedDataLeft); - groupRanges["__barchartLeft"].yAxisOrientation = "left"; - groupIds.push("__barchartLeft"); - } - if (barCombinedDataRight.length > 0) { - // sort by time and by group - barCombinedDataRight.sort(function (a, b) { - if (a.x == b.x) { - return a.groupId - b.groupId; - } else { - return a.x - b.x; + cur.lastCalcEvent = cur.futureCalcEvent || ev; + cur.futureCalcEvent = ev; } - }) - var intersections = {}; - this._getDataIntersections(intersections, barCombinedDataRight); - groupRanges["__barchartRight"] = this._getStackedBarYRange(intersections, barCombinedDataRight); - groupRanges["__barchartRight"].yAxisOrientation = "right"; - groupIds.push("__barchartRight"); - } - } - }; - LineGraph.prototype._getStackedBarYRange = function (intersections, combinedData) { - var key; - var yMin = combinedData[0].y; - var yMax = combinedData[0].y; - for (var i = 0; i < combinedData.length; i++) { - key = combinedData[i].x; - if (intersections[key] === undefined) { - yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin; - yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax; - } - else { - intersections[key].accumulated += combinedData[i].y; - } - } - for (var xpos in intersections) { - if (intersections.hasOwnProperty(xpos)) { - yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin; - yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax; - } - } + ev.velocityX = calcData.velocity.x; + ev.velocityY = calcData.velocity.y; + ev.interimAngle = calcData.angle; + ev.interimDirection = calcData.direction; + }, - return {min: yMin, max: yMax}; - }; + /** + * extend eventData for Hammer.gestures + * @method extendEventData + * @param {Object} ev + * @return {Object} ev + */ + extendEventData: function extendEventData(ev) { + var cur = this.current, + startEv = cur.startEvent, + lastEv = cur.lastEvent || startEv; + // update the start touchlist to calculate the scale/rotation + if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { + startEv.touches = []; + Utils.each(ev.touches, function(touch) { + startEv.touches.push({ + clientX: touch.clientX, + clientY: touch.clientY + }); + }); + } - /** - * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden. - * @param {array} groupIds - * @private - */ - LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) { - var changeCalled = false; - var yAxisLeftUsed = false; - var yAxisRightUsed = false; - var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal; + var deltaTime = ev.timeStamp - startEv.timeStamp, + deltaX = ev.center.clientX - startEv.center.clientX, + deltaY = ev.center.clientY - startEv.center.clientY; - // if groups are present - if (groupIds.length > 0) { - for (var i = 0; i < groupIds.length; i++) { - if (groupRanges[groupIds[i]].ignore !== true) { - minVal = groupRanges[groupIds[i]].min; - maxVal = groupRanges[groupIds[i]].max; + this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY); - if (groupRanges[groupIds[i]].yAxisOrientation == 'left') { - yAxisLeftUsed = true; - minLeft = minLeft > minVal ? minVal : minLeft; - maxLeft = maxLeft < maxVal ? maxVal : maxLeft; - } - else { - yAxisRightUsed = true; - minRight = minRight > minVal ? minVal : minRight; - maxRight = maxRight < maxVal ? maxVal : maxRight; - } - } - } + Utils.extend(ev, { + startEvent: startEv, - if (yAxisLeftUsed == true) { - this.yAxisLeft.setRange(minLeft, maxLeft); - } - if (yAxisRightUsed == true) { - this.yAxisRight.setRange(minRight, maxRight); - } - } + deltaTime: deltaTime, + deltaX: deltaX, + deltaY: deltaY, - changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled; - changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled; + distance: Utils.getDistance(startEv.center, ev.center), + angle: Utils.getAngle(startEv.center, ev.center), + direction: Utils.getDirection(startEv.center, ev.center), + scale: Utils.getScale(startEv.touches, ev.touches), + rotation: Utils.getRotation(startEv.touches, ev.touches) + }); - if (yAxisRightUsed == true && yAxisLeftUsed == true) { - this.yAxisLeft.drawIcons = true; - this.yAxisRight.drawIcons = true; - } - else { - this.yAxisLeft.drawIcons = false; - this.yAxisRight.drawIcons = false; - } + return ev; + }, - this.yAxisRight.master = !yAxisLeftUsed; + /** + * register new gesture + * @method register + * @param {Object} gesture object, see `gestures/` for documentation + * @return {Array} gestures + */ + register: function register(gesture) { + // add an enable gesture options if there is no given + var options = gesture.defaults || {}; + if(options[gesture.name] === undefined) { + options[gesture.name] = true; + } - if (this.yAxisRight.master == false) { - if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;} - else {this.yAxisLeft.lineOffset = 0;} + // extend Hammer default options with the Hammer.gesture options + Utils.extend(Hammer.defaults, options, true); - changeCalled = this.yAxisLeft.redraw() || changeCalled; - this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels; - changeCalled = this.yAxisRight.redraw() || changeCalled; - } - else { - changeCalled = this.yAxisRight.redraw() || changeCalled; - } + // set its index + gesture.index = gesture.index || 1000; - // clean the accumulated lists - if (groupIds.indexOf("__barchartLeft") != -1) { - groupIds.splice(groupIds.indexOf("__barchartLeft"),1); - } - if (groupIds.indexOf("__barchartRight") != -1) { - groupIds.splice(groupIds.indexOf("__barchartRight"),1); - } + // add Hammer.gesture to the list + this.gestures.push(gesture); - return changeCalled; - }; + // sort the list by index + this.gestures.sort(function(a, b) { + if(a.index < b.index) { + return -1; + } + if(a.index > b.index) { + return 1; + } + return 0; + }); - /** - * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function - * - * @param {boolean} axisUsed - * @returns {boolean} - * @private - * @param axis - */ - LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { - var changed = false; - if (axisUsed == false) { - if (axis.dom.frame.parentNode) { - axis.hide(); - changed = true; - } - } - else { - if (!axis.dom.frame.parentNode) { - axis.show(); - changed = true; + return this.gestures; } - } - return changed; }; /** - * draw a bar graph - * @param datapoints - * @param group + * @module hammer */ - LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) { - var combinedData = []; - var intersections = {}; - var coreDistance; - var key; - var group; - var i,j; - var barPoints = 0; - // combine all barchart data - for (i = 0; i < groupIds.length; i++) { - group = this.groups[groupIds[i]]; - if (group.options.style == 'bar') { - if (group.visible == true) { - for (j = 0; j < processedGroupData[groupIds[i]].length; j++) { - combinedData.push({ - x: processedGroupData[groupIds[i]][j].x, - y: processedGroupData[groupIds[i]][j].y, - groupId: groupIds[i] - }); - barPoints += 1; - } - } - } - } + /** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * + * @class Instance + * @constructor + * @param {HTMLElement} element + * @param {Object} [options={}] options are merged with `Hammer.defaults` + * @return {Hammer.Instance} + */ + Hammer.Instance = function(element, options) { + var self = this; - if (barPoints == 0) {return;} + // setup HammerJS window events and register all gestures + // this also sets up the default options + setup(); - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x == b.x) { - return a.groupId - b.groupId; - } else { - return a.x - b.x; - } - }); + /** + * @property element + * @type {HTMLElement} + */ + this.element = element; - // get intersections - this._getDataIntersections(intersections, combinedData); + /** + * @property enabled + * @type {Boolean} + * @protected + */ + this.enabled = true; - // plot barchart - for (i = 0; i < combinedData.length; i++) { - group = this.groups[combinedData[i].groupId]; - var minWidth = 0.1 * group.options.barChart.width; + /** + * options, merged with the defaults + * options with an _ are converted to camelCase + * @property options + * @type {Object} + */ + Utils.each(options, function(value, name) { + delete options[name]; + options[Utils.toCamelCase(name)] = value; + }); - 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));} - var drawData = this._getSafeDrawData(coreDistance, group, minWidth); - } - else { - var nextKey = i + (intersections[key].amount - intersections[key].resolved); - var prevKey = i - (intersections[key].resolved + 1); - if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);} - if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));} - var drawData = this._getSafeDrawData(coreDistance, group, minWidth); - intersections[key].resolved += 1; + this.options = Utils.extend(Utils.extend({}, Hammer.defaults), options || {}); - if (group.options.barChart.handleOverlap == 'stack') { - heightOffset = intersections[key].accumulated; - intersections[key].accumulated += group.zeroPosition - combinedData[i].y; - } - else if (group.options.barChart.handleOverlap == 'sideBySide') { - drawData.width = drawData.width / intersections[key].amount; - drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1)); - if (group.options.barChart.align == 'left') {offset -= 0.5*drawData.width;} - else if (group.options.barChart.align == 'right') {offset += 0.5*drawData.width;} - } - } - DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg); - // draw points - if (group.options.drawPoints.enabled == true) { - DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg); + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.behavior) { + Utils.toggleBehavior(this.element, this.options.behavior, true); } - } - }; + /** + * event start handler on the element to start the detection + * @property eventStartHandler + * @type {Object} + */ + this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) { + if(self.enabled && ev.eventType == EVENT_START) { + Detection.startDetect(self, ev); + } else if(ev.eventType == EVENT_TOUCH) { + Detection.detect(ev); + } + }); - LineGraph.prototype._getDataIntersections = function (intersections, combinedData) { - // get intersections - var coreDistance; - for (var i = 0; i < combinedData.length; i++) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); - } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); - } - if (coreDistance == 0) { - if (intersections[combinedData[i].x] === undefined) { - intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0}; - } - intersections[combinedData[i].x].amount += 1; - } - } + /** + * keep a list of user event handlers which needs to be removed when calling 'dispose' + * @property eventHandlers + * @type {Array} + */ + this.eventHandlers = []; }; - //LineGraph.prototype._accumulate = function (intersections, combinedData) { + Hammer.Instance.prototype = { + /** + * bind events to the instance + * @method on + * @chainable + * @param {String} gestures multiple gestures by splitting with a space + * @param {Function} handler + * @param {Object} handler.ev event object + */ + on: function onEvent(gestures, handler) { + var self = this; + Event.on(self.element, gestures, handler, function(type) { + self.eventHandlers.push({ gesture: type, handler: handler }); + }); + return self; + }, + + /** + * unbind events to the instance + * @method off + * @chainable + * @param {String} gestures + * @param {Function} handler + */ + off: function offEvent(gestures, handler) { + var self = this; + Event.off(self.element, gestures, handler, function(type) { + var index = Utils.inArray({ gesture: type, handler: handler }); + if(index !== false) { + self.eventHandlers.splice(index, 1); + } + }); + return self; + }, + /** + * trigger gesture event + * @method trigger + * @chainable + * @param {String} gesture + * @param {Object} [eventData] + */ + trigger: function triggerEvent(gesture, eventData) { + // optional + if(!eventData) { + eventData = {}; + } - LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) { - var width, offset; - if (coreDistance < group.options.barChart.width && coreDistance > 0) { - width = coreDistance < minWidth ? minWidth : coreDistance; + // create DOM event + var event = Hammer.DOCUMENT.createEvent('Event'); + event.initEvent(gesture, true, true); + event.gesture = eventData; - 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 { - // no collisions, plot with 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; - } - } + // trigger on the target if it is in the instance element, + // this is for event delegation tricks + var element = this.element; + if(Utils.hasParent(eventData.target, element)) { + element = eventData.target; + } - return {width: width, offset: offset}; - }; + element.dispatchEvent(event); + return this; + }, + /** + * enable of disable hammer.js detection + * @method enable + * @chainable + * @param {Boolean} state + */ + enable: function enable(state) { + this.enabled = state; + return this; + }, - /** - * draw a line graph - * - * @param datapoints - * @param group - */ - LineGraph.prototype._drawLineGraph = function (dataset, group) { - if (dataset != null) { - if (dataset.length > 0) { - var path, d; - var svgHeight = Number(this.svg.style.height.replace("px","")); - path = DOMutil.getSVGElement('path', this.svgElements, this.svg); - path.setAttributeNS(null, "class", group.className); + /** + * dispose this hammer instance + * @method dispose + * @return {Null} + */ + dispose: function dispose() { + var i, eh; - // construct path from dataset - if (group.options.catmullRom.enabled == true) { - d = this._catmullRom(dataset, group); - } - else { - d = this._linear(dataset); - } + // undo all changes made by stop_browser_behavior + Utils.toggleBehavior(this.element, this.options.behavior, false); - // append with points for fill and finalize the path - if (group.options.shaded.enabled == true) { - var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg); - var dFill; - if (group.options.shaded.orientation == 'top') { - dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0; - } - else { - dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight; + // unbind all custom event handlers + for(i = -1; (eh = this.eventHandlers[++i]);) { + Utils.off(this.element, eh.gesture, eh.handler); } - fillPath.setAttributeNS(null, "class", group.className + " fill"); - fillPath.setAttributeNS(null, "d", dFill); - } - // copy properties to path for drawing. - path.setAttributeNS(null, "d", "M" + d); - // draw points - if (group.options.drawPoints.enabled == true) { - this._drawPoints(dataset, group, this.svgElements, this.svg); - } + this.eventHandlers = []; + + // unbind the start event listener + Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler); + + return null; } - } }; + /** - * draw the data points - * - * @param dataset - * @param JSONcontainer - * @param svg - * @param group + * @module gestures */ - LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) { - if (offset === undefined) {offset = 0;} - for (var i = 0; i < dataset.length; i++) { - DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg); - } - }; - - - /** - * 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. + * Move with x fingers (default 1) around on the page. + * Preventing the default browser behavior is a good way to improve feel and working. + * ```` + * hammertime.on("drag", function(ev) { + * console.log(ev); + * ev.gesture.preventDefault(); + * }); + * ```` * - * @param datapoints - * @returns {Array} - * @private + * @class Drag + * @static */ - LineGraph.prototype._convertXcoordinates = function (datapoints) { - var extractedData = []; - var xValue, yValue; - var toScreen = this.body.util.toScreen; - - for (var i = 0; i < datapoints.length; i++) { - xValue = toScreen(datapoints[i].x) + this.width - 1; - yValue = datapoints[i].y; - extractedData.push({x: xValue, y: yValue}); - } - - return extractedData; - }; - - - /** - * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the - * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for - * the yAxis. - * - * @param datapoints - * @returns {Array} - * @private + * @event drag + * @param {Object} ev */ - 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; - } - - for (var i = 0; i < datapoints.length; i++) { - xValue = toScreen(datapoints[i].x) + this.width - 1; - yValue = Math.round(axis.convertValue(datapoints[i].y)); - extractedData.push({x: xValue, y: yValue}); - } - - group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); - - return extractedData; - }; - /** - * This uses an uniform parametrization of the CatmullRom algorithm: - * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al. - * @param data - * @returns {string} - * @private + * @event dragstart + * @param {Object} ev + */ + /** + * @event dragend + * @param {Object} ev + */ + /** + * @event drapleft + * @param {Object} ev */ - LineGraph.prototype._catmullRomUniform = function(data) { - // catmull rom - var p0, p1, p2, p3, bp1, bp2; - var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; - var normalization = 1/6; - var length = data.length; - for (var i = 0; i < length - 1; i++) { - - p0 = (i == 0) ? data[0] : data[i-1]; - p1 = data[i]; - p2 = data[i+1]; - p3 = (i + 2 < length) ? data[i+2] : p2; - - - // 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 - - // 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 }; - - d += "C" + - bp1.x + "," + - bp1.y + " " + - bp2.x + "," + - bp2.y + " " + - p2.x + "," + - p2.y + " "; - } - - return d; - }; - /** - * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm. - * By default, the centripetal parameterization is used because this gives the nicest results. - * These parameterizations are relatively heavy because the distance between 4 points have to be calculated. - * - * One optimization can be used to reuse distances since this is a sliding window approach. - * @param data - * @returns {string} - * @private + * @event dragright + * @param {Object} ev + */ + /** + * @event dragup + * @param {Object} ev + */ + /** + * @event dragdown + * @param {Object} ev */ - LineGraph.prototype._catmullRom = function(data, group) { - var alpha = group.options.catmullRom.alpha; - if (alpha == 0 || alpha === undefined) { - return this._catmullRomUniform(data); - } - else { - var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M; - var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA; - var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; - var length = data.length; - for (var i = 0; i < length - 1; i++) { - p0 = (i == 0) ? data[0] : data[i-1]; - p1 = data[i]; - p2 = data[i+1]; - p3 = (i + 2 < length) ? data[i+2] : p2; + /** + * @param {String} name + */ + (function(name) { + var triggered = false; - 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)); + function dragGesture(ev, inst) { + var cur = Detection.current; - // 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 ] + // max touches + if(inst.options.dragMaxTouches > 0 && + ev.touches.length > inst.options.dragMaxTouches) { + return; + } - // [ 0 1 0 0 ] - // [ -d2pow2a/N A/N d1pow2a/N 0 ] - // [ 0 d3pow2a/M B/M -d2pow2a/M ] - // [ 0 0 1 0 ] + switch(ev.eventType) { + case EVENT_START: + triggered = false; + break; - 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); + case EVENT_MOVE: + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.distance < inst.options.dragMinDistance && + cur.name != name) { + return; + } - 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;} + var startCenter = cur.startEvent.center; - bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N), - y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)}; + // we are dragging! + if(cur.name != name) { + cur.name = name; + if(inst.options.dragDistanceCorrection && ev.distance > 0) { + // When a drag is triggered, set the event center to dragMinDistance pixels from the original event center. + // Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0. + // It might be useful to save the original start point somewhere + var factor = Math.abs(inst.options.dragMinDistance / ev.distance); + startCenter.pageX += ev.deltaX * factor; + startCenter.pageY += ev.deltaY * factor; + startCenter.clientX += ev.deltaX * factor; + startCenter.clientY += ev.deltaY * factor; - bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M), - y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)}; + // recalculate event data using new start point + ev = Detection.extendEventData(ev); + } + } - 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 + " "; - } + // lock drag to axis? + if(cur.lastEvent.dragLockToAxis || + ( inst.options.dragLockToAxis && + inst.options.dragLockMinDistance <= ev.distance + )) { + ev.dragLockToAxis = true; + } - return d; - } - }; + // keep direction on the axis that the drag gesture started on + var lastDirection = cur.lastEvent.direction; + if(ev.dragLockToAxis && lastDirection !== ev.direction) { + if(Utils.isVertical(lastDirection)) { + ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN; + } else { + ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + } - /** - * this generates the SVG path for a linear drawing between datapoints. - * @param data - * @returns {string} - * @private - */ - LineGraph.prototype._linear = function(data) { - // linear - var d = ""; - for (var i = 0; i < data.length; i++) { - if (i == 0) { - d += data[i].x + "," + data[i].y; - } - else { - d += " " + data[i].x + "," + data[i].y; - } - } - return d; - }; + // first time, trigger dragstart event + if(!triggered) { + inst.trigger(name + 'start', ev); + triggered = true; + } - module.exports = LineGraph; + // trigger events + inst.trigger(name, ev); + inst.trigger(name + ev.direction, ev); + var isVertical = Utils.isVertical(ev.direction); -/***/ }, -/* 27 */ -/***/ function(module, exports, __webpack_require__) { + // block the browser events + if((inst.options.dragBlockVertical && isVertical) || + (inst.options.dragBlockHorizontal && !isVertical)) { + ev.preventDefault(); + } + break; - var util = __webpack_require__(1); - var Component = __webpack_require__(18); - var TimeStep = __webpack_require__(17); - var moment = __webpack_require__(40); + case EVENT_RELEASE: + if(triggered && ev.changedLength <= inst.options.dragMaxTouches) { + inst.trigger(name + 'end', ev); + triggered = false; + } + break; - /** - * 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, - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [], - redundant: { - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [] + case EVENT_END: + triggered = false; + break; + } } - }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 - }, - lineTop: 0 - }; - this.defaultOptions = { - orientation: 'bottom', // supported: 'top', 'bottom' - // TODO: implement timeaxis orientations 'left' and 'right' - showMinorLabels: true, - showMajorLabels: true - }; - this.options = util.extend({}, this.defaultOptions); + Hammer.gestures.Drag = { + name: name, + index: 50, + handler: dragGesture, + defaults: { + /** + * minimal movement that have to be made before the drag event gets triggered + * @property dragMinDistance + * @type {Number} + * @default 10 + */ + dragMinDistance: 10, - this.body = body; + /** + * Set dragDistanceCorrection to true to make the starting point of the drag + * be calculated from where the drag was triggered, not from where the touch started. + * Useful to avoid a jerk-starting drag, which can make fine-adjustments + * through dragging difficult, and be visually unappealing. + * @property dragDistanceCorrection + * @type {Boolean} + * @default true + */ + dragDistanceCorrection: true, + + /** + * set 0 for unlimited, but this can conflict with transform + * @property dragMaxTouches + * @type {Number} + * @default 1 + */ + dragMaxTouches: 1, + + /** + * prevent default browser behavior when dragging occurs + * be careful with it, it makes the element a blocking element + * when you are using the drag gesture, it is a good practice to set this true + * @property dragBlockHorizontal + * @type {Boolean} + * @default false + */ + dragBlockHorizontal: false, - // create the HTML DOM - this._create(); + /** + * same as `dragBlockHorizontal`, but for vertical movement + * @property dragBlockVertical + * @type {Boolean} + * @default false + */ + dragBlockVertical: false, - this.setOptions(options); - } + /** + * dragLockToAxis keeps the drag gesture on the axis that it started on, + * It disallows vertical directions if the initial direction was horizontal, and vice versa. + * @property dragLockToAxis + * @type {Boolean} + * @default false + */ + dragLockToAxis: false, - TimeAxis.prototype = new Component(); + /** + * drag lock only kicks in when distance > dragLockMinDistance + * This way, locking occurs only when the distance has become large enough to reliably determine the direction + * @property dragLockMinDistance + * @type {Number} + * @default 25 + */ + dragLockMinDistance: 25 + } + }; + })('drag'); /** - * Set options for the TimeAxis. - * Parameters will be merged in current options. - * @param {Object} options Available options: - * {string} [orientation] - * {boolean} [showMinorLabels] - * {boolean} [showMajorLabels] + * @module gestures */ - TimeAxis.prototype.setOptions = function(options) { - if (options) { - // copy all options that we know - util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options); - - // 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); - } + /** + * trigger a simple gesture event, so you can do anything in your handler. + * only usable if you know what your doing... + * + * @class Gesture + * @static + */ + /** + * @event gesture + * @param {Object} ev + */ + Hammer.gestures.Gesture = { + name: 'gesture', + index: 1337, + handler: function releaseGesture(ev, inst) { + inst.trigger(this.name, ev); } - } }; /** - * Create the HTML DOM for the TimeAxis + * @module gestures */ - TimeAxis.prototype._create = function() { - this.dom.foreground = document.createElement('div'); - this.dom.background = document.createElement('div'); - - this.dom.foreground.className = 'timeaxis foreground'; - this.dom.background.className = 'timeaxis background'; - }; - /** - * Destroy the TimeAxis + * Touch stays at the same place for x time + * + * @class Hold + * @static */ - 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; - }; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * @event hold + * @param {Object} ev */ - TimeAxis.prototype.redraw = function () { - var options = this.options, - props = this.props, - foreground = this.dom.foreground, - background = this.dom.background; - // determine the correct parent DOM element (depending on option orientation) - var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom; - var parentChanged = (foreground.parentNode !== parent); + /** + * @param {String} name + */ + (function(name) { + var timer; - // calculate character width and height - this._calculateCharSize(); + function holdGesture(ev, inst) { + var options = inst.options, + current = Detection.current; - // TODO: recalculate sizes only needed when parent is resized or options is changed - var orientation = this.options.orientation, - showMinorLabels = this.options.showMinorLabels, - showMajorLabels = this.options.showMajorLabels; + switch(ev.eventType) { + case EVENT_START: + clearTimeout(timer); - // 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; + // set the gesture so we can check in the timeout if it still is + current.name = name; - props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - - (options.orientation == '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 + // set timer and if after the timeout it still is hold, + // we trigger the hold event + timer = setTimeout(function() { + if(current && current.name == name) { + inst.trigger(name, ev); + } + }, options.holdTimeout); + break; - // 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); + case EVENT_MOVE: + if(ev.distance > options.holdThreshold) { + clearTimeout(timer); + } + break; - foreground.style.height = this.props.height + 'px'; + case EVENT_RELEASE: + clearTimeout(timer); + break; + } + } - this._repaintLabels(); + Hammer.gestures.Hold = { + name: name, + index: 10, + defaults: { + /** + * @property holdTimeout + * @type {Number} + * @default 500 + */ + holdTimeout: 500, - // 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) - } + /** + * movement allowed while holding + * @property holdThreshold + * @type {Number} + * @default 2 + */ + holdThreshold: 2 + }, + handler: holdGesture + }; + })('hold'); - return this._isResized() || parentChanged; + /** + * @module gestures + */ + /** + * when a touch is being released from the page + * + * @class Release + * @static + */ + /** + * @event release + * @param {Object} ev + */ + Hammer.gestures.Release = { + name: 'release', + index: Infinity, + handler: function releaseGesture(ev, inst) { + if(ev.eventType == EVENT_RELEASE) { + inst.trigger(this.name, ev); + } + } }; /** - * Repaint major and minor text labels and vertical grid lines - * @private + * @module gestures + */ + /** + * triggers swipe events when the end velocity is above the threshold + * for best usage, set `preventDefault` (on the drag gesture) to `true` + * ```` + * hammertime.on("dragleft swipeleft", function(ev) { + * console.log(ev); + * ev.gesture.preventDefault(); + * }); + * ```` + * + * @class Swipe + * @static + */ + /** + * @event swipe + * @param {Object} ev + */ + /** + * @event swipeleft + * @param {Object} ev + */ + /** + * @event swiperight + * @param {Object} ev + */ + /** + * @event swipeup + * @param {Object} ev + */ + /** + * @event swipedown + * @param {Object} ev */ - TimeAxis.prototype._repaintLabels = function () { - var orientation = this.options.orientation; + Hammer.gestures.Swipe = { + name: 'swipe', + index: 40, + defaults: { + /** + * @property swipeMinTouches + * @type {Number} + * @default 1 + */ + swipeMinTouches: 1, - // calculate range and step (step such that we have space for 7 characters per label) - var start = util.convert(this.body.range.start, 'Number'), - end = util.convert(this.body.range.end, 'Number'), - minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf() - -this.body.util.toTime(0).valueOf(); - var step = new TimeStep(new Date(start), new Date(end), minimumStep); - this.step = step; + /** + * @property swipeMaxTouches + * @type {Number} + * @default 1 + */ + swipeMaxTouches: 1, - // Move all DOM elements to a "redundant" list, where they - // can be picked for re-use, and clear the lists with lines and texts. - // At the end of the function _repaintLabels, left over elements will be cleaned up - var dom = this.dom; - dom.redundant.majorLines = dom.majorLines; - dom.redundant.majorTexts = dom.majorTexts; - dom.redundant.minorLines = dom.minorLines; - dom.redundant.minorTexts = dom.minorTexts; - dom.majorLines = []; - dom.majorTexts = []; - dom.minorLines = []; - dom.minorTexts = []; + /** + * horizontal swipe velocity + * @property swipeVelocityX + * @type {Number} + * @default 0.6 + */ + swipeVelocityX: 0.6, - step.first(); - var xFirstMajorLabel = undefined; - var max = 0; - while (step.hasNext() && max < 1000) { - max++; - var cur = step.getCurrent(), - x = this.body.util.toScreen(cur), - isMajor = step.isMajor(); + /** + * vertical swipe velocity + * @property swipeVelocityY + * @type {Number} + * @default 0.6 + */ + swipeVelocityY: 0.6 + }, - // TODO: lines must have a width, such that we can create css backgrounds + handler: function swipeGesture(ev, inst) { + if(ev.eventType == EVENT_RELEASE) { + var touches = ev.touches.length, + options = inst.options; - if (this.options.showMinorLabels) { - this._repaintMinorText(x, step.getLabelMinor(), orientation); - } + // max touches + if(touches < options.swipeMinTouches || + touches > options.swipeMaxTouches) { + return; + } - if (isMajor && this.options.showMajorLabels) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(ev.velocityX > options.swipeVelocityX || + ev.velocityY > options.swipeVelocityY) { + // trigger swipe events + inst.trigger(this.name, ev); + inst.trigger(this.name + ev.direction, ev); + } } - this._repaintMajorText(x, step.getLabelMajor(), orientation); - } - this._repaintMajorLine(x, orientation); - } - else { - this._repaintMinorLine(x, orientation); - } - - step.next(); - } - - // create a major label on the left when needed - if (this.options.showMajorLabels) { - var leftTime = this.body.util.toTime(0), - leftText = step.getLabelMajor(leftTime), - widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation - - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText, orientation); - } - } - - // Cleanup leftover DOM elements from the redundant list - util.forEach(this.dom.redundant, function (arr) { - while (arr.length) { - var elem = arr.pop(); - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } } - }); }; /** - * Create a minor label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @private + * @module gestures */ - TimeAxis.prototype._repaintMinorText = function (x, text, orientation) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); - - if (!label) { - // create new label - var content = document.createTextNode(''); - label = document.createElement('div'); - label.appendChild(content); - label.className = 'text minor'; - this.dom.foreground.appendChild(label); - } - 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.title = title; // TODO: this is a heavy operation - }; - /** - * Create a Major label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @private + * Single tap and a double tap on a place + * + * @class Tap + * @static */ - TimeAxis.prototype._repaintMajorText = function (x, text, orientation) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); - - if (!label) { - // create label - var content = document.createTextNode(text); - label = document.createElement('div'); - label.className = 'text major'; - label.appendChild(content); - this.dom.foreground.appendChild(label); - } - this.dom.majorTexts.push(label); - - label.childNodes[0].nodeValue = text; - //label.title = title; // TODO: this is a heavy operation - - label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px'); - label.style.left = x + 'px'; - }; - /** - * Create a minor line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) - * @private + * @event tap + * @param {Object} ev */ - TimeAxis.prototype._repaintMinorLine = function (x, orientation) { - // reuse redundant line - var line = this.dom.redundant.minorLines.shift(); - - if (!line) { - // create vertical line - line = document.createElement('div'); - line.className = 'grid vertical minor'; - this.dom.background.appendChild(line); - } - this.dom.minorLines.push(line); - - var props = this.props; - if (orientation == 'top') { - line.style.top = props.majorLabelHeight + 'px'; - } - else { - line.style.top = this.body.domProps.top.height + 'px'; - } - line.style.height = props.minorLineHeight + 'px'; - line.style.left = (x - props.minorLineWidth / 2) + 'px'; - }; - /** - * Create a Major line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) - * @private + * @event doubletap + * @param {Object} ev */ - TimeAxis.prototype._repaintMajorLine = function (x, orientation) { - // reuse redundant line - var line = this.dom.redundant.majorLines.shift(); - - if (!line) { - // create vertical line - line = document.createElement('DIV'); - line.className = 'grid vertical major'; - this.dom.background.appendChild(line); - } - this.dom.majorLines.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'; - }; /** - * 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 + * @param {String} name */ - 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. + (function(name) { + var hasMoved = false; + + function tapGesture(ev, inst) { + var options = inst.options, + current = Detection.current, + prev = Detection.previous, + sincePrev, + didDoubleTap; + + switch(ev.eventType) { + case EVENT_START: + hasMoved = false; + break; - // determine the char width and height on the minor axis - if (!this.dom.measureCharMinor) { - this.dom.measureCharMinor = document.createElement('DIV'); - this.dom.measureCharMinor.className = 'text minor measure'; - this.dom.measureCharMinor.style.position = 'absolute'; + case EVENT_MOVE: + hasMoved = hasMoved || (ev.distance > options.tapMaxDistance); + break; - 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; + case EVENT_END: + if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) { + // previous gesture, for the double tap since these are two different gesture detections + sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp; + didDoubleTap = false; - // determine the char width and height on the major axis - if (!this.dom.measureCharMajor) { - this.dom.measureCharMajor = document.createElement('DIV'); - this.dom.measureCharMajor.className = 'text minor measure'; - this.dom.measureCharMajor.style.position = 'absolute'; + // check if double tap + if(prev && prev.name == name && + (sincePrev && sincePrev < options.doubleTapInterval) && + ev.distance < options.doubleTapDistance) { + inst.trigger('doubletap', ev); + didDoubleTap = true; + } - 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; - }; + // do a single tap + if(!didDoubleTap || options.tapAlways) { + current.name = name; + inst.trigger(current.name, ev); + } + } + break; + } + } - /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate - */ - TimeAxis.prototype.snap = function(date) { - return this.step.snap(date); - }; + Hammer.gestures.Tap = { + name: name, + index: 100, + handler: tapGesture, + defaults: { + /** + * max time of a tap, this is for the slow tappers + * @property tapMaxTime + * @type {Number} + * @default 250 + */ + tapMaxTime: 250, - module.exports = TimeAxis; + /** + * max distance of movement of a tap, this is for the slow tappers + * @property tapMaxDistance + * @type {Number} + * @default 10 + */ + tapMaxDistance: 10, + /** + * always trigger the `tap` event, even while double-tapping + * @property tapAlways + * @type {Boolean} + * @default true + */ + tapAlways: true, -/***/ }, -/* 28 */ -/***/ function(module, exports, __webpack_require__) { + /** + * max distance between two taps + * @property doubleTapDistance + * @type {Number} + * @default 20 + */ + doubleTapDistance: 20, - var Hammer = __webpack_require__(41); + /** + * max time between two taps + * @property doubleTapInterval + * @type {Number} + * @default 300 + */ + doubleTapInterval: 300 + } + }; + })('tap'); /** - * @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 + * @module gestures */ - function Item (data, conversion, options) { - this.id = null; - this.parent = null; - this.data = data; - this.dom = null; - this.conversion = conversion || {}; - this.options = options || {}; + /** + * when a touch is being touched at the page + * + * @class Touch + * @static + */ + /** + * @event touch + * @param {Object} ev + */ + Hammer.gestures.Touch = { + name: 'touch', + index: -Infinity, + defaults: { + /** + * call preventDefault at touchstart, and makes the element blocking by disabling the scrolling of the page, + * but it improves gestures like transforming and dragging. + * be careful with using this, it can be very annoying for users to be stuck on the page + * @property preventDefault + * @type {Boolean} + * @default false + */ + preventDefault: false, - this.selected = false; - this.displayed = false; - this.dirty = true; + /** + * disable mouse events, so only touch (or pen!) input triggers events + * @property preventMouse + * @type {Boolean} + * @default false + */ + preventMouse: false + }, + handler: function touchGesture(ev, inst) { + if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) { + ev.stopDetect(); + return; + } - this.top = null; - this.left = null; - this.width = null; - this.height = null; - } + if(inst.options.preventDefault) { + ev.preventDefault(); + } - /** - * Select current item - */ - Item.prototype.select = function() { - this.selected = true; - if (this.displayed) this.redraw(); + if(ev.eventType == EVENT_TOUCH) { + inst.trigger('touch', ev); + } + } }; /** - * Unselect current item + * @module gestures */ - Item.prototype.unselect = function() { - this.selected = false; - if (this.displayed) this.redraw(); - }; - /** - * Set a parent for the item - * @param {ItemSet | Group} parent + * User want to scale or rotate with 2 fingers + * Preventing the default browser behavior is a good way to improve feel and working. This can be done with the + * `preventDefault` option. + * + * @class Transform + * @static */ - Item.prototype.setParent = function(parent) { - if (this.displayed) { - this.hide(); - this.parent = parent; - if (this.parent) { - this.show(); - } - } - else { - this.parent = parent; - } - }; - /** - * 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 + * @event transform + * @param {Object} ev */ - Item.prototype.isVisible = function(range) { - // Should be implemented by Item implementations - return false; - }; - /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed + * @event transformstart + * @param {Object} ev */ - Item.prototype.show = function() { - return false; - }; - /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed + * @event transformend + * @param {Object} ev */ - Item.prototype.hide = function() { - return false; - }; - /** - * Repaint the item + * @event pinchin + * @param {Object} ev */ - Item.prototype.redraw = function() { - // should be implemented by the item - }; - /** - * Reposition the Item horizontally + * @event pinchout + * @param {Object} ev */ - Item.prototype.repositionX = function() { - // should be implemented by the item - }; - /** - * Reposition the Item vertically + * @event rotate + * @param {Object} ev */ - Item.prototype.repositionY = function() { - // should be implemented by the item - }; /** - * Repaint a delete button on the top right of the item when the item is selected - * @param {HTMLElement} anchor - * @protected + * @param {String} name */ - Item.prototype._repaintDeleteButton = function (anchor) { - if (this.selected && this.options.editable.remove && !this.dom.deleteButton) { - // create and show button - var me = this; + (function(name) { + var triggered = false; - var deleteButton = document.createElement('div'); - deleteButton.className = 'delete'; - deleteButton.title = 'Delete this item'; + function transformGesture(ev, inst) { + switch(ev.eventType) { + case EVENT_START: + triggered = false; + break; - Hammer(deleteButton, { - preventDefault: true - }).on('tap', function (event) { - me.parent.removeFromDataSet(me); - event.stopPropagation(); - }); + case EVENT_MOVE: + // at least multitouch + if(ev.touches.length < 2) { + return; + } - 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 scaleThreshold = Math.abs(1 - ev.scale); + var rotationThreshold = Math.abs(ev.rotation); - module.exports = Item; + // when the distance we moved is too small we skip this gesture + // or we can be already in dragging + if(scaleThreshold < inst.options.transformMinScale && + rotationThreshold < inst.options.transformMinRotation) { + return; + } + // we are transforming! + Detection.current.name = name; -/***/ }, -/* 29 */ -/***/ function(module, exports, __webpack_require__) { + // first time, trigger dragstart event + if(!triggered) { + inst.trigger(name + 'start', ev); + triggered = true; + } - var Item = __webpack_require__(28); + inst.trigger(name, ev); // basic transform event - /** - * @constructor ItemBox - * @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 ItemBox (data, conversion, options) { - this.props = { - dot: { - width: 0, - height: 0 - }, - line: { - width: 0, - height: 0 - } - }; + // trigger rotate event + if(rotationThreshold > inst.options.transformMinRotation) { + inst.trigger('rotate', ev); + } - // validate data - if (data) { - if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + // trigger pinch event + if(scaleThreshold > inst.options.transformMinScale) { + inst.trigger('pinch', ev); + inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev); + } + break; + + case EVENT_RELEASE: + if(triggered && ev.changedLength < 2) { + inst.trigger(name + 'end', ev); + triggered = false; + } + break; + } } - } - Item.call(this, data, conversion, options); - } + Hammer.gestures.Transform = { + name: name, + index: 45, + defaults: { + /** + * minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 + * @property transformMinScale + * @type {Number} + * @default 0.01 + */ + transformMinScale: 0.01, - ItemBox.prototype = new Item (null, null, null); + /** + * rotation in degrees + * @property transformMinRotation + * @type {Number} + * @default 1 + */ + transformMinRotation: 1 + }, - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - ItemBox.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); - }; + handler: transformGesture + }; + })('transform'); /** - * Repaint the item + * @module hammer */ - ItemBox.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; - // create main box - dom.box = document.createElement('DIV'); + // AMD export + if(true) { + !(__WEBPACK_AMD_DEFINE_RESULT__ = (function() { + return Hammer; + }.call(exports, __webpack_require__, exports, module)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + // commonjs export + } else if(typeof module !== 'undefined' && module.exports) { + module.exports = Hammer; + // browser export + } else { + window.Hammer = Hammer; + } - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); + })(window); - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; +/***/ }, +/* 20 */ +/***/ function(module, exports, __webpack_require__) { - // dot on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; + var util = __webpack_require__(1); + var hammerUtil = __webpack_require__(21); + var moment = __webpack_require__(2); + var Component = __webpack_require__(22); - // attach this item as attribute - dom.box['timeline-item'] = this; - } + /** + * @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 + */ + function Range(body, options) { + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.start = now.clone().add('days', -3).valueOf(); // Number + this.end = now.clone().add('days', 4).valueOf(); // Number - // 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 time axis: 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 time axis: 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 time axis: parent has no axis container element'); - axis.appendChild(dom.dot); - } - this.displayed = true; + this.body = body; - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } + // 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); - this.dirty = true; - } + this.props = { + touch: {} + }; - // update title - if (this.data.title != this.title) { - dom.box.title = this.data.title; - this.title = this.data.title; - } + // drag listeners for dragging + this.body.emitter.on('dragstart', this._onDragStart.bind(this)); + this.body.emitter.on('drag', this._onDrag.bind(this)); + this.body.emitter.on('dragend', this._onDragEnd.bind(this)); - // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.box.className = 'item box' + className; - dom.line.className = 'item line' + className; - dom.dot.className = 'item dot' + className; + // ignore dragging when holding + this.body.emitter.on('hold', this._onHold.bind(this)); + + // mouse wheel for zooming + this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); + this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF - this.dirty = true; - } + // pinch to zoom + this.body.emitter.on('touch', this._onTouch.bind(this)); + this.body.emitter.on('pinch', this._onPinch.bind(this)); - // recalculate size - if (this.dirty) { - this.props.dot.height = dom.dot.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.line.width = dom.line.offsetWidth; - this.width = dom.box.offsetWidth; - this.height = dom.box.offsetHeight; + this.setOptions(options); + } - this.dirty = false; - } + Range.prototype = new Component(); - this._repaintDeleteButton(dom.box); + /** + * 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']; + util.selectiveExtend(fields, this.options, options); + + if ('start' in options || 'end' in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); + } + } }; /** - * Show the item in the DOM (when not already displayed). The items DOM will - * be created when needed. + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - ItemBox.prototype.show = function() { - if (!this.displayed) { - this.redraw(); + function validateDirection (direction) { + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + + 'Choose "horizontal" or "vertical".'); + } + } + + /** + * Set a new start and end range + * @param {Number} [start] + * @param {Number} [end] + */ + Range.prototype.setRange = function(start, end) { + var changed = this._applyRange(start, end); + if (changed) { + var params = { + start: new Date(this.start), + end: new Date(this.end) + }; + this.body.emitter.emit('rangechange', params); + this.body.emitter.emit('rangechanged', params); } }; /** - * Hide the item from the DOM (when visible) + * 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 */ - ItemBox.prototype.hide = function() { - if (this.displayed) { - var dom = this.dom; + 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; - 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); + // 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 + '"'); + } - this.top = null; - this.left = null; + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } - this.displayed = false; + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = (min - newStart); + newStart += diff; + newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } + } } - }; - /** - * Reposition the item horizontally - * @Override - */ - ItemBox.prototype.repositionX = function() { - var start = this.conversion.toScreen(this.data.start), - align = this.options.align, - left, - box = this.dom.box, - line = this.dom.line, - dot = this.dom.dot; + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = (newEnd - max); + newStart -= diff; + newEnd -= diff; - // calculate left position of the box - if (align == 'right') { - this.left = start - this.width; + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } + } } - else if (align == 'left') { - this.left = start; + + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; + } + if ((newEnd - newStart) < zoomMin) { + if ((this.end - this.start) === zoomMin) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the minimum + diff = (zoomMin - (newEnd - newStart)); + newStart -= diff / 2; + newEnd += diff / 2; + } + } } - else { - // default or 'center' - this.left = start - this.width / 2; + + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } + if ((newEnd - newStart) > zoomMax) { + if ((this.end - this.start) === zoomMax) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the maximum + diff = ((newEnd - newStart) - zoomMax); + newStart += diff / 2; + newEnd -= diff / 2; + } + } } - // reposition box - box.style.left = this.left + 'px'; + var changed = (this.start != newStart || this.end != newEnd); - // reposition line - line.style.left = (start - this.props.line.width / 2) + 'px'; + this.start = newStart; + this.end = newEnd; - // reposition dot - dot.style.left = (start - this.props.dot.width / 2) + 'px'; + return changed; }; /** - * Reposition the item vertically - * @Override + * Retrieve the current range. + * @return {Object} An object with start and end properties */ - ItemBox.prototype.repositionY = function() { - var orientation = this.options.orientation, - box = this.dom.box, - line = this.dom.line, - dot = this.dom.dot; + Range.prototype.getRange = function() { + return { + start: this.start, + end: this.end + }; + }; - if (orientation == 'top') { - box.style.top = (this.top || 0) + 'px'; + /** + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.prototype.conversion = function (width) { + return Range.conversion(this.start, this.end, width); + }; - line.style.top = '0'; - line.style.height = (this.parent.top + this.top + 1) + 'px'; - line.style.bottom = ''; + /** + * Static method to calculate the conversion offset and scale for a range, + * based on the provided start, end, and width + * @param {Number} start + * @param {Number} end + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.conversion = function (start, end, width) { + if (width != 0 && (end - start != 0)) { + return { + offset: start, + scale: width / (end - start) + } } - else { // 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'; + else { + return { + offset: 0, + scale: 1 + }; } - - dot.style.top = (-this.props.dot.height / 2) + 'px'; }; - module.exports = ItemBox; - - -/***/ }, -/* 30 */ -/***/ function(module, exports, __webpack_require__) { - - var Item = __webpack_require__(28); - /** - * @constructor ItemPoint - * @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 + * Start dragging horizontally or vertically + * @param {Event} event + * @private */ - function ItemPoint (data, conversion, options) { - this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, - content: { - height: 0, - marginLeft: 0 - } - }; + Range.prototype._onDragStart = function(event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + 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; - Item.call(this, data, conversion, options); - } + this.props.touch.start = this.start; + this.props.touch.end = this.end; - ItemPoint.prototype = new Item (null, null, null); + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'move'; + } + }; /** - * 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 + * Perform dragging operation + * @param {Event} event + * @private */ - ItemPoint.prototype.isVisible = function(range) { - // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); + Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; + var direction = this.options.direction; + validateDirection(direction); + // 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 delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, + interval = (this.props.touch.end - this.props.touch.start), + width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height, + diffRange = -delta / width * interval; + this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange); + this.body.emitter.emit('rangechange', { + start: new Date(this.start), + end: new Date(this.end) + }); }; /** - * Repaint the item + * Stop dragging operation + * @param {event} event + * @private */ - ItemPoint.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - // background box - dom.point = document.createElement('div'); - // className is updated in redraw() + // 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; - // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.point.appendChild(dom.content); + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'auto'; + } - // dot at start - dom.dot = document.createElement('div'); - dom.point.appendChild(dom.dot); + // fire a rangechanged event + this.body.emitter.emit('rangechanged', { + start: new Date(this.start), + end: new Date(this.end) + }); + }; - // attach this item as attribute - dom.point['timeline-item'] = this; - } + /** + * 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; - // 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 time axis: parent has no foreground container element'); - } - foreground.appendChild(dom.point); + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; } - this.displayed = true; - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // perform the zoom action. Delta is normally 1 or -1 + + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - (delta / 5); } else { - throw new Error('Property "content" missing in item ' + this.data.id); + scale = 1 / (1 + (delta / 5)) ; } - this.dirty = true; - } - - // update title - if (this.data.title != this.title) { - dom.point.title = this.data.title; - this.title = this.data.title; - } - - // update class - var className = (this.data.className? ' ' + this.data.className : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.point.className = 'item point' + className; - dom.dot.className = 'item dot' + className; + // calculate center, the date to zoom around + var gesture = hammerUtil.fakeGesture(this, event), + pointer = getPointer(gesture.center, this.body.dom.center), + pointerDate = this._pointerToDate(pointer); - this.dirty = true; + this.zoom(scale, pointerDate); } - // recalculate size - if (this.dirty) { - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; - - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - - dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; - dom.dot.style.left = (this.props.dot.width / 2) + 'px'; - - this.dirty = false; - } + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); + }; - this._repaintDeleteButton(dom.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; }; /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * On start of a hold gesture + * @private */ - ItemPoint.prototype.show = function() { - if (!this.displayed) { - this.redraw(); - } + Range.prototype._onHold = function () { + this.props.touch.allowDragging = false; }; /** - * Hide the item from the DOM (when visible) + * Handle pinch event + * @param {Event} event + * @private */ - ItemPoint.prototype.hide = function() { - if (this.displayed) { - if (this.dom.point.parentNode) { - this.dom.point.parentNode.removeChild(this.dom.point); + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; + + this.props.touch.allowDragging = false; + + if (event.gesture.touches.length > 1) { + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center); } - this.top = null; - this.left = null; + var scale = 1 / event.gesture.scale, + initDate = this._pointerToDate(this.props.touch.center); + + // calculate new start and end + var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale); + var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale); - this.displayed = false; + // apply new range + this.setRange(newStart, newEnd); } }; /** - * Reposition the item horizontally - * @Override + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date + * @private */ - ItemPoint.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'; - }; + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - /** - * Reposition the item vertically - * @Override - */ - ItemPoint.prototype.repositionY = function() { - var orientation = this.options.orientation, - point = this.dom.point; + validateDirection(direction); - if (orientation == 'top') { - point.style.top = this.top + 'px'; + if (direction == 'horizontal') { + var width = this.body.domProps.center.width; + conversion = this.conversion(width); + return pointer.x / conversion.scale + conversion.offset; } else { - point.style.top = (this.parent.height - this.top - this.height) + 'px'; + var height = this.body.domProps.center.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; } }; - module.exports = ItemPoint; - - -/***/ }, -/* 31 */ -/***/ function(module, exports, __webpack_require__) { - - var Hammer = __webpack_require__(41); - var Item = __webpack_require__(28); - /** - * @constructor ItemRange - * @extends Item - * @param {Object} data Object containing parameters start, end - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe options + * Get the pointer location relative to the location of the dom element + * @param {{pageX: Number, pageY: Number}} touch + * @param {Element} element HTML DOM element + * @return {{x: Number, y: Number}} pointer + * @private */ - function ItemRange (data, conversion, options) { - this.props = { - content: { - width: 0 - } + function getPointer (touch, element) { + return { + x: touch.pageX - util.getAbsoluteLeft(element), + y: touch.pageY - util.getAbsoluteTop(element) }; - 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); - } + /** + * Zoom the range the given scale in or out. Start and end date will + * be adjusted, and the timeline will be redrawn. You can optionally give a + * date around which to zoom. + * For example, try scale = 0.9 or 1.1 + * @param {Number} scale Scaling factor. Values above 1 will zoom out, + * values below 1 will zoom in. + * @param {Number} [center] Value representing a date around which will + * be zoomed. + */ + Range.prototype.zoom = function(scale, center) { + // if centerDate is not provided, take it half between start Date and end Date + if (center == null) { + center = (this.start + this.end) / 2; } - Item.call(this, data, conversion, options); - } - - ItemRange.prototype = new Item (null, null, null); + // calculate new start and end + var newStart = center + (this.start - center) * scale; + var newEnd = center + (this.end - center) * scale; - ItemRange.prototype.baseClassName = 'item range'; + this.setRange(newStart, newEnd); + }; /** - * 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 + * 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 */ - ItemRange.prototype.isVisible = function(range) { - // determine visibility - return (this.data.start < range.end) && (this.data.end > range.start); + Range.prototype.move = function(delta) { + // zoom start Date and end Date relative to the centerDate + var diff = (this.end - this.start); + + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; + + // TODO: reckon with min and max range + + this.start = newStart; + this.end = newEnd; }; /** - * Repaint the item + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ - ItemRange.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() + Range.prototype.moveTo = function(moveTo) { + var center = (this.start + this.end) / 2; - // contents box - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); + var diff = center - moveTo; - // attach this item as attribute - dom.box['timeline-item'] = this; - } + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; - // 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 time axis: parent has no foreground container element'); - } - foreground.appendChild(dom.box); - } - this.displayed = true; + this.setRange(newStart, newEnd); + }; - // update contents - if (this.data.content != this.content) { - this.content = this.data.content; - if (this.content instanceof Element) { - dom.content.innerHTML = ''; - dom.content.appendChild(this.content); - } - else if (this.data.content != undefined) { - dom.content.innerHTML = this.content; - } - else { - throw new Error('Property "content" missing in item ' + this.data.id); - } + module.exports = Range; - this.dirty = true; - } - // update title - if (this.data.title != this.title) { - dom.box.title = this.data.title; - this.title = this.data.title; - } +/***/ }, +/* 21 */ +/***/ function(module, exports, __webpack_require__) { - // update class - var className = (this.data.className ? (' ' + this.data.className) : '') + - (this.selected ? ' selected' : ''); - if (this.className != className) { - this.className = className; - dom.box.className = this.baseClassName + className; + var Hammer = __webpack_require__(18); - this.dirty = true; - } + /** + * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent + * @param {Element} element + * @param {Event} event + */ + exports.fakeGesture = function(element, event) { + var eventType = null; - // recalculate size - if (this.dirty) { - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; + // for hammer.js 1.0.5 + // var gesture = Hammer.event.collectEventData(this, eventType, event); - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; + // for hammer.js 1.0.6+ + var touches = Hammer.event.getTouchList(event, eventType); + var gesture = Hammer.event.collectEventData(this, eventType, touches, event); - this.dirty = false; + // on IE in standards mode, no touches are recognized by hammer.js, + // resulting in NaN values for center.pageX and center.pageY + if (isNaN(gesture.center.pageX)) { + gesture.center.pageX = event.pageX; + } + if (isNaN(gesture.center.pageY)) { + gesture.center.pageY = event.pageY; } - this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); + return gesture; }; + +/***/ }, +/* 22 */ +/***/ function(module, exports, __webpack_require__) { + /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * Prototype for visual components + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] + * @param {Object} [options] */ - ItemRange.prototype.show = function() { - if (!this.displayed) { - this.redraw(); + function Component (body, options) { + this.options = null; + this.props = null; + } + + /** + * 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); } }; /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - ItemRange.prototype.hide = function() { - if (this.displayed) { - var box = this.dom.box; - - if (box.parentNode) { - box.parentNode.removeChild(box); - } - - this.top = null; - this.left = null; + Component.prototype.redraw = function() { + // should be implemented by the component + return false; + }; - this.displayed = false; - } + /** + * Destroy the component. Cleanup DOM and event listeners + */ + Component.prototype.destroy = function() { + // should be implemented by the component }; /** - * Reposition the item horizontally - * @Override + * Test whether the component is resized since the last time _isResized() was + * called. + * @return {Boolean} Returns true if the component is resized + * @protected */ - ItemRange.prototype.repositionX = function() { - var props = this.props, - parentWidth = this.parent.width, - start = this.conversion.toScreen(this.data.start), - end = this.conversion.toScreen(this.data.end), - padding = this.options.padding, - contentLeft; + Component.prototype._isResized = function() { + var resized = (this.props._previousWidth !== this.props.width || + this.props._previousHeight !== this.props.height); - // limit the width of the this, as browsers cannot draw very wide divs - if (start < -parentWidth) { - start = -parentWidth; - } - if (end > 2 * parentWidth) { - end = 2 * parentWidth; - } - var boxWidth = Math.max(end - start, 1); + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - if (this.overflow) { - // when range exceeds left of the window, position the contents at the left of the visible area - contentLeft = Math.max(-start, 0); + return resized; + }; - this.left = start; - this.width = boxWidth + this.props.content.width; - // Note: The calculation of width is an optimistic calculation, giving - // a width which will not change when moving the Timeline - // So no restacking needed, which is nicer for the eye; - } - else { // no overflow - // when range exceeds left of the window, position the contents at the left of the visible area - if (start < 0) { - contentLeft = Math.min(-start, - (end - start - props.content.width - 2 * padding)); - // TODO: remove the need for options.padding. it's terrible. - } - else { - contentLeft = 0; - } + module.exports = Component; - this.left = start; - this.width = boxWidth; - } - this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = boxWidth + 'px'; - this.dom.content.style.left = contentLeft + 'px'; - }; +/***/ }, +/* 23 */ +/***/ function(module, exports, __webpack_require__) { + + var Emitter = __webpack_require__(10); + var Hammer = __webpack_require__(18); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(8); + var Range = __webpack_require__(20); + var TimeAxis = __webpack_require__(24); + var CurrentTime = __webpack_require__(26); + var CustomTime = __webpack_require__(28); + var ItemSet = __webpack_require__(29); /** - * Reposition the item vertically - * @Override + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Core.setOptions for the available options. + * @constructor */ - ItemRange.prototype.repositionY = function() { - var orientation = this.options.orientation, - box = this.dom.box; + function Core () {} - if (orientation == 'top') { - box.style.top = this.top + 'px'; - } - else { - box.style.top = (this.parent.height - this.top - this.height) + 'px'; - } - }; + // turn Core into an event emitter + Emitter(Core.prototype); /** - * Repaint a drag area on the left side of the range when the range is selected - * @protected + * 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. + * @private */ - ItemRange.prototype._repaintDragLeft = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { - // create and show drag area - var dragLeft = document.createElement('div'); - dragLeft.className = 'drag-left'; - dragLeft.dragLeftItem = this; + Core.prototype._create = function (container) { + this.dom = {}; - // TODO: this should be redundant? - Hammer(dragLeft, { - preventDefault: true - }).on('drag', function () { - //console.log('drag left') - }); + 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'); - 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.dom.background.className = 'vispanel background'; + this.dom.backgroundVertical.className = 'vispanel background vertical'; + this.dom.backgroundHorizontal.className = 'vispanel background horizontal'; + this.dom.centerContainer.className = 'vispanel center'; + this.dom.leftContainer.className = 'vispanel left'; + this.dom.rightContainer.className = 'vispanel right'; + this.dom.top.className = 'vispanel top'; + this.dom.bottom.className = 'vispanel bottom'; + this.dom.left.className = 'content'; + this.dom.center.className = 'content'; + this.dom.right.className = 'content'; + this.dom.shadowTop.className = 'shadow top'; + this.dom.shadowBottom.className = 'shadow bottom'; + this.dom.shadowTopLeft.className = 'shadow top'; + this.dom.shadowBottomLeft.className = 'shadow bottom'; + this.dom.shadowTopRight.className = 'shadow top'; + this.dom.shadowBottomRight.className = 'shadow bottom'; + + 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); + + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); + + 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); + + this.on('rangechange', this.redraw.bind(this)); + this.on('change', this.redraw.bind(this)); + this.on('touch', this._onTouch.bind(this)); + this.on('pinch', this._onPinch.bind(this)); + this.on('dragstart', this._onDragStart.bind(this)); + this.on('drag', this._onDrag.bind(this)); + + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = Hammer(this.dom.root, { + prevent_default: true + }); + this.listeners = {}; + + var me = this; + var events = [ + 'touch', 'pinch', + 'tap', 'doubletap', 'hold', + 'dragstart', 'drag', 'dragend', + 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (event) { + var listener = function () { + var args = [event].concat(Array.prototype.slice.call(arguments, 0)); + me.emit.apply(me, args); + }; + me.hammer.on(event, listener); + me.listeners[event] = listener; + }); + + // 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.touch = {}; // store state information needed for touch events + + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); }; /** - * Repaint a drag area on the right side of the range when the range is selected - * @protected - */ - ItemRange.prototype._repaintDragRight = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { - // create and show drag area - var dragRight = document.createElement('div'); - dragRight.className = 'drag-right'; - dragRight.dragRightItem = this; + * Destroy the Core, clean up all DOM elements and event listeners. + */ + Core.prototype.destroy = function () { + // unbind datasets + this.clear(); - // TODO: this should be redundant? - Hammer(dragRight, { - preventDefault: true - }).on('drag', function () { - //console.log('drag right') - }); + // remove all event listeners + this.off(); - this.dom.box.appendChild(dragRight); - this.dom.dragRight = dragRight; + // stop checking for changed size + this._stopAutoResize(); + + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); } - 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 = null; + + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; } - this.dom.dragRight = null; } - }; + this.listeners = null; + this.hammer = null; - module.exports = ItemRange; + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + component.destroy(); + }); + this.body = null; + }; -/***/ }, -/* 32 */ -/***/ function(module, exports, __webpack_require__) { - var Emitter = __webpack_require__(48); - var Hammer = __webpack_require__(41); - var mousetrap = __webpack_require__(49); - var util = __webpack_require__(1); - var hammerUtil = __webpack_require__(44); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var dotparser = __webpack_require__(38); - var gephiParser = __webpack_require__(39); - var Groups = __webpack_require__(34); - var Images = __webpack_require__(35); - var Node = __webpack_require__(36); - var Edge = __webpack_require__(33); - var Popup = __webpack_require__(37); - var MixinLoader = __webpack_require__(47); - var locales = __webpack_require__(45); + /** + * Set a custom time bar + * @param {Date} time + */ + Core.prototype.setCustomTime = function (time) { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); + } - // Load custom shapes into CanvasRenderingContext2D - __webpack_require__(46); + this.customTime.setCustomTime(time); + }; /** - * @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 + * Retrieve the current custom time. + * @return {Date} customTime */ - function Network (container, data, options) { - if (!(this instanceof Network)) { - throw new SyntaxError('Constructor must be called with the new operator'); + Core.prototype.getCustomTime = function() { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); } - this._initializeMixinLoaders(); - - // create variables and set default values - this.containerElement = container; + return this.customTime.getCustomTime(); + }; - // render and calculation settings - this.renderRefreshRate = 60; // hz (fps) - this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on - this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame - this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step. - this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation - this.initializing = true; + /** + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items + */ + Core.prototype.getVisibleItems = function() { + return this.itemSet && this.itemSet.getVisibleItems() || []; + }; - this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null}; - // set constant values - this.defaultOptions = { - nodes: { - mass: 1, - radiusMin: 10, - radiusMax: 30, - radius: 10, - shape: 'ellipse', - image: undefined, - widthMin: 16, // px - widthMax: 64, // px - fixed: false, - fontColor: 'black', - fontSize: 14, // px - fontFace: 'verdana', - level: -1, - color: { - border: '#2B7CE9', - background: '#97C2FC', - highlight: { - border: '#2B7CE9', - background: '#D2E5FF' - }, - hover: { - border: '#2B7CE9', - background: '#D2E5FF' - } - }, - borderColor: '#2B7CE9', - backgroundColor: '#97C2FC', - highlightColor: '#D2E5FF', - group: undefined, - borderWidth: 1 - }, - edges: { - widthMin: 1, - widthMax: 15, - width: 1, - widthSelectionMultiplier: 2, - hoverWidth: 1.5, - style: 'line', - color: { - color:'#848484', - highlight:'#848484', - hover: '#848484' - }, - fontColor: '#343434', - fontSize: 14, // px - fontFace: 'arial', - fontFill: 'white', - arrowScaleFactor: 1, - dash: { - length: 10, - gap: 5, - altLength: undefined - }, - inheritColor: "from" // to, from, false, true (== from) - }, - configurePhysics:false, - physics: { - barnesHut: { - enabled: true, - theta: 1 / 0.6, // inverted to save time during calculation - gravitationalConstant: -2000, - centralGravity: 0.3, - springLength: 95, - springConstant: 0.04, - damping: 0.09 - }, - repulsion: { - centralGravity: 0.0, - springLength: 200, - springConstant: 0.05, - nodeDistance: 100, - damping: 0.09 - }, - hierarchicalRepulsion: { - enabled: false, - centralGravity: 0.0, - springLength: 100, - springConstant: 0.01, - nodeDistance: 150, - damping: 0.09 - }, - damping: null, - centralGravity: null, - springLength: null, - springConstant: null - }, - clustering: { // Per Node in Cluster = PNiC - enabled: false, // (Boolean) | global on/off switch for clustering. - initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold. - clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes - reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this - chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains). - clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered. - sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. - screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. - fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). - maxFontSize: 1000, - forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). - distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). - edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. - nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster. - height: 1, // (px PNiC) | growth of the height per node in cluster. - radius: 1}, // (px PNiC) | growth of the radius per node in cluster. - maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster. - activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open. - clusterLevelDifference: 2 - }, - navigation: { - enabled: false - }, - keyboard: { - enabled: false, - speed: {x: 10, y: 10, zoom: 0.02} - }, - dataManipulation: { - enabled: false, - initiallyVisible: false - }, - hierarchicalLayout: { - enabled:false, - levelSeparation: 150, - nodeSpacing: 100, - direction: "UD" // UD, DU, LR, RL - }, - freezeForStabilization: false, - smoothCurves: { - enabled: true, - dynamic: true, - type: "continuous", - roundness: 0.5 - }, - dynamicSmoothCurves: true, - maxVelocity: 30, - minVelocity: 0.1, // px/s - stabilize: true, // stabilize before displaying the network - stabilizationIterations: 1000, // maximum number of iteration to stabilize - locale: 'en', - locales: locales, - tooltip: { - delay: 300, - fontColor: 'black', - fontSize: 14, // px - fontFace: 'verdana', - color: { - border: '#666', - background: '#FFFFC6' - } - }, - dragNetwork: true, - dragNodes: true, - zoomable: true, - hover: false, - hideEdgesOnDrag: false, - hideNodesOnDrag: false, - width : '100%', - height : '100%', - selectable: true - }; - this.constants = util.extend({}, this.defaultOptions); - this.hoverObj = {nodes:{},edges:{}}; - this.controlNodesActive = false; + /** + * Clear the Core. By Default, items, groups and options are cleared. + * Example usage: + * + * timeline.clear(); // clear items, groups, and options + * timeline.clear({options: true}); // clear options only + * + * @param {Object} [what] Optionally specify what to clear. By default: + * {items: true, groups: true, options: true} + */ + Core.prototype.clear = function(what) { + // clear items + if (!what || what.items) { + this.setItems(null); + } - // Node variables - var network = this; - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - network._redraw(); - }); + // clear groups + if (!what || what.groups) { + this.setGroups(null); + } - // keyboard navigation variables - this.xIncrement = 0; - this.yIncrement = 0; - this.zoomIncrement = 0; + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + component.setOptions(component.defaultOptions); + }); - // loading all the mixins: - // load the force calculation functions, grouped under the physics system. - this._loadPhysicsSystem(); - // create a frame and canvas - this._create(); - // load the sector system. (mandatory, fully integrated with Network) - this._loadSectorSystem(); - // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) - this._loadClusterSystem(); - // load the selection system. (mandatory, required by Network) - this._loadSelectionSystem(); - // load the selection system. (mandatory, required by Network) - this._loadHierarchySystem(); + this.setOptions(this.defaultOptions); // this will also do a redraw + } + }; - // apply options - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); - this.setOptions(options); + /** + * Set Core window such that it fits all items + */ + Core.prototype.fit = function() { + // apply the data range as range + var dataRange = this.getItemRange(); - // other vars - this.freezeSimulation = false;// freeze the simulation - this.cachedFunctions = {}; + // add 5% space on both sides + var start = dataRange.min; + var end = dataRange.max; + if (start != null && end != null) { + var interval = (end.valueOf() - start.valueOf()); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day + } + start = new Date(start.valueOf() - interval * 0.05); + end = new Date(end.valueOf() + interval * 0.05); + } - // containers for nodes and edges - this.calculationNodes = {}; - this.calculationNodeIndices = []; - this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects + // skip range set if there is no start and end date + if (start === null && end === null) { + return; + } - // position and scale variables and objects - this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. - this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action - this.scale = 1; // defining the global scale variable in the constructor - this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out + this.range.setRange(start, end); + }; - // datasets or dataviews - this.nodesData = null; // A DataSet or DataView - this.edgesData = null; // A DataSet or DataView - // create event listeners used to subscribe on the DataSets of the nodes and edges - this.nodesListeners = { - 'add': function (event, params) { - network._addNodes(params.items); - network.start(); - }, - 'update': function (event, params) { - network._updateNodes(params.items); - network.start(); - }, - 'remove': function (event, params) { - network._removeNodes(params.items); - network.start(); - } - }; - this.edgesListeners = { - 'add': function (event, params) { - network._addEdges(params.items); - network.start(); - }, - 'update': function (event, params) { - network._updateEdges(params.items); - network.start(); - }, - 'remove': function (event, params) { - network._removeEdges(params.items); - network.start(); - } + /** + * Set the visible window. Both parameters are optional, you can change only + * start or only end. Syntax: + * + * TimeLine.setWindow(start, end) + * TimeLine.setWindow(range) + * + * Where start and end can be a Date, number, or string, and range is an + * object with properties start and end. + * + * @param {Date | Number | String | Object} [start] Start date of visible window + * @param {Date | Number | String} [end] End date of visible window + */ + Core.prototype.setWindow = function(start, end) { + if (arguments.length == 1) { + var range = arguments[0]; + this.range.setRange(range.start, range.end); + } + else { + this.range.setRange(start, end); + } + }; + + /** + * Get the visible window + * @return {{start: Date, end: Date}} Visible range + */ + Core.prototype.getWindow = function() { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) }; + }; - // properties for the animation - this.moving = true; - this.timer = undefined; // Scheduling function. Is definded in this.start(); + /** + * Force a redraw of the Core. Can be useful to manually redraw when + * option autoResize=false + */ + Core.prototype.redraw = function() { + var resized = false, + options = this.options, + props = this.props, + dom = this.dom; + + if (!dom) return; // when destroyed + + // update class names + dom.root.className = 'vis timeline root ' + options.orientation; + + // 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, ''); + + // 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; + + // 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; + + // 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 + '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'; - // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled); + // 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(); - // hierarchical layout - this.initializing = false; - if (this.constants.hierarchicalLayout.enabled == true) { - this._setupHierarchicalLayout(); - } - else { - // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here. - if (this.constants.stabilize == false) { - this.zoomExtent(true,this.constants.clustering.enabled); - } + // reposition the scrollable contents + var offset = this.props.scrollTop; + if (options.orientation == 'bottom') { + 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 clustering is disabled, the simulation will have started in the setData function - if (this.constants.clustering.enabled) { - this.startWithClustering(); + // 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 + this.redraw(); } - } + }; - // Extend Network with an Emitter mixin - Emitter(Network.prototype); + // TODO: deprecated since version 1.1.0, remove some day + Core.prototype.repaint = function () { + throw new Error('Function repaint is deprecated. Use redraw instead.'); + }; /** - * Get the script path where the vis.js library is located - * - * @returns {string | null} path Path or null when not found. Path does not - * end with a slash. + * Convert a position on screen (pixels) to a datetime + * @param {int} x Position on the screen in pixels + * @return {Date} time The datetime the corresponds with given position x * @private */ - Network.prototype._getScriptPath = function() { - var scripts = document.getElementsByTagName( 'script' ); - - // find script named vis.js or vis.min.js - for (var i = 0; i < scripts.length; i++) { - var src = scripts[i].src; - var match = src && /\/?vis(.min)?\.js$/.exec(src); - if (match) { - // return path without the script name - return src.substring(0, src.length - match[0].length); - } - } - - return null; + // TODO: move this function to Range + Core.prototype._toTime = function(x) { + var conversion = this.range.conversion(this.props.center.width); + return new Date(x / conversion.scale + conversion.offset); }; /** - * Find the center position of the network + * 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 * @private */ - Network.prototype._getRange = function() { - var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - node = this.nodes[nodeId]; - if (minX > (node.x)) {minX = node.x;} - if (maxX < (node.x)) {maxX = node.x;} - if (minY > (node.y)) {minY = node.y;} - if (maxY < (node.y)) {maxY = node.y;} - } - } - if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) { - minY = 0, maxY = 0, minX = 0, maxX = 0; - } - return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + // TODO: move this function to Range + Core.prototype._toGlobalTime = function(x) { + var conversion = this.range.conversion(this.props.root.width); + return new Date(x / conversion.scale + conversion.offset); }; - /** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} + * Convert a datetime (Date object) into a position on the screen + * @param {Date} time A date + * @return {int} x The position on the screen in pixels which corresponds + * with the given date. * @private */ - Network.prototype._findCenter = function(range) { - return {x: (0.5 * (range.maxX + range.minX)), - y: (0.5 * (range.maxY + range.minY))}; + // TODO: move this function to Range + Core.prototype._toScreen = function(time) { + var conversion = this.range.conversion(this.props.center.width); + return (time.valueOf() - conversion.offset) * conversion.scale; }; /** - * center the network - * - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * 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. + * @private */ - Network.prototype._centerNetwork = function(range) { - var center = this._findCenter(range); - - center.x *= this.scale; - center.y *= this.scale; - center.x -= 0.5 * this.frame.canvas.clientWidth; - center.y -= 0.5 * this.frame.canvas.clientHeight; - - this._setTranslation(-center.x,-center.y); // set at 0,0 + // TODO: move this function to Range + Core.prototype._toGlobalScreen = function(time) { + var conversion = this.range.conversion(this.props.root.width); + return (time.valueOf() - conversion.offset) * conversion.scale; }; /** - * This function zooms out to fit all data on screen based on amount of nodes - * - * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; - * @param {Boolean} [disableStart] | If true, start is not called. + * Initialize watching when option autoResize is true + * @private */ - Network.prototype.zoomExtent = function(initialZoom, disableStart) { - if (initialZoom === undefined) { - initialZoom = false; + Core.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); } - if (disableStart === undefined) { - disableStart = false; + else { + this._stopAutoResize(); } + }; - var range = this._getRange(); - var zoomLevel; + /** + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private + */ + Core.prototype._startAutoResize = function () { + var me = this; - if (initialZoom == true) { - var numberOfNodes = this.nodeIndices.length; - if (this.constants.smoothCurves == true) { - if (this.constants.clustering.enabled == true && - numberOfNodes >= this.constants.clustering.initialMaxNodes) { - zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - else { - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } + this._stopAutoResize(); + + this._onResize = function() { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; } - else { - if (this.constants.clustering.enabled == true && - numberOfNodes >= this.constants.clustering.initialMaxNodes) { - zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } - else { - zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + + if (me.dom.root) { + // check whether the frame is resized + if ((me.dom.root.clientWidth != me.props.lastWidth) || + (me.dom.root.clientHeight != me.props.lastHeight)) { + me.props.lastWidth = me.dom.root.clientWidth; + me.props.lastHeight = me.dom.root.clientHeight; + + me.emit('change'); } } + }; - // correct for larger canvasses. - var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600); - zoomLevel *= factor; + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); + + this.watchTimer = setInterval(this._onResize, 1000); + }; + + /** + * Stop watching for a resize of the frame. + * @private + */ + Core.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; } - else { - var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1; - var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1; - var xZoomLevel = this.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.frame.canvas.clientHeight / yDistance; + // remove event listener on window.resize + util.removeEventListener(window, 'resize', this._onResize); + this._onResize = null; + }; + + /** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ + Core.prototype._onTouch = function (event) { + this.touch.allowDragging = true; + }; + + /** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ + Core.prototype._onPinch = function (event) { + this.touch.allowDragging = false; + }; + + /** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ + Core.prototype._onDragStart = function (event) { + this.touch.initialScrollTop = this.props.scrollTop; + }; - zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; - } + /** + * 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; - if (zoomLevel > 1.0) { - zoomLevel = 1.0; - } + var delta = event.gesture.deltaY; + var oldScrollTop = this._getScrollTop(); + var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - this._setScale(zoomLevel); - this._centerNetwork(range); - if (disableStart == false) { - this.moving = true; - this.start(); + if (newScrollTop != oldScrollTop) { + this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already } }; - /** - * Update the this.nodeIndices with the most recent node index list + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ - Network.prototype._updateNodeIndexList = function() { - this._clearNodeIndexList(); - for (var idx in this.nodes) { - if (this.nodes.hasOwnProperty(idx)) { - this.nodeIndices.push(idx); - } - } + Core.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; }; - /** - * 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 {Boolean} [disableStart] | optional: disable the calling of the start function. + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private */ - Network.prototype.setData = function(data, disableStart) { - if (disableStart === undefined) { - disableStart = false; - } - - if (data && data.dot && (data.nodes || data.edges)) { - throw new SyntaxError('Data must contain either parameter "dot" or ' + - ' parameter pair "nodes" and "edges", but not both.'); - } - - // set options - this.setOptions(data && data.options); - - // set all data - if (data && data.dot) { - // parse DOT file - if(data && data.dot) { - var dotData = dotparser.DOTToGraph(data.dot); - this.setData(dotData); - return; - } - } - else if (data && data.gephi) { - // parse DOT file - if(data && data.gephi) { - var gephiData = gephiParser.parseGephi(data.gephi); - this.setData(gephiData); - return; + 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 == 'bottom') { + this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin); } - } - else { - this._setNodes(data && data.nodes); - this._setEdges(data && data.edges); + this.props.scrollTopMin = scrollTopMin; } - this._putDataInSector(); - if (!disableStart) { - // find a stable position or start animating to a stable position - if (this.constants.stabilize) { - var me = this; - setTimeout(function() {me._stabilize(); me.start();},0) - } - else { - this.start(); - } - } + // 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; + + return this.props.scrollTop; }; /** - * Set options - * @param {Object} options - * @param {Boolean} [initializeView] | set zoom and translation to default. + * Get the current scrollTop + * @returns {number} scrollTop + * @private */ - Network.prototype.setOptions = function (options) { - if (options) { - var prop; + Core.prototype._getScrollTop = function () { + return this.props.scrollTop; + }; - var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation','keyboard','dataManipulation', - 'onAdd','onEdit','onEditEdge','onConnect','onDelete' - ]; - util.selectiveNotDeepExtend(fields,this.constants, options); - util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes); - util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges); + module.exports = Core; - if (options.physics) { - util.mergeOptions(this.constants.physics, options.physics,'barnesHut'); - util.mergeOptions(this.constants.physics, options.physics,'repulsion'); - if (options.physics.hierarchicalRepulsion) { - this.constants.hierarchicalLayout.enabled = true; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this.constants.physics.barnesHut.enabled = false; - for (prop in options.physics.hierarchicalRepulsion) { - if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) { - this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop]; - } - } - } - } +/***/ }, +/* 24 */ +/***/ function(module, exports, __webpack_require__) { - if (options.onAdd) {this.triggerFunctions.add = options.onAdd;} - if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;} - if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;} - if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;} - if (options.onDelete) {this.triggerFunctions.del = options.onDelete;} + var util = __webpack_require__(1); + var Component = __webpack_require__(22); + var TimeStep = __webpack_require__(25); + var moment = __webpack_require__(2); - util.mergeOptions(this.constants, options,'smoothCurves'); - util.mergeOptions(this.constants, options,'hierarchicalLayout'); - util.mergeOptions(this.constants, options,'clustering'); - util.mergeOptions(this.constants, options,'navigation'); - util.mergeOptions(this.constants, options,'keyboard'); - util.mergeOptions(this.constants, options,'dataManipulation'); + /** + * 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, + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [], + redundant: { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [] + } + }; + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 + }, + lineTop: 0 + }; + this.defaultOptions = { + orientation: 'bottom', // supported: 'top', 'bottom' + // TODO: implement timeaxis orientations 'left' and 'right' + showMinorLabels: true, + showMajorLabels: true + }; + this.options = util.extend({}, this.defaultOptions); - if (options.dataManipulation) { - this.editMode = this.constants.dataManipulation.initiallyVisible; - } + this.body = body; + // create the HTML DOM + this._create(); - // TODO: work out these options and document them - if (options.edges) { - if (options.edges.color !== undefined) { - if (util.isString(options.edges.color)) { - this.constants.edges.color = {}; - this.constants.edges.color.color = options.edges.color; - this.constants.edges.color.highlight = options.edges.color; - this.constants.edges.color.hover = options.edges.color; - } - else { - if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;} - if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;} - if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;} - } - } + this.setOptions(options); + } - if (!options.edges.fontColor) { - if (options.edges.color !== undefined) { - if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;} - else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;} - } - } - } + TimeAxis.prototype = new Component(); - if (options.nodes) { - if (options.nodes.color) { - var newColorObj = util.parseColor(options.nodes.color); - this.constants.nodes.color.background = newColorObj.background; - this.constants.nodes.color.border = newColorObj.border; - this.constants.nodes.color.highlight.background = newColorObj.highlight.background; - this.constants.nodes.color.highlight.border = newColorObj.highlight.border; - this.constants.nodes.color.hover.background = newColorObj.hover.background; - this.constants.nodes.color.hover.border = newColorObj.hover.border; - } - } - if (options.groups) { - for (var groupname in options.groups) { - if (options.groups.hasOwnProperty(groupname)) { - var group = options.groups[groupname]; - this.groups.add(groupname, group); - } - } - } + /** + * Set options for the TimeAxis. + * Parameters will be merged in current options. + * @param {Object} options Available options: + * {string} [orientation] + * {boolean} [showMinorLabels] + * {boolean} [showMajorLabels] + */ + TimeAxis.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options); - if (options.tooltip) { - for (prop in options.tooltip) { - if (options.tooltip.hasOwnProperty(prop)) { - this.constants.tooltip[prop] = options.tooltip[prop]; - } + // 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); } - if (options.tooltip.color) { - this.constants.tooltip.color = util.parseColor(options.tooltip.color); + else { + moment.lang(options.locale); } } - - if (options.labels) { - throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.'); - } } + }; - // (Re)loading the mixins that can be enabled or disabled in the options. - // load the force calculation functions, grouped under the physics system. - this._loadPhysicsSystem(); - // load the navigation system. - this._loadNavigationControls(); - // load the data manipulation system - this._loadManipulationSystem(); - // configure the smooth curves - this._configureSmoothCurves(); + /** + * 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 = 'timeaxis foreground'; + this.dom.background.className = 'timeaxis background'; + }; - // bind keys. If disabled, this will not do anything; - this._createKeyBinds(); - this.setSize(this.constants.width, this.constants.height); - this.moving = true; - this.start(); + /** + * Destroy the TimeAxis + */ + 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; }; /** - * Create the main frame for the Network. - * This function is executed once when a Network object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Network.prototype._create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } + TimeAxis.prototype.redraw = function () { + var options = this.options, + props = this.props, + foreground = this.dom.foreground, + background = this.dom.background; - this.frame = document.createElement('div'); - this.frame.className = 'network-frame'; - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + // determine the correct parent DOM element (depending on option orientation) + var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom; + var parentChanged = (foreground.parentNode !== parent); - // create the network 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); - } + // calculate character width and height + this._calculateCharSize(); - var me = this; - this.drag = {}; - this.pinch = {}; - this.hammer = Hammer(this.frame.canvas, { - prevent_default: true - }); - this.hammer.on('tap', me._onTap.bind(me) ); - this.hammer.on('doubletap', me._onDoubleTap.bind(me) ); - this.hammer.on('hold', me._onHold.bind(me) ); - this.hammer.on('pinch', me._onPinch.bind(me) ); - this.hammer.on('touch', me._onTouch.bind(me) ); - this.hammer.on('dragstart', me._onDragStart.bind(me) ); - this.hammer.on('drag', me._onDrag.bind(me) ); - this.hammer.on('dragend', me._onDragEnd.bind(me) ); - this.hammer.on('release', me._onRelease.bind(me) ); - this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); - this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF - this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); + // TODO: recalculate sizes only needed when parent is resized or options is changed + var orientation = this.options.orientation, + showMinorLabels = this.options.showMinorLabels, + showMajorLabels = this.options.showMajorLabels; - // add the frame to the container element - this.containerElement.appendChild(this.frame); + // 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; - }; + props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight - + (options.orientation == '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) + } + if (backgroundNextSibling) { + this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling); + } + else { + this.body.dom.backgroundVertical.appendChild(background) + } + return this._isResized() || parentChanged; + }; /** - * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin + * Repaint major and minor text labels and vertical grid lines * @private */ - Network.prototype._createKeyBinds = function() { - var me = this; - this.mousetrap = mousetrap; + TimeAxis.prototype._repaintLabels = function () { + var orientation = this.options.orientation; - this.mousetrap.reset(); + // calculate range and step (step such that we have space for 7 characters per label) + var start = util.convert(this.body.range.start, 'Number'), + end = util.convert(this.body.range.end, 'Number'), + minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf() + -this.body.util.toTime(0).valueOf(); + var step = new TimeStep(new Date(start), new Date(end), minimumStep); + this.step = step; - if (this.constants.keyboard.enabled == true) { - this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown"); - this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup"); - this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown"); - this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup"); - this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown"); - this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup"); - this.mousetrap.bind("right",this._moveRight.bind(me), "keydown"); - this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup"); - this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown"); - this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown"); - this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown"); - this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown"); - this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown"); - this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup"); - this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); - this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); + // Move all DOM elements to a "redundant" list, where they + // can be picked for re-use, and clear the lists with lines and texts. + // At the end of the function _repaintLabels, left over elements will be cleaned up + var dom = this.dom; + dom.redundant.majorLines = dom.majorLines; + dom.redundant.majorTexts = dom.majorTexts; + dom.redundant.minorLines = dom.minorLines; + dom.redundant.minorTexts = dom.minorTexts; + dom.majorLines = []; + dom.majorTexts = []; + dom.minorLines = []; + dom.minorTexts = []; + + step.first(); + var xFirstMajorLabel = undefined; + var max = 0; + while (step.hasNext() && max < 1000) { + max++; + var cur = step.getCurrent(), + x = this.body.util.toScreen(cur), + isMajor = step.isMajor(); + + // TODO: lines must have a width, such that we can create css backgrounds + + if (this.options.showMinorLabels) { + this._repaintMinorText(x, step.getLabelMinor(), orientation); + } + + if (isMajor && this.options.showMajorLabels) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this._repaintMajorText(x, step.getLabelMajor(), orientation); + } + this._repaintMajorLine(x, orientation); + } + else { + this._repaintMinorLine(x, orientation); + } + + step.next(); } - if (this.constants.dataManipulation.enabled == true) { - this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); - this.mousetrap.bind("del",this._deleteSelected.bind(me)); + // 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 + + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText, orientation); + } } + + // Cleanup leftover DOM elements from the redundant list + util.forEach(this.dom.redundant, function (arr) { + while (arr.length) { + var elem = arr.pop(); + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + }); }; /** - * Get the pointer location from a touch location - * @param {{pageX: Number, pageY: Number}} touch - * @return {{x: Number, y: Number}} pointer + * Create a minor label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) * @private */ - Network.prototype._getPointer = function (touch) { - return { - x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas), - y: touch.pageY - util.getAbsoluteTop(this.frame.canvas) - }; - }; + TimeAxis.prototype._repaintMinorText = function (x, text, orientation) { + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); - /** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ - Network.prototype._onTouch = function (event) { - this.drag.pointer = this._getPointer(event.gesture.center); - this.drag.pinched = false; - this.pinch.scale = this._getScale(); + if (!label) { + // create new label + var content = document.createTextNode(''); + label = document.createElement('div'); + label.appendChild(content); + label.className = 'text minor'; + this.dom.foreground.appendChild(label); + } + this.dom.minorTexts.push(label); - this._handleTouch(this.drag.pointer); - }; + label.childNodes[0].nodeValue = text; - /** - * handle drag start event - * @private - */ - Network.prototype._onDragStart = function () { - this._handleDragStart(); + label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0'; + label.style.left = x + 'px'; + //label.title = title; // TODO: this is a heavy operation }; - /** - * This function is called by _onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. - * + * Create a Major label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) * @private */ - Network.prototype._handleDragStart = function() { - var drag = this.drag; - var node = this._getNodeAt(drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location + TimeAxis.prototype._repaintMajorText = function (x, text, orientation) { + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); - drag.dragging = true; - drag.selection = []; - drag.translation = this._getTranslation(); - drag.nodeId = null; + if (!label) { + // create label + var content = document.createTextNode(text); + label = document.createElement('div'); + label.className = 'text major'; + label.appendChild(content); + this.dom.foreground.appendChild(label); + } + this.dom.majorTexts.push(label); - if (node != null) { - drag.nodeId = node.id; - // select the clicked node if not yet selected - if (!node.isSelected()) { - this._selectObject(node,false); - } + label.childNodes[0].nodeValue = text; + //label.title = title; // TODO: this is a heavy operation - // create an array with the selected nodes and their original location and status - for (var objectId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(objectId)) { - var object = this.selectionObj.nodes[objectId]; - var s = { - id: object.id, - node: object, + label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px'); + label.style.left = x + 'px'; + }; - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.xFixed, - yFixed: object.yFixed - }; + /** + * Create a minor line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) + * @private + */ + TimeAxis.prototype._repaintMinorLine = function (x, orientation) { + // reuse redundant line + var line = this.dom.redundant.minorLines.shift(); - object.xFixed = true; - object.yFixed = true; + if (!line) { + // create vertical line + line = document.createElement('div'); + line.className = 'grid vertical minor'; + this.dom.background.appendChild(line); + } + this.dom.minorLines.push(line); - drag.selection.push(s); - } - } + var props = this.props; + if (orientation == 'top') { + line.style.top = props.majorLabelHeight + 'px'; + } + else { + line.style.top = this.body.domProps.top.height + 'px'; } + line.style.height = props.minorLineHeight + 'px'; + line.style.left = (x - props.minorLineWidth / 2) + 'px'; }; - /** - * handle drag event + * Create a Major line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) * @private */ - Network.prototype._onDrag = function (event) { - this._handleOnDrag(event) - }; + TimeAxis.prototype._repaintMajorLine = function (x, orientation) { + // reuse redundant line + var line = this.dom.redundant.majorLines.shift(); + + if (!line) { + // create vertical line + line = document.createElement('DIV'); + line.className = 'grid vertical major'; + this.dom.background.appendChild(line); + } + this.dom.majorLines.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'; + }; /** - * This function is called by _onDrag. - * It is separated out because we can then overload it for the datamanipulation system. - * + * 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 */ - Network.prototype._handleOnDrag = function(event) { - if (this.drag.pinched) { - return; - } - - var pointer = this._getPointer(event.gesture.center); - - var me = this; - var drag = this.drag; - var selection = drag.selection; - if (selection && selection.length && this.constants.dragNodes == true) { - // calculate delta's and new location - var deltaX = pointer.x - drag.pointer.x; - var deltaY = pointer.y - drag.pointer.y; - - // update position of all selected nodes - selection.forEach(function (s) { - var node = s.node; - - if (!s.xFixed) { - node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); - } - - if (!s.yFixed) { - node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); - } - }); + 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. + // determine the char width and height on the minor axis + if (!this.dom.measureCharMinor) { + this.dom.measureCharMinor = document.createElement('DIV'); + this.dom.measureCharMinor.className = 'text minor measure'; + this.dom.measureCharMinor.style.position = 'absolute'; - // start _animationStep if not yet running - if (!this.moving) { - this.moving = true; - this.start(); - } + this.dom.measureCharMinor.appendChild(document.createTextNode('0')); + this.dom.foreground.appendChild(this.dom.measureCharMinor); } - else { - if (this.constants.dragNetwork == true) { - // move the network - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; + this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; + this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; - this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY - ); - this._redraw(); - // this.moving = true; - // this.start(); - } + // determine the char width and height on the major axis + if (!this.dom.measureCharMajor) { + this.dom.measureCharMajor = document.createElement('DIV'); + this.dom.measureCharMajor.className = 'text minor 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; }; /** - * handle drag start event - * @private + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * @param {Date} date the date to be snapped. + * @return {Date} snappedDate */ - Network.prototype._onDragEnd = function () { - this.drag.dragging = false; - var selection = this.drag.selection; - if (selection && selection.length) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; - }); - this.moving = true; - this.start(); - } - else { - this._redraw(); - } + TimeAxis.prototype.snap = function(date) { + return this.step.snap(date); + }; + + module.exports = TimeAxis; + - }; +/***/ }, +/* 25 */ +/***/ function(module, exports, __webpack_require__) { + + var moment = __webpack_require__(2); /** - * handle tap/click event: select/unselect a node - * @private + * @constructor TimeStep + * The class TimeStep is an iterator for dates. You provide a start date and an + * end date. The class itself determines the best scale (step size) based on the + * provided start Date, end Date, and minimumStep. + * + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * + * Alternatively, you can set a scale by hand. + * After creation, you can initialize the class by executing first(). Then you + * can iterate from the start date to the end date via next(). You can check if + * the end date is reached with the function hasNext(). After each step, you can + * retrieve the current date via getCurrent(). + * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, + * days, to years. + * + * Version: 1.2 + * + * @param {Date} [start] The start date, for example new Date(2010, 9, 21) + * or new Date(2010, 9, 21, 23, 45, 00) + * @param {Date} [end] The end date + * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ - Network.prototype._onTap = function (event) { - var pointer = this._getPointer(event.gesture.center); - this.pointerPosition = pointer; - this._handleTap(pointer); + function TimeStep(start, end, minimumStep) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); - }; + this.autoScale = true; + this.scale = TimeStep.SCALE.DAY; + this.step = 1; + // initialize the range + this.setRange(start, end, minimumStep); + } - /** - * handle doubletap event - * @private - */ - Network.prototype._onDoubleTap = function (event) { - var pointer = this._getPointer(event.gesture.center); - this._handleDoubleTap(pointer); + /// enum scale + TimeStep.SCALE = { + MILLISECOND: 1, + SECOND: 2, + MINUTE: 3, + HOUR: 4, + DAY: 5, + WEEKDAY: 6, + MONTH: 7, + YEAR: 8 }; /** - * handle long tap event: multi select nodes - * @private + * 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 */ - Network.prototype._onHold = function (event) { - var pointer = this._getPointer(event.gesture.center); - this.pointerPosition = pointer; - this._handleOnHold(pointer); + TimeStep.prototype.setRange = function(start, end, minimumStep) { + if (!(start instanceof Date) || !(end instanceof Date)) { + throw "No legal start or end date in method setRange"; + } + + this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); + this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } }; /** - * handle the release of the screen - * - * @private + * Set the range iterator to the start date. */ - Network.prototype._onRelease = function (event) { - var pointer = this._getPointer(event.gesture.center); - this._handleOnRelease(pointer); + TimeStep.prototype.first = function() { + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); }; /** - * Handle pinch event - * @param event - * @private + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date */ - Network.prototype._onPinch = function (event) { - var pointer = this._getPointer(event.gesture.center); - - this.drag.pinched = true; - if (!('scale' in this.pinch)) { - this.pinch.scale = 1; + TimeStep.prototype.roundToMinor = function() { + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.YEAR: + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case TimeStep.SCALE.MONTH: this.current.setDate(1); + case TimeStep.SCALE.DAY: // intentional fall through + case TimeStep.SCALE.WEEKDAY: this.current.setHours(0); + case TimeStep.SCALE.HOUR: this.current.setMinutes(0); + case TimeStep.SCALE.MINUTE: this.current.setSeconds(0); + case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0); + //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds } - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.gesture.scale; - this._zoom(scale, pointer) + if (this.step != 1) { + // round down to the first minor value that is a multiple of the current step size + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break; + default: break; + } + } }; /** - * Zoom the network in or out - * @param {Number} scale a number around 1, and between 0.01 and 10 - * @param {{x: Number, y: Number}} pointer Position on screen - * @return {Number} appliedScale scale is limited within the boundaries - * @private + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date */ - Network.prototype._zoom = function(scale, pointer) { - if (this.constants.zoomable == true) { - var scaleOld = this._getScale(); - if (scale < 0.00001) { - scale = 0.00001; - } - if (scale > 10) { - scale = 10; - } - - var preScaleDragPointer = null; - if (this.drag !== undefined) { - if (this.drag.dragging == true) { - preScaleDragPointer = this.DOMtoCanvas(this.drag.pointer); - } - } - // + this.frame.canvas.clientHeight / 2 - var translation = this._getTranslation(); - - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - - this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), - "y" : this._YconvertDOMtoCanvas(pointer.y)}; - - this._setScale(scale); - this._setTranslation(tx, ty); - this.updateClustersDefault(); - - if (preScaleDragPointer != null) { - var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; - } - - this._redraw(); - - if (scaleOld < scale) { - this.emit("zoom", {direction:"+"}); - } - else { - this.emit("zoom", {direction:"-"}); - } - - return scale; - } + TimeStep.prototype.hasNext = function () { + return (this.current.valueOf() <= this._end.valueOf()); }; - /** - * Event handler for mouse wheel event, used to zoom the timeline - * See http://adomas.org/javascript-mouse-wheel/ - * https://github.com/EightMedia/hammer.js/issues/256 - * @param {MouseEvent} event - * @private + * Do the next step */ - Network.prototype._onMouseWheel = function(event) { - // retrieve delta - var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta/120; - } else if (event.detail) { /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail/3; - } + TimeStep.prototype.next = function() { + var prev = this.current.valueOf(); - // 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) { + // Two cases, needed to prevent issues with switching daylight savings + // (end of March and end of October) + if (this.current.getMonth() < 6) { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: - // calculate the new scale - var scale = this._getScale(); - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); + this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; + case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; + case TimeStep.SCALE.HOUR: + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); + // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) + var h = this.current.getHours(); + this.current.setHours(h - (h % this.step)); + break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; + } + } + else { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break; + case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break; + case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break; + case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break; + case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break; + case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break; + default: break; } - scale *= (1 + zoom); - - // calculate the pointer location - var gesture = hammerUtil.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); + } - // apply the new scale - this._zoom(scale, pointer); + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; + case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; + case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; + case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; + case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; + case TimeStep.SCALE.YEAR: break; // nothing to do for year + default: break; + } } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); + } }; /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private + * Get the current datetime + * @return {Date} current The current date */ - Network.prototype._onMouseMoveTitle = function (event) { - var gesture = hammerUtil.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); + TimeStep.prototype.getCurrent = function() { + return this.current; + }; - // check if the previously selected node is still selected - if (this.popupObj) { - this._checkHidePopup(pointer); - } + /** + * Set a custom scale. Autoscaling will be disabled. + * For example setScale(SCALE.MINUTES, 5) will result + * in minor steps of 5 minutes, and major steps of an hour. + * + * @param {TimeStep.SCALE} newScale + * A scale. Choose from SCALE.MILLISECOND, + * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, + * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, + * SCALE.YEAR. + * @param {Number} newStep A step size, by default 1. Choose for + * example 1, 2, 5, or 10. + */ + TimeStep.prototype.setScale = function(newScale, newStep) { + this.scale = newScale; - // start a timeout that will check if the mouse is positioned above - // an element - var me = this; - var checkShow = function() { - me._checkShowPopup(pointer); - }; - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running calculationTimer - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); + if (newStep > 0) { + this.step = newStep; } + this.autoScale = false; + }; - /** - * Adding hover highlights - */ - if (this.constants.hover == true) { - // removing all 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]; - } - } + /** + * Enable or disable autoscaling + * @param {boolean} enable If true, autoascaling is set true + */ + TimeStep.prototype.setAutoScale = function (enable) { + this.autoScale = enable; + }; - // adding hover highlights - var obj = this._getNodeAt(pointer); - if (obj == null) { - obj = this._getEdgeAt(pointer); - } - if (obj != null) { - this._hoverObject(obj); - } - // removing all node hover highlights except for the selected one. - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { - this._blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } - } - } - this.redraw(); + /** + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds + */ + TimeStep.prototype.setMinimumStep = function(minimumStep) { + if (minimumStep == undefined) { + return; } + + var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); + var stepMonth = (1000 * 60 * 60 * 24 * 30); + var stepDay = (1000 * 60 * 60 * 24); + var stepHour = (1000 * 60 * 60); + var stepMinute = (1000 * 60); + var stepSecond = (1000); + var stepMillisecond= (1); + + // find the smallest step that is larger than the provided minimumStep + if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} + if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} + if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} + if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} + if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} + if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} + if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} + if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} + if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} + if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} + if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} + if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} + if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} + if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} + if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} + if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} + if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} + if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} + if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} + if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} + if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} + if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} + if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} + if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} + if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} + if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} + if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} + if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} + if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} }; /** - * Check if there is an element on the given position in the network - * (a node or edge). If so, and if this element has a title, - * show a popup window with its title. - * - * @param {{x:Number, y:Number}} pointer - * @private + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * @param {Date} date the date to be snapped. + * @return {Date} snappedDate */ - Network.prototype._checkShowPopup = function (pointer) { - var obj = { - left: this._XconvertDOMtoCanvas(pointer.x), - top: this._YconvertDOMtoCanvas(pointer.y), - right: this._XconvertDOMtoCanvas(pointer.x), - bottom: this._YconvertDOMtoCanvas(pointer.y) - }; + TimeStep.prototype.snap = function(date) { + var clone = new Date(date.valueOf()); - var id; - var lastPopupNode = this.popupObj; + if (this.scale == TimeStep.SCALE.YEAR) { + var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); + clone.setFullYear(Math.round(year / this.step) * this.step); + clone.setMonth(0); + clone.setDate(0); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.MONTH) { + if (clone.getDate() > 15) { + clone.setDate(1); + clone.setMonth(clone.getMonth() + 1); + // important: first set Date to 1, after that change the month. + } + else { + clone.setDate(1); + } - if (this.popupObj == undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodes = this.nodes; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) { - this.popupObj = node; - break; - } - } + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.DAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 24) * 24); break; + default: + clone.setHours(Math.round(clone.getHours() / 12) * 12); break; } + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); } - - if (this.popupObj === undefined) { - // search the edges for overlap - var edges = this.edges; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected && (edge.getTitle() !== undefined) && - edge.isOverlappingWith(obj)) { - this.popupObj = edge; - break; - } - } + else if (this.scale == TimeStep.SCALE.WEEKDAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.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 (this.scale == TimeStep.SCALE.HOUR) { + switch (this.step) { + case 4: + clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; + default: + clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; + } + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (this.scale == TimeStep.SCALE.MINUTE) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); + clone.setSeconds(0); + break; + case 5: + clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; + default: + clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; } + clone.setMilliseconds(0); } - - if (this.popupObj) { - // show popup message window - if (this.popupObj != lastPopupNode) { - var me = this; - if (!me.popup) { - me.popup = new Popup(me.frame, me.constants.tooltip); - } - - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - me.popup.setPosition(pointer.x - 3, pointer.y - 3); - me.popup.setText(me.popupObj.getTitle()); - me.popup.show(); + else if (this.scale == TimeStep.SCALE.SECOND) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); + clone.setMilliseconds(0); + break; + case 5: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; + default: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; } } - else { - if (this.popup) { - this.popup.hide(); - } + else if (this.scale == TimeStep.SCALE.MILLISECOND) { + var step = this.step > 5 ? this.step / 2 : 1; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step); } + + return clone; }; - /** - * Check if the popup must be hided, which is the case when the mouse is no - * longer hovering on the object - * @param {{x:Number, y:Number}} pointer - * @private + * 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. */ - Network.prototype._checkHidePopup = function (pointer) { - if (!this.popupObj || !this._getNodeAt(pointer) ) { - this.popupObj = undefined; - if (this.popup) { - this.popup.hide(); - } + TimeStep.prototype.isMajor = function() { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + return (this.current.getMilliseconds() == 0); + case TimeStep.SCALE.SECOND: + return (this.current.getSeconds() == 0); + case TimeStep.SCALE.MINUTE: + return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); + // Note: this is no bug. Major label is equal for both minute and hour scale + case TimeStep.SCALE.HOUR: + return (this.current.getHours() == 0); + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: + return (this.current.getDate() == 1); + case TimeStep.SCALE.MONTH: + return (this.current.getMonth() == 0); + case TimeStep.SCALE.YEAR: + return false; + default: + return false; } }; /** - * Set a new size for the network - * @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%') + * 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 */ - Network.prototype.setSize = function(width, height) { - this.frame.style.width = width; - this.frame.style.height = height; + TimeStep.prototype.getLabelMinor = function(date) { + if (date == undefined) { + date = this.current; + } - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); + case TimeStep.SCALE.SECOND: return moment(date).format('s'); + case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); + case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); + case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); + case TimeStep.SCALE.DAY: return moment(date).format('D'); + case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); + case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); + default: return ''; + } + }; - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; - if (this.manipulationDiv !== undefined) { - this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px"; - } - if (this.navigationDivs !== undefined) { - if (this.navigationDivs['wrapper'] !== undefined) { - this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; - this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; - } + /** + * 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; } - this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height}); + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); + case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); + case TimeStep.SCALE.MINUTE: + case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); + case TimeStep.SCALE.WEEKDAY: + case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); + case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); + case TimeStep.SCALE.YEAR: return ''; + default: return ''; + } }; + module.exports = TimeStep; + + +/***/ }, +/* 26 */ +/***/ function(module, exports, __webpack_require__) { + + var util = __webpack_require__(1); + var Component = __webpack_require__(22); + var moment = __webpack_require__(2); + var locales = __webpack_require__(27); + /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. - * @private + * A current time bar + * @param {{range: Range, dom: Object, domProps: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCurrentTime] + * @constructor CurrentTime + * @extends Component */ - Network.prototype._setNodes = function(nodes) { - var oldNodesData = this.nodesData; + function CurrentTime (body, options) { + this.body = body; - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.nodesData = nodes; - } - else if (nodes instanceof Array) { - this.nodesData = new DataSet(); - this.nodesData.add(nodes); - } - else if (!nodes) { - this.nodesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); - } + // default options + this.defaultOptions = { + showCurrentTime: true, - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.off(event, callback); - }); - } + locales: locales, + locale: 'en' + }; + this.options = util.extend({}, this.defaultOptions); - // remove drawn nodes - this.nodes = {}; + this._create(); - if (this.nodesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.nodesListeners, function (callback, event) { - me.nodesData.on(event, callback); - }); + this.setOptions(options); + } - // draw all new nodes - var ids = this.nodesData.getIds(); - this._addNodes(ids); - } - this._updateSelection(); - }; + CurrentTime.prototype = new Component(); /** - * Add nodes - * @param {Number[] | String[]} ids + * Create the HTML DOM for the current time bar * @private */ - Network.prototype._addNodes = function(ids) { - var id; - for (var i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - var data = this.nodesData.get(id); - var node = new Node(data, this.images, this.groups, this.constants); - this.nodes[id] = node; // note: this may replace an existing node - if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { - var radius = 10 * 0.1*ids.length + 10; - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) {node.x = radius * Math.cos(angle);} - if (node.yFixed == false) {node.y = radius * Math.sin(angle);} - } - this.moving = true; - } - this._updateNodeIndexList(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this._updateCalculationNodes(); - this._reconnectEdges(); - this._updateValueRange(this.nodes); - this.updateLabels(); + CurrentTime.prototype._create = function() { + var bar = document.createElement('div'); + bar.className = 'currenttime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + + this.bar = bar; + }; + + /** + * Destroy the CurrentTime bar + */ + CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing + + this.body = null; }; /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] */ - Network.prototype._updateNodes = function(ids) { - var nodes = this.nodes, - nodesData = this.nodesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = nodesData.get(id); - if (node) { - // update node - node.setProperties(data, this.constants); - } - else { - // create node - node = new Node(properties, this.images, this.groups, this.constants); - nodes[id] = node; - } - } - this.moving = true; - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); + CurrentTime.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['showCurrentTime', 'locale', 'locales'], this.options, options); } - this._updateNodeIndexList(); - this._reconnectEdges(); - this._updateValueRange(nodes); }; /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Network.prototype._removeNodes = function(ids) { - var nodes = this.nodes; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - delete nodes[id]; + 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); + + this.start(); + } + + var now = new Date(); + var x = this.body.util.toScreen(now); + + var locale = this.options.locales[this.options.locale]; + 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; } - this._updateNodeIndexList(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); + else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + this.stop(); } - this._updateCalculationNodes(); - this._reconnectEdges(); - this._updateSelection(); - this._updateValueRange(nodes); + + return false; }; /** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private - * @private + * Start auto refreshing the current time bar */ - Network.prototype._setEdges = function(edges) { - var oldEdgesData = this.edgesData; + CurrentTime.prototype.start = function() { + var me = this; - if (edges instanceof DataSet || edges instanceof DataView) { - this.edgesData = edges; - } - else if (edges instanceof Array) { - this.edgesData = new DataSet(); - this.edgesData.add(edges); - } - else if (!edges) { - this.edgesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); + 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 timer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); } - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.off(event, callback); - }); + update(); + }; + + /** + * Stop auto refreshing the current time bar + */ + CurrentTime.prototype.stop = function() { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; } + }; - // remove drawn edges - this.edges = {}; + module.exports = CurrentTime; - if (this.edgesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.edgesListeners, function (callback, event) { - me.edgesData.on(event, callback); - }); - // draw all new nodes - var ids = this.edgesData.getIds(); - this._addEdges(ids); - } +/***/ }, +/* 27 */ +/***/ function(module, exports, __webpack_require__) { - this._reconnectEdges(); + // English + exports['en'] = { + current: 'current', + time: 'time' + }; + exports['en_EN'] = exports['en']; + exports['en_US'] = exports['en']; + + // Dutch + exports['nl'] = { + custom: 'aangepaste', + time: 'tijd' }; + exports['nl_NL'] = exports['nl']; + exports['nl_BE'] = exports['nl']; + + +/***/ }, +/* 28 */ +/***/ function(module, exports, __webpack_require__) { + + var Hammer = __webpack_require__(18); + var util = __webpack_require__(1); + var Component = __webpack_require__(22); + var moment = __webpack_require__(2); + var locales = __webpack_require__(27); /** - * Add edges - * @param {Number[] | String[]} ids - * @private + * A custom time bar + * @param {{range: Range, dom: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCustomTime] + * @constructor CustomTime + * @extends Component */ - Network.prototype._addEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; + function CustomTime (body, options) { + this.body = body; - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } + // default options + this.defaultOptions = { + showCustomTime: false, + locales: locales, + locale: 'en' + }; + this.options = util.extend({}, this.defaultOptions); - var data = edgesData.get(id, {"showInternalIds" : true}); - edges[id] = new Edge(data, this, this.constants); - } + this.customTime = new Date(); + this.eventParams = {}; // stores state parameters while dragging the bar - this.moving = true; - this._updateValueRange(edges); - this._createBezierNodes(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); + // create the DOM + this._create(); + + this.setOptions(options); + } + + CustomTime.prototype = new Component(); + + /** + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCustomTime] + */ + CustomTime.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['showCustomTime', 'locale', 'locales'], this.options, options); } - this._updateCalculationNodes(); }; /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private + * Create the DOM for the custom time + * @private + */ + CustomTime.prototype._create = function() { + var bar = document.createElement('div'); + bar.className = 'customtime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + this.bar = bar; + + var drag = document.createElement('div'); + drag.style.position = 'relative'; + drag.style.top = '0px'; + drag.style.left = '-10px'; + drag.style.height = '100%'; + drag.style.width = '20px'; + bar.appendChild(drag); + + // attach event listeners + this.hammer = Hammer(bar, { + prevent_default: true + }); + this.hammer.on('dragstart', this._onDragStart.bind(this)); + this.hammer.on('drag', this._onDrag.bind(this)); + this.hammer.on('dragend', this._onDragEnd.bind(this)); + }; + + /** + * Destroy the CustomTime bar */ - Network.prototype._updateEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; + CustomTime.prototype.destroy = function () { + this.options.showCustomTime = false; + this.redraw(); // will remove the bar from the DOM - var data = edgesData.get(id); - var edge = edges[id]; - if (edge) { - // update edge - edge.disconnect(); - edge.setProperties(data, this.constants); - edge.connect(); - } - else { - // create edge - edge = new Edge(data, this, this.constants); - this.edges[id] = edge; - } - } + this.hammer.enable(false); + this.hammer = null; - this._createBezierNodes(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this.moving = true; - this._updateValueRange(edges); + this.body = null; }; /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Network.prototype._removeEdges = function (ids) { - var edges = this.edges; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge) { - if (edge.via != null) { - delete this.sectors['support']['nodes'][edge.via.id]; + CustomTime.prototype.redraw = function () { + if (this.options.showCustomTime) { + 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); } - edge.disconnect(); - delete edges[id]; + parent.appendChild(this.bar); } - } - this.moving = true; - this._updateValueRange(edges); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); + var x = this.body.util.toScreen(this.customTime); + + var locale = this.options.locales[this.options.locale]; + 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; } - this._updateCalculationNodes(); + else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + } + + return false; }; /** - * Reconnect all edges + * Set custom time. + * @param {Date} time + */ + CustomTime.prototype.setCustomTime = function(time) { + this.customTime = new Date(time.valueOf()); + this.redraw(); + }; + + /** + * Retrieve the current custom time. + * @return {Date} customTime + */ + CustomTime.prototype.getCustomTime = function() { + return new Date(this.customTime.valueOf()); + }; + + /** + * Start moving horizontally + * @param {Event} event * @private */ - Network.prototype._reconnectEdges = function() { - var id, - nodes = this.nodes, - edges = this.edges; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } + CustomTime.prototype._onDragStart = function(event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } - } + event.stopPropagation(); + event.preventDefault(); }; /** - * 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). + * Perform moving operating. + * @param {Event} event * @private */ - Network.prototype._updateValueRange = function(obj) { - var id; + CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); - valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); - } - } - } + var deltaX = event.gesture.deltaX, + x = this.body.util.toScreen(this.eventParams.customTime) + deltaX, + time = this.body.util.toTime(x); - // adjust the range of all objects - if (valueMin !== undefined && valueMax !== undefined) { - for (id in obj) { - if (obj.hasOwnProperty(id)) { - obj[id].setValueRange(valueMin, valueMax); - } - } - } + this.setCustomTime(time); + + // fire a timechange event + this.body.emitter.emit('timechange', { + time: new Date(this.customTime.valueOf()) + }); + + event.stopPropagation(); + event.preventDefault(); }; /** - * Redraw the network with the current data - * chart will be resized too. + * Stop moving operating. + * @param {event} event + * @private */ - Network.prototype.redraw = function() { - this.setSize(this.constants.width, this.constants.height); - this._redraw(); + CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; + + // fire a timechanged event + this.body.emitter.emit('timechanged', { + time: new Date(this.customTime.valueOf()) + }); + + event.stopPropagation(); + event.preventDefault(); }; + module.exports = CustomTime; + + +/***/ }, +/* 29 */ +/***/ function(module, exports, __webpack_require__) { + + var Hammer = __webpack_require__(18); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(8); + var Component = __webpack_require__(22); + var Group = __webpack_require__(30); + var ItemBox = __webpack_require__(34); + var ItemPoint = __webpack_require__(35); + var ItemRange = __webpack_require__(32); + + + var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + /** - * Redraw the network with the current data - * @private + * 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 */ - Network.prototype._redraw = function() { - var ctx = this.frame.canvas.getContext('2d'); - // clear the canvas - var w = this.frame.canvas.width; - var h = this.frame.canvas.height; - ctx.clearRect(0, 0, w, h); + function ItemSet(body, options) { + this.body = body; - // set scaling and translation - ctx.save(); - ctx.translate(this.translation.x, this.translation.y); - ctx.scale(this.scale, this.scale); + this.defaultOptions = { + type: null, // 'box', 'point', 'range' + orientation: 'bottom', // 'top' or 'bottom' + align: 'center', // alignment of box items + stack: true, + groupOrder: null, + + selectable: true, + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, + + onAdd: function (item, callback) { + callback(item); + }, + onUpdate: function (item, callback) { + callback(item); + }, + onMove: function (item, callback) { + callback(item); + }, + onRemove: function (item, callback) { + callback(item); + }, + + margin: { + item: { + horizontal: 10, + vertical: 10 + }, + axis: 20 + }, + padding: 5 + }; - this.canvasTopLeft = { - "x": this._XconvertDOMtoCanvas(0), - "y": this._YconvertDOMtoCanvas(0) + // options is shared by this ItemSet and all its items + this.options = util.extend({}, this.defaultOptions); + + // options for getting items from the DataSet with the correct type + this.itemOptions = { + type: {start: 'Date', end: 'Date'} }; - this.canvasBottomRight = { - "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth), - "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight) + + this.conversion = { + toScreen: body.util.toScreen, + toTime: body.util.toTime }; + this.dom = {}; + this.props = {}; + this.hammer = null; + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - this._doInAllSectors("_drawAllSectorNodes",ctx); - if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { - this._doInAllSectors("_drawEdges",ctx); - } + // listeners for the DataSet of the items + this.itemListeners = { + 'add': function (event, params, senderId) { + me._onAdd(params.items); + }, + 'update': function (event, params, senderId) { + me._onUpdate(params.items); + }, + 'remove': function (event, params, senderId) { + me._onRemove(params.items); + } + }; - if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { - this._doInAllSectors("_drawNodes",ctx,false); - } + // listeners for the DataSet of the groups + this.groupListeners = { + 'add': function (event, params, senderId) { + me._onAddGroups(params.items); + }, + 'update': function (event, params, senderId) { + me._onUpdateGroups(params.items); + }, + 'remove': function (event, params, senderId) { + me._onRemoveGroups(params.items); + } + }; - if (this.controlNodesActive == true) { - this._doInAllSectors("_drawControlNodes",ctx); - } + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group + this.groupIds = []; - // this._doInSupportSector("_drawNodes",ctx,true); - // this._drawTree(ctx,"#F00F0F"); + this.selection = []; // list with the ids of all selected nodes + this.stackDirty = true; // if true, all items will be restacked on next redraw - // restore original scaling and translation - ctx.restore(); - }; + this.touchParams = {}; // stores properties while dragging + // create the HTML DOM - /** - * Set the translation of the network - * @param {Number} offsetX Horizontal offset - * @param {Number} offsetY Vertical offset - * @private - */ - Network.prototype._setTranslation = function(offsetX, offsetY) { - if (this.translation === undefined) { - this.translation = { - x: 0, - y: 0 - }; - } + this._create(); - if (offsetX !== undefined) { - this.translation.x = offsetX; - } - if (offsetY !== undefined) { - this.translation.y = offsetY; - } + this.setOptions(options); + } - this.emit('viewChanged'); - }; + ItemSet.prototype = new Component(); - /** - * Get the translation of the network - * @return {Object} translation An object with parameters x and y, both a number - * @private - */ - Network.prototype._getTranslation = function() { - return { - x: this.translation.x, - y: this.translation.y - }; + // available item types will be registered here + ItemSet.types = { + box: ItemBox, + range: ItemRange, + point: ItemPoint }; /** - * Scale the network - * @param {Number} scale Scaling factor 1.0 is unscaled - * @private + * Create the HTML DOM for the ItemSet */ - Network.prototype._setScale = function(scale) { - this.scale = scale; - }; + ItemSet.prototype._create = function(){ + var frame = document.createElement('div'); + frame.className = 'itemset'; + frame['timeline-itemset'] = this; + this.dom.frame = frame; - /** - * Get the current scale of the network - * @return {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._getScale = function() { - return this.scale; - }; + // create background panel + var background = document.createElement('div'); + background.className = 'background'; + frame.appendChild(background); + this.dom.background = background; - /** - * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} x - * @returns {number} - * @private - */ - Network.prototype._XconvertDOMtoCanvas = function(x) { - return (x - this.translation.x) / this.scale; - }; + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; - /** - * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the X coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} x - * @returns {number} - * @private - */ - Network.prototype._XconvertCanvasToDOM = function(x) { - return x * this.scale + this.translation.x; - }; + // create axis panel + var axis = document.createElement('div'); + axis.className = 'axis'; + this.dom.axis = axis; - /** - * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to - * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) - * @param {number} y - * @returns {number} - * @private - */ - Network.prototype._YconvertDOMtoCanvas = function(y) { - return (y - this.translation.y) / this.scale; - }; + // create labelset + var labelSet = document.createElement('div'); + labelSet.className = 'labelset'; + this.dom.labelSet = labelSet; - /** - * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to - * the Y coordinate in DOM-space (coordinate point in browser relative to the container div) - * @param {number} y - * @returns {number} - * @private - */ - Network.prototype._YconvertCanvasToDOM = function(y) { - return y * this.scale + this.translation.y ; - }; + // create ungrouped Group + this._updateUngrouped(); + + // 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 = Hammer(this.body.dom.centerContainer, { + prevent_default: true + }); + // drag items when selected + this.hammer.on('touch', this._onTouch.bind(this)); + this.hammer.on('dragstart', this._onDragStart.bind(this)); + this.hammer.on('drag', this._onDrag.bind(this)); + this.hammer.on('dragend', this._onDragEnd.bind(this)); - /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ - Network.prototype.canvasToDOM = function(pos) { - return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)}; - } + // single select (or unselect) when tapping an item + this.hammer.on('tap', this._onSelectItem.bind(this)); - /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ - Network.prototype.DOMtoCanvas = function(pos) { - return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)}; - } + // multi select when holding mouse/touch, or on ctrl+click + this.hammer.on('hold', this._onMultiSelectItem.bind(this)); + + // add item on doubletap + this.hammer.on('doubletap', this._onAddItem.bind(this)); + + // attach to the DOM + this.show(); + }; /** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @param {Boolean} [alwaysShow] - * @private + * 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', or 'range'. The default + * Style can be overwritten by individual items. + * {String} align + * Alignment for the items, only applicable for + * ItemBox. Choose 'center' (default), 'left', or + * 'right'. + * {String} orientation + * Orientation of the item set. Choose 'top' or + * 'bottom' (default). + * {Function} groupOrder + * A sorting function for ordering groups + * {Boolean} stack + * If true (deafult), 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. + * {Number} padding + * Padding of the contents of an item in pixels. + * Must correspond with the items css. Default is 5. + * {Boolean} selectable + * If true (default), items can be selected. + * {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. */ - Network.prototype._drawNodes = function(ctx,alwaysShow) { - if (alwaysShow === undefined) { - alwaysShow = false; - } - - // first draw the unselected nodes - var nodes = this.nodes; - var selected = []; + ItemSet.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder']; + util.selectiveExtend(fields, this.options, options); - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight); - if (nodes[id].isSelected()) { - selected.push(id); + 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 (nodes[id].inArea() || alwaysShow) { - nodes[id].draw(ctx); + 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); + } } } } - } - - // draw the selected nodes on top - for (var s = 0, sMax = selected.length; s < sMax; s++) { - if (nodes[selected[s]].inArea() || alwaysShow) { - nodes[selected[s]].draw(ctx); - } - } - }; - /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Network.prototype._drawEdges = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.setScale(this.scale); - if (edge.connected) { - edges[id].draw(ctx); + 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); } } - } - }; - - /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Network.prototype._drawControlNodes = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - edges[id]._drawControlNodes(ctx); - } - } - }; - /** - * Find a stable position for all nodes - * @private - */ - Network.prototype._stabilize = function() { - if (this.constants.freezeForStabilization == true) { - this._freezeDefinedNodes(); - } + // callback functions + var addCallback = (function (name) { + if (name in options) { + var fn = options[name]; + 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'].forEach(addCallback); - // find stable position - var count = 0; - while (this.moving && count < this.constants.stabilizationIterations) { - this._physicsTick(); - count++; - } - this.zoomExtent(false,true); - if (this.constants.freezeForStabilization == true) { - this._restoreFrozenNodes(); + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); } - this.emit("stabilized",{iterations:count}); }; /** - * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization - * because only the supportnodes for the smoothCurves have to settle. - * - * @private + * Mark the ItemSet dirty so it will refresh everything with next redraw */ - Network.prototype._freezeDefinedNodes = function() { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].x != null && nodes[id].y != null) { - nodes[id].fixedData.x = nodes[id].xFixed; - nodes[id].fixedData.y = nodes[id].yFixed; - nodes[id].xFixed = true; - nodes[id].yFixed = true; - } - } - } + ItemSet.prototype.markDirty = function() { + this.groupIds = []; + this.stackDirty = true; }; /** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * - * @private + * Destroy the ItemSet */ - Network.prototype._restoreFrozenNodes = function() { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].fixedData.x != null) { - nodes[id].xFixed = nodes[id].fixedData.x; - nodes[id].yFixed = nodes[id].fixedData.y; - } - } - } - }; + ItemSet.prototype.destroy = function() { + this.hide(); + this.setItems(null); + this.setGroups(null); + this.hammer = null; - /** - * Check if any of the nodes is still moving - * @param {number} vmin the minimum velocity considered as 'moving' - * @return {boolean} true if moving, false if non of the nodes is moving - * @private - */ - Network.prototype._isMoving = function(vmin) { - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { - return true; - } - } - return false; + this.body = null; + this.conversion = null; }; - /** - * /** - * Perform one discrete step for all nodes - * - * @private + * Hide the component from the DOM */ - Network.prototype._discreteStepNodes = function(checkMovement) { - var interval = this.physicsDiscreteStepsize; - var nodes = this.nodes; - var nodeId; - var nodesPresent = false; - - if (this.constants.maxVelocity > 0) { - for (nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity); - nodesPresent = true; - } - } - } - else { - for (nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - nodes[nodeId].discreteStep(interval); - nodesPresent = true; - } - } + ItemSet.prototype.hide = function() { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); } - if (nodesPresent == true && (checkMovement === undefined || checkMovement == true)) { - var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05); - if (vminCorrected > 0.5*this.constants.maxVelocity) { - this.moving = true; - } - else { - this.moving = this._isMoving(vminCorrected); - if (this.moving == false) { - this.emit("stabilized",{iterations:null}); - } - this.moving = this.moving || this.configurePhysics; + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } - } + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); } }; /** - * A single simulation step (or "tick") in the physics simulation - * - * @private + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed */ - Network.prototype._physicsTick = function() { - if (!this.freezeSimulation) { - if (this.moving == true) { - this._doInAllActiveSectors("_initializeForceCalculation"); - this._doInAllActiveSectors("_discreteStepNodes"); - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._doInSupportSector("_discreteStepNodes", false); - } - this._findCenter(this._getRange()) - } + 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); + } + }; /** - * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick. - * It reschedules itself at the beginning of the function - * - * @private + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. */ - Network.prototype._animationStep = function() { - // reset the timer so a new scheduled animation step can be set - this.timer = undefined; - // handle the keyboad movement - this._handleNavigation(); + ItemSet.prototype.setSelection = function(ids) { + var i, ii, id, item; - // this schedules a new animation step - this.start(); + if (ids) { + if (!Array.isArray(ids)) { + throw new TypeError('Array expected'); + } - // start the physics simulation - var calculationTime = Date.now(); - var maxSteps = 1; - this._physicsTick(); - var timeRequired = Date.now() - calculationTime; - while (timeRequired < 0.9*(this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) { - this._physicsTick(); - timeRequired = Date.now() - calculationTime; - maxSteps++; - } - // start the rendering process - var renderTime = Date.now(); - this._redraw(); - this.renderTime = Date.now() - renderTime; + // unselect currently selected items + for (i = 0, ii = this.selection.length; i < ii; i++) { + id = this.selection[i]; + item = this.items[id]; + if (item) item.unselect(); + } + // select items + this.selection = []; + for (i = 0, ii = ids.length; i < ii; i++) { + id = ids[i]; + item = this.items[id]; + if (item) { + this.selection.push(id); + item.select(); + } + } + } }; - if (typeof window !== 'undefined') { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || - window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; - } + /** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ + ItemSet.prototype.getSelection = function() { + return this.selection.concat([]); + }; /** - * Schedule a animation step with the refreshrate interval. + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - Network.prototype.start = function() { - if (this.moving == true || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { - if (!this.timer) { - var ua = navigator.userAgent.toLowerCase(); + 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); - var requiresTimeout = false; - if (ua.indexOf('msie 9.0') != -1) { // IE 9 - requiresTimeout = true; - } - else if (ua.indexOf('safari') != -1) { // safari - if (ua.indexOf('chrome') <= -1) { - requiresTimeout = true; - } - } + var ids = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + var group = this.groups[groupId]; + var rawVisibleItems = group.visibleItems; - if (requiresTimeout == true) { - this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function - } - else{ - this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function + // 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); + } } } } - else { - this._redraw(); - } - }; + return ids; + }; /** - * Move the network according to the keyboard presses. - * + * Deselect a selected item + * @param {String | Number} id * @private */ - Network.prototype._handleNavigation = function() { - if (this.xIncrement != 0 || this.yIncrement != 0) { - var translation = this._getTranslation(); - this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement); - } - if (this.zoomIncrement != 0) { - var center = { - x: this.frame.canvas.clientWidth / 2, - y: this.frame.canvas.clientHeight / 2 - }; - this._zoom(this.scale*(1 + this.zoomIncrement), center); + 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; + } } }; - /** - * Freeze the _animationStep + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Network.prototype.toggleFreeze = function() { - if (this.freezeSimulation == false) { - this.freezeSimulation = true; - } - else { - this.freezeSimulation = false; - this.start(); - } - }; + ItemSet.prototype.redraw = function() { + var margin = this.options.margin, + range = this.body.range, + asSize = util.option.asSize, + options = this.options, + orientation = options.orientation, + resized = false, + frame = this.dom.frame, + editable = options.editable.updateTime || options.editable.updateGroup; + // update class name + frame.className = 'itemset' + (editable ? ' editable' : ''); - /** - * This function cleans the support nodes if they are not needed and adds them when they are. - * - * @param {boolean} [disableStart] - * @private - */ - Network.prototype._configureSmoothCurves = function(disableStart) { - if (disableStart === undefined) { - disableStart = true; - } - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._createBezierNodes(); - // cleanup unused support nodes - for (var nodeId in this.sectors['support']['nodes']) { - if (this.sectors['support']['nodes'].hasOwnProperty(nodeId)) { - if (this.edges[this.sectors['support']['nodes'][nodeId].parentEdgeId] === undefined) { - delete this.sectors['support']['nodes'][nodeId]; - } - } - } - } - else { - // delete the support nodes - this.sectors['support']['nodes'] = {}; - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - this.edges[edgeId].via = null; - } - } - } + // reorder the groups (if needed) + resized = this._orderGroups() || resized; + + // 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; + + // redraw all groups + var restack = this.stackDirty, + firstGroup = this._firstGroup(), + firstMargin = { + item: margin.item, + axis: margin.axis + }, + nonFirstMargin = { + item: margin.item, + axis: margin.item.vertical / 2 + }, + height = 0, + minHeight = margin.axis + margin.item.vertical; + 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; + + // update frame height + frame.style.height = asSize(height); + + // calculate actual size and position + this.props.top = frame.offsetTop; + this.props.left = frame.offsetLeft; + 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 = this.body.domProps.border.left + 'px'; - this._updateCalculationNodes(); - if (!disableStart) { - this.moving = true; - this.start(); - } - }; + // check if this component is resized + resized = this._isResized() || resized; + return resized; + }; /** - * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but - * are used for the force calculation. - * + * Get the first group, aligned with the axis + * @return {Group | null} firstGroup * @private */ - Network.prototype._createBezierNodes = function() { - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - for (var edgeId in this.edges) { - if (this.edges.hasOwnProperty(edgeId)) { - var edge = this.edges[edgeId]; - if (edge.via == null) { - var nodeId = "edgeId:".concat(edge.id); - this.sectors['support']['nodes'][nodeId] = new Node( - {id:nodeId, - mass:1, - shape:'circle', - image:"", - internalMultiplier:1 - },{},{},this.constants); - edge.via = this.sectors['support']['nodes'][nodeId]; - edge.via.parentEdgeId = edge.id; - edge.positionBezierNode(); - } - } - } - } + ItemSet.prototype._firstGroup = function() { + var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1); + var firstGroupId = this.groupIds[firstGroupIndex]; + var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; + + return firstGroup || null; }; /** - * load the functions that load the mixins into the prototype. - * - * @private + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. + * @protected */ - Network.prototype._initializeMixinLoaders = function () { - for (var mixin in MixinLoader) { - if (MixinLoader.hasOwnProperty(mixin)) { - Network.prototype[mixin] = MixinLoader[mixin]; + ItemSet.prototype._updateUngrouped = function() { + var ungrouped = this.groups[UNGROUPED]; + + if (this.groupsData) { + // remove the group holding all ungrouped items + if (ungrouped) { + ungrouped.hide(); + delete this.groups[UNGROUPED]; } } - }; + 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; - /** - * Load the XY positions of the nodes into the dataset. - */ - Network.prototype.storePosition = function() { - var dataArray = []; - for (var nodeId in this.nodes) { - if (this.nodes.hasOwnProperty(nodeId)) { - var node = this.nodes[nodeId]; - var allowedToMoveX = !this.nodes.xFixed; - var allowedToMoveY = !this.nodes.yFixed; - if (this.nodesData._data[nodeId].x != Math.round(node.x) || this.nodesData._data[nodeId].y != Math.round(node.y)) { - dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY}); + for (var itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + ungrouped.add(this.items[itemId]); + } } + + ungrouped.show(); } } - this.nodesData.update(dataArray); }; - /** - * Center a node in view. - * - * @param {Number} nodeId - * @param {Number} [zoomLevel] + * Get the element for the labelset + * @return {HTMLElement} labelSet */ - Network.prototype.focusOnNode = function (nodeId, zoomLevel) { - if (this.nodes.hasOwnProperty(nodeId)) { - if (zoomLevel === undefined) { - zoomLevel = this._getScale(); - } - var nodePosition= {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y}; - - var requiredScale = zoomLevel; - this._setScale(requiredScale); - - var canvasCenter = this.DOMtoCanvas({x:0.5 * this.frame.canvas.width,y:0.5 * this.frame.canvas.height}); - var translation = this._getTranslation(); + ItemSet.prototype.getLabelSet = function() { + return this.dom.labelSet; + }; - var distanceFromCenter = {x:canvasCenter.x - nodePosition.x, - y:canvasCenter.y - nodePosition.y}; + /** + * Set items + * @param {vis.DataSet | null} items + */ + ItemSet.prototype.setItems = function(items) { + var me = this, + ids, + oldItemsData = this.itemsData; - this._setTranslation(translation.x + requiredScale * distanceFromCenter.x, - translation.y + requiredScale * distanceFromCenter.y); - this.redraw(); + // replace the dataset + if (!items) { + this.itemsData = null; + } + else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; } else { - console.log("This nodeId cannot be found.") + throw new TypeError('Data must be an instance of DataSet or DataView'); } - }; - - module.exports = Network; - -/***/ }, -/* 33 */ -/***/ function(module, exports, __webpack_require__) { - - var util = __webpack_require__(1); - var Node = __webpack_require__(36); + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with properties. Must contain - * At least properties from and to. - * Available properties: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {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 - */ - function Edge (properties, network, networkConstants) { - if (!network) { - throw "No network provided"; + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); } - var fields = ['edges','physics']; - var constants = util.selectiveBridgeObject(fields,networkConstants); - this.options = constants.edges; - this.physics = constants.physics; - this.options['smoothCurves'] = networkConstants['smoothCurves']; - - - this.network = network; - - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.title = undefined; - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - this.value = undefined; - this.selected = false; - this.hover = false; - - this.from = null; // a node - this.to = null; // a node - this.via = null; // a temp node - - // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster - // by storing the original information we can revert to the original connection when the cluser is opened. - this.originalFromId = []; - this.originalToId = []; - - this.connected = false; - this.widthFixed = false; - this.lengthFixed = false; + 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.setProperties(properties); + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); - this.controlNodesEnabled = false; - this.controlNodes = {from:null, to:null, positions:{}}; - this.connectedNode = null; - } + // update the group holding all ungrouped items + this._updateUngrouped(); + } + }; /** - * Set or overwrite properties for the edge - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties + * Get the current items + * @returns {vis.DataSet | null} */ - Edge.prototype.setProperties = function(properties) { - if (!properties) { - return; - } - - var fields = ['style','fontSize','fontFace','fontColor','fontFill','width', - 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash' - ]; - util.selectiveDeepExtend(fields, this.options, properties); + ItemSet.prototype.getItems = function() { + return this.itemsData; + }; - if (properties.from !== undefined) {this.fromId = properties.from;} - if (properties.to !== undefined) {this.toId = properties.to;} + /** + * Set groups + * @param {vis.DataSet} groups + */ + ItemSet.prototype.setGroups = function(groups) { + var me = this, + ids; - if (properties.id !== undefined) {this.id = properties.id;} - if (properties.label !== undefined) {this.label = properties.label;} + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - if (properties.title !== undefined) {this.title = properties.title;} - if (properties.value !== undefined) {this.value = properties.value;} - if (properties.length !== undefined) {this.physics.springLength = properties.length;} + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - // scale the arrow - if (properties.arrowScaleFactor !== undefined) {this.options.arrowScaleFactor = properties.arrowScaleFactor;} + // replace the dataset + if (!groups) { + this.groupsData = null; + } + else if (groups instanceof DataSet || groups instanceof DataView) { + this.groupsData = groups; + } + else { + throw new TypeError('Data must be an instance of DataSet or DataView'); + } - if (properties.inheritColor !== undefined) {this.options.inheritColor = properties.inheritColor;} + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); - if (properties.color !== undefined) { - this.options.inheritColor = false; - if (util.isString(properties.color)) { - this.options.color.color = properties.color; - this.options.color.highlight = properties.color; - } - else { - if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;} - if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;} - if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;} - } + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); } - // A node is connected when it has a from and to node. - this.connect(); + // update the group holding all ungrouped items + this._updateUngrouped(); - this.widthFixed = this.widthFixed || (properties.width !== undefined); - this.lengthFixed = this.lengthFixed || (properties.length !== undefined); + // update the order of all items in each group + this._order(); - this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; + this.body.emitter.emit('change'); + }; - // set draw method based on style - switch (this.options.style) { - case 'line': this.draw = this._drawLine; break; - case 'arrow': this.draw = this._drawArrow; break; - case 'arrow-center': this.draw = this._drawArrowCenter; break; - case 'dash-line': this.draw = this._drawDashLine; break; - default: this.draw = this._drawLine; break; + /** + * Get the current groups + * @returns {vis.DataSet | null} groups + */ + ItemSet.prototype.getGroups = function() { + return this.groupsData; + }; + + /** + * 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(); + + 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); + } + }); } }; /** - * Connect an edge to its nodes + * Handle updated items + * @param {Number[]} ids + * @protected */ - Edge.prototype.connect = function () { - this.disconnect(); + ItemSet.prototype._onUpdate = function(ids) { + var me = this; - this.from = this.network.nodes[this.fromId] || null; - this.to = this.network.nodes[this.toId] || null; - this.connected = (this.from && this.to); + ids.forEach(function (id) { + var itemData = me.itemsData.get(id, me.itemOptions), + item = me.items[id], + type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box'); - if (this.connected) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } - else { - if (this.from) { - this.from.detachEdge(this); + var constructor = ItemSet.types[type]; + + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, delete the item and recreate it + me._removeItem(item); + item = null; + } + else { + me._updateItem(item, itemData); + } } - if (this.to) { - this.to.detachEdge(this); + + 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); + } + 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.timeline .item.range .content {overflow: visible;}'); + } + else { + throw new TypeError('Unknown item type "' + type + '"'); + } } - } + }); + + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); }; /** - * Disconnect an edge from its nodes + * Handle added items + * @param {Number[]} ids + * @protected */ - Edge.prototype.disconnect = function () { - if (this.from) { - this.from.detachEdge(this); - this.from = null; - } - if (this.to) { - this.to.detachEdge(this); - this.to = null; - } + ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; - this.connected = false; + /** + * 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); + } + }); + + if (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); + } }; /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. + * Update the order of item in all groups + * @private */ - Edge.prototype.getTitle = function() { - return typeof this.title === "function" ? this.title() : this.title; + 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(); + }); }; - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value + * Handle updated groups + * @param {Number[]} ids + * @private */ - Edge.prototype.getValue = function() { - return this.value; + ItemSet.prototype._onUpdateGroups = function(ids) { + this._onAddGroups(ids); }; /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max + * Handle changed groups + * @param {Number[]} ids + * @private */ - Edge.prototype.setValueRange = function(min, max) { - if (!this.widthFixed && this.value !== undefined) { - var scale = (this.options.widthMax - this.options.widthMin) / (max - min); - this.options.width= (this.value - min) * scale + this.options.widthMin; - this.widthSelected = this.options.width* this.options.widthSelectionMultiplier; - } + ItemSet.prototype._onAddGroups = function(ids) { + var me = this; + + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; + + if (!group) { + // check for reserved ids + if (id == UNGROUPED) { + throw new Error('Illegal group id. ' + id + ' is a reserved id.'); + } + + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); + + group = new Group(id, groupData, me); + me.groups[id] = group; + + // 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); + } + } + } + + group.order(); + group.show(); + } + else { + // update group + group.setData(groupData); + } + }); + + this.body.emitter.emit('change'); }; /** - * 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 + * Handle removed groups + * @param {Number[]} ids + * @private */ - Edge.prototype.draw = function(ctx) { - throw "Method draw not initialized in edge"; + ItemSet.prototype._onRemoveGroups = function(ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; + + if (group) { + group.hide(); + delete groups[id]; + } + }); + + this.markDirty(); + + this.body.emitter.emit('change'); }; /** - * 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 + * Reorder the groups if needed + * @return {boolean} changed + * @private */ - Edge.prototype.isOverlappingWith = function(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; + ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder + }); - var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + 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(); + }); - return (dist < distMax); + // show the groups again, attach them to the DOM in correct order + groupIds.forEach(function (groupId) { + groups[groupId].show(); + }); + + this.groupIds = groupIds; + } + + return changed; } else { - return false + return false; } }; - Edge.prototype._getColor = function() { - var colorObj = this.options.color; - if (this.options.inheritColor == "to") { - colorObj = { - highlight: this.to.options.color.highlight.border, - hover: this.to.options.color.hover.border, - color: this.to.options.color.border - }; - } - else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { - colorObj = { - highlight: this.from.options.color.highlight.border, - hover: this.from.options.color.hover.border, - color: this.from.options.color.border - }; - } - - if (this.selected == true) {return colorObj.highlight;} - else if (this.hover == true) {return colorObj.hover;} - else {return colorObj.color;} - } + /** + * Add a new item + * @param {Item} item + * @private + */ + ItemSet.prototype._addItem = function(item) { + this.items[item.id] = item; + // add to group + var groupId = this.groupsData ? item.data.group : UNGROUPED; + var group = this.groups[groupId]; + if (group) group.add(item); + }; /** - * Redraw a edge as a line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * Update an existing item + * @param {Item} item + * @param {Object} itemData * @private */ - Edge.prototype._drawLine = function(ctx) { - // set style - ctx.strokeStyle = this._getColor(); - ctx.lineWidth = this._getLineWidth(); - - if (this.from != this.to) { - // draw line - var via = this._line(ctx); + ItemSet.prototype._updateItem = function(item, itemData) { + var oldGroupId = item.data.group; - // draw label - var point; - if (this.label) { - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { - point = this._pointOnLine(0.5); - } - this._label(ctx, this.label, point.x, point.y); - } + item.data = itemData; + if (item.displayed) { + item.redraw(); } - else { - var x, y; - var radius = this.physics.springLength / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - node.height / 2; - } - this._circle(ctx, x, y, radius); - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + + // update group + if (oldGroupId != item.data.group) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); + + var groupId = this.groupsData ? item.data.group : UNGROUPED; + var group = this.groups[groupId]; + if (group) group.add(item); } }; /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width + * 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 */ - Edge.prototype._getLineWidth = function() { - if (this.selected == true) { - return Math.min(this.widthSelected, this.options.widthMax)*this.networkScaleInv; - } - else { - if (this.hover == true) { - return Math.min(this.options.hoverWidth, this.options.widthMax)*this.networkScaleInv; - } - else { - return this.options.width*this.networkScaleInv; - } - } - }; + ItemSet.prototype._removeItem = function(item) { + // remove from DOM + item.hide(); - Edge.prototype._getViaCoordinates = function () { - var xVia = null; - var yVia = null; - var factor = this.options.smoothCurves.roundness; - var type = this.options.smoothCurves.type; + // remove from items + delete this.items[item.id]; - var dx = Math.abs(this.from.x - this.to.x); - var dy = Math.abs(this.from.y - this.to.y); - if (type == 'discrete' || type == 'diagonalCross') { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - } - } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - } - } - if (type == "discrete") { - xVia = dx < factor * dy ? this.from.x : xVia; - } - } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - } - } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - } - else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - } - } - if (type == "discrete") { - yVia = dy < factor * dx ? this.from.y : yVia; - } + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); + + // remove from group + var groupId = this.groupsData ? item.data.group : UNGROUPED; + var group = this.groups[groupId]; + if (group) group.remove(item); + }; + + /** + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} + * @private + */ + ItemSet.prototype._constructByEndArray = function(array) { + var endArray = []; + + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof ItemRange) { + endArray.push(array[i]); } } - else if (type == "straightCross") { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1-factor) * dy; + return endArray; + }; + + /** + * Register the clicked item on touch, before dragStart is initiated. + * + * dragStart is initiated from a mousemove event, which can have left the item + * already resulting in an item == null + * + * @param {Event} event + * @private + */ + ItemSet.prototype._onTouch = function (event) { + // store the touched item, used in _onDragStart + this.touchParams.item = ItemSet.itemFromTarget(event); + }; + + /** + * Start dragging the selected events + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragStart = function (event) { + if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { + return; + } + + var item = this.touchParams.item || null, + me = this, + props; + + if (item && item.selected) { + var dragLeftItem = event.target.dragLeftItem; + var dragRightItem = event.target.dragRightItem; + + if (dragLeftItem) { + props = { + item: dragLeftItem + }; + + if (me.options.editable.updateTime) { + props.start = item.data.start.valueOf(); } - else { - yVia = this.to.y + (1-factor) * dy; + if (me.options.editable.updateGroup) { + if ('group' in item.data) props.group = item.data.group; } + + this.touchParams.itemProps = [props]; } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right - if (this.from.x < this.to.x) { - xVia = this.to.x - (1-factor) * dx; + else if (dragRightItem) { + props = { + item: dragRightItem + }; + + if (me.options.editable.updateTime) { + props.end = item.data.end.valueOf(); } - else { - xVia = this.to.x + (1-factor) * dx; + if (me.options.editable.updateGroup) { + if ('group' in item.data) props.group = item.data.group; } - yVia = this.from.y; - } - } - else if (type == 'horizontal') { - if (this.from.x < this.to.x) { - xVia = this.to.x - (1-factor) * dx; - } - else { - xVia = this.to.x + (1-factor) * dx; - } - yVia = this.from.y; - } - else if (type == 'vertical') { - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1-factor) * dy; + + this.touchParams.itemProps = [props]; } else { - yVia = this.to.y + (1-factor) * dy; - } - } - else { // continuous - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - // console.log(1) - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; - } - else if (this.from.x > this.to.x) { - // console.log(2) - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - xVia = this.to.x > xVia ? this.to.x :xVia; - } - } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - // console.log(3) - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x < xVia ? this.to.x : xVia; + this.touchParams.itemProps = this.getSelection().map(function (id) { + var item = me.items[id]; + var props = { + item: item + }; + + if (me.options.editable.updateTime) { + if ('start' in item.data) props.start = item.data.start.valueOf(); + if ('end' in item.data) props.end = item.data.end.valueOf(); } - else if (this.from.x > this.to.x) { - // console.log(4, this.from.x, this.to.x) - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x > xVia ? this.to.x : xVia; + if (me.options.editable.updateGroup) { + if ('group' in item.data) props.group = item.data.group; } - } + + return props; + }); } - else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - // console.log(5) - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } - else if (this.from.x > this.to.x) { - // console.log(6) - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - yVia = this.to.y > yVia ? this.to.y : yVia; - } + + event.stopPropagation(); + } + }; + + /** + * Drag selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.itemProps) { + var range = this.body.range, + snap = this.body.util.snap || null, + deltaX = event.gesture.deltaX, + scale = (this.props.width / (range.end - range.start)), + offset = deltaX / scale; + + // move + this.touchParams.itemProps.forEach(function (props) { + if ('start' in props) { + var start = new Date(props.start + offset); + props.item.data.start = snap ? snap(start) : start; } - else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - // console.log(7) - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } - else if (this.from.x > this.to.x) { - // console.log(8) - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } + + if ('end' in props) { + var end = new Date(props.end + offset); + props.item.data.end = snap ? snap(end) : end; } - } + + if ('group' in props) { + // drag from one group to another + var group = ItemSet.groupFromTarget(event); + _moveToGroup(props.item, group); + } + }); + + // TODO: implement onMoving handler + + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); + + event.stopPropagation(); + } + }; + + /** + * Move an item to another group + * @param {Item} item + * @param {Group} group + * @private + */ + function _moveToGroup (item, group) { + if (group && group.groupId != item.data.group) { + var oldGroup = item.parent; + oldGroup.remove(item); + oldGroup.order(); + group.add(item); + group.order(); + + item.data.group = group.groupId; } + } + + /** + * End of dragging selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + // prepare a change set for the changed items + var changes = [], + me = this, + dataset = this.itemsData.getDataSet(); + + var itemProps = this.touchParams.itemProps ; + this.touchParams.itemProps = null; + itemProps.forEach(function (props) { + var id = props.item.id, + itemData = me.itemsData.get(id, me.itemOptions); + var changed = false; + if ('start' in props.item.data) { + changed = (props.start != props.item.data.start.valueOf()); + itemData.start = util.convert(props.item.data.start, + dataset._options.type && dataset._options.type.start || 'Date'); + } + if ('end' in props.item.data) { + changed = changed || (props.end != props.item.data.end.valueOf()); + itemData.end = util.convert(props.item.data.end, + dataset._options.type && dataset._options.type.end || 'Date'); + } + if ('group' in props.item.data) { + changed = changed || (props.group != props.item.data.group); + itemData.group = props.item.data.group; + } - return {x:xVia, y:yVia}; - } + // only apply changes when start or end is actually changed + if (changed) { + 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 + if ('start' in props) props.item.data.start = props.start; + if ('end' in props) props.item.data.end = props.end; + if ('group' in props && props.item.data.group != props.group) { + var group = me.groups[props.group]; + _moveToGroup(props.item, group); + } - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._line = function (ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - if (this.options.smoothCurves.enabled == true) { - if (this.options.smoothCurves.dynamic == false) { - var via = this._getViaCoordinates(); - if (via.x == null) { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; - } - else { - // this.via.x = via.x; - // this.via.y = via.y; - ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y); - ctx.stroke(); - return via; + me.stackDirty = true; // force re-stacking of all items next redraw + me.body.emitter.emit('change'); + } + }); } + }); + + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); } - else { - ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y); - ctx.stroke(); - return this.via; - } - } - else { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; + + event.stopPropagation(); } }; /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius + * Handle selecting/deselecting an item when tapping it + * @param {Event} event * @private */ - Edge.prototype._circle = function (ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); - }; + ItemSet.prototype._onSelectItem = function (event) { + if (!this.options.selectable) return; - /** - * Draw label with white background and with the middle at (x, y) - * @param {CanvasRenderingContext2D} ctx - * @param {String} text - * @param {Number} x - * @param {Number} y - * @private - */ - Edge.prototype._label = function (ctx, text, x, y) { - if (text) { - // TODO: cache the calculated size - ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") + - this.options.fontSize + "px " + this.options.fontFace; - ctx.fillStyle = this.options.fontFill; - var width = ctx.measureText(text).width; - var height = this.options.fontSize; - var left = x - width / 2; - var top = y - height / 2; + var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey; + var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey; + if (ctrlKey || shiftKey) { + this._onMultiSelectItem(event); + return; + } - ctx.fillRect(left, top, width, height); + var oldSelection = this.getSelection(); - // draw text - ctx.fillStyle = this.options.fontColor || "black"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - ctx.fillText(text, left, top); + var item = ItemSet.itemFromTarget(event); + var selection = item ? [item.id] : []; + this.setSelection(selection); + + var newSelection = this.getSelection(); + + // 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: this.getSelection() + }); } + + event.stopPropagation(); }; /** - * Redraw a edge as a dashed line - * Draw this edge in the given canvas - * @author David Jordan - * @date 2012-08-08 - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * Handle creation and updates of an item on double tap + * @param event * @private */ - Edge.prototype._drawDashLine = function(ctx) { - // set style - if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight;} - else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover;} - else {ctx.strokeStyle = this.options.color.color;} + ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; - ctx.lineWidth = this._getLineWidth(); + var me = this, + snap = this.body.util.snap || null, + item = ItemSet.itemFromTarget(event); - var via = null; - // only firefox and chrome support this method, else we use the legacy one. - if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) { - // configure the dash pattern - var pattern = [0]; - if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) { - pattern = [this.options.dash.length,this.options.dash.gap]; - } - else { - pattern = [5,5]; - } + if (item) { + // update item - // set dash settings for chrome or firefox - if (typeof ctx.setLineDash !== 'undefined') { //Chrome - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; + // execute async handler to update the item (or cancel it) + var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset + this.options.onUpdate(itemData, function (itemData) { + if (itemData) { + me.itemsData.update(itemData); + } + }); + } + else { + // add item + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.gesture.center.pageX - xAbs; + var start = this.body.util.toTime(x); + var newItem = { + start: snap ? snap(start) : start, + content: 'new item' + }; - } else { //Firefox - ctx.mozDash = pattern; - ctx.mozDashOffset = 0; + // 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) : end; } - // draw the line - via = this._line(ctx); - - // restore the dash settings. - if (typeof ctx.setLineDash !== 'undefined') { //Chrome - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; + newItem[this.itemsData.fieldId] = util.randomUUID(); - } else { //Firefox - ctx.mozDash = [0]; - ctx.mozDashOffset = 0; - } - } - else { // unsupporting smooth lines - // draw dashed line - ctx.beginPath(); - ctx.lineCap = 'round'; - if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]); - } - else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, - [this.options.dash.length,this.options.dash.gap]); - } - else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); + var group = ItemSet.groupFromTarget(event); + if (group) { + newItem.group = group.groupId; } - ctx.stroke(); + + // execute async handler to customize (or cancel) adding an item + this.options.onAdd(newItem, function (item) { + if (item) { + me.itemsData.add(newItem); + // TODO: need to trigger a redraw? + } + }); } + }; - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; + /** + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private + */ + ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; + + var selection, + item = ItemSet.itemFromTarget(event); + + if (item) { + // multi select items + selection = this.getSelection(); // current selection + var index = selection.indexOf(item.id); + if (index == -1) { + // item is not yet selected -> select it + selection.push(item.id); } else { - point = this._pointOnLine(0.5); + // item is already selected -> deselect it + selection.splice(index, 1); } - this._label(ctx, this.label, point.x, point.y); + this.setSelection(selection); + + this.body.emitter.emit('select', { + items: this.getSelection() + }); + + event.stopPropagation(); } }; /** - * Get a point on a line - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private + * 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 */ - Edge.prototype._pointOnLine = function (percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y + ItemSet.itemFromTarget = function(event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; } + + return null; }; /** - * 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 + * 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 */ - Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { - var angle = (percentage - 3/8) * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) + ItemSet.groupFromTarget = function(event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-group')) { + return target['timeline-group']; + } + target = target.parentNode; } + + return null; }; /** - * Redraw a edge as a line with an arrow halfway the line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private + * 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 */ - Edge.prototype._drawArrowCenter = function(ctx) { - var point; - // set style - if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight; ctx.fillStyle = this.options.color.highlight;} - else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover; ctx.fillStyle = this.options.color.hover;} - else {ctx.strokeStyle = this.options.color.color; ctx.fillStyle = this.options.color.color;} - ctx.lineWidth = this._getLineWidth(); + ItemSet.itemSetFromTarget = function(event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; + } + target = target.parentNode; + } - if (this.from != this.to) { - // draw line - var via = this._line(ctx); + return null; + }; - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - // draw an arrow halfway the line - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { - point = this._pointOnLine(0.5); - } + module.exports = ItemSet; - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); - // draw label - if (this.label) { - this._label(ctx, this.label, point.x, point.y); - } - } - else { - // draw circle - var x, y; - var radius = 0.25 * Math.max(100,this.physics.springLength); - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - node.height * 0.5; - } - this._circle(ctx, x, y, radius); +/***/ }, +/* 30 */ +/***/ function(module, exports, __webpack_require__) { - // draw all arrows - var angle = 0.2 * Math.PI; - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - point = this._pointOnCircle(x, y, radius, 0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); + var util = __webpack_require__(1); + var stack = __webpack_require__(31); + var ItemRange = __webpack_require__(32); - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + /** + * @constructor Group + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet + */ + function Group (groupId, data, itemSet) { + this.groupId = groupId; + + this.itemSet = itemSet; + + this.dom = {}; + this.props = { + label: { + width: 0, + height: 0 } - } - }; + }; + this.className = null; + + this.items = {}; // items filtered by groupId of this group + this.visibleItems = []; // items currently visible in window + this.orderedItems = { // items sorted by start and by end + byStart: [], + byEnd: [] + }; + this._create(); + this.setData(data); + } /** - * Redraw a edge as a line with an arrow - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * Create DOM elements for the group * @private */ - Edge.prototype._drawArrow = function(ctx) { - // set style - if (this.selected == true) {ctx.strokeStyle = this.options.color.highlight; ctx.fillStyle = this.options.color.highlight;} - else if (this.hover == true) {ctx.strokeStyle = this.options.color.hover; ctx.fillStyle = this.options.color.hover;} - else {ctx.strokeStyle = this.options.color.color; ctx.fillStyle = this.options.color.color;} - - ctx.lineWidth = this._getLineWidth(); + Group.prototype._create = function() { + var label = document.createElement('div'); + label.className = 'vlabel'; + this.dom.label = label; - var angle, length; - //draw a line - if (this.from != this.to) { - angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var inner = document.createElement('div'); + inner.className = 'inner'; + label.appendChild(inner); + this.dom.inner = inner; - var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); - var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; - var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; - var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; + var foreground = document.createElement('div'); + foreground.className = 'group'; + foreground['timeline-group'] = this; + this.dom.foreground = foreground; - var via; - if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) { - via = this.via; - } - else if (this.options.smoothCurves.enabled == true) { - via = this._getViaCoordinates(); - } + this.dom.background = document.createElement('div'); + this.dom.background.className = 'group'; - if (this.options.smoothCurves.enabled == true && via.x != null) { - angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x)); - dx = (this.to.x - via.x); - dy = (this.to.y - via.y); - edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - } - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + this.dom.axis = document.createElement('div'); + this.dom.axis.className = 'group'; - var xTo,yTo; - if (this.options.smoothCurves.enabled == true && via.x != null) { - xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y; - } - else { - xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; - } + // 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); + }; - ctx.beginPath(); - ctx.moveTo(xFrom,yFrom); - if (this.options.smoothCurves.enabled == true && via.x != null) { - ctx.quadraticCurveTo(via.x,via.y,xTo, yTo); - } - else { - ctx.lineTo(xTo, yTo); - } - ctx.stroke(); + /** + * 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 arrow at the end of the line - length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(xTo, yTo, angle, length); - ctx.fill(); - ctx.stroke(); + // update title + this.dom.label.title = data && data.title || ''; - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x)); - var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y)); - point = {x:midpointX, y:midpointY}; - } - else { - point = this._pointOnLine(0.5); - } - this._label(ctx, this.label, point.x, point.y); - } + if (!this.dom.inner.firstChild) { + util.addClassName(this.dom.inner, 'hidden'); } else { - // draw circle - var node = this.from; - var x, y, arrow; - var radius = 0.25 * Math.max(100,this.physics.springLength); - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - y = node.y - radius; - arrow = { - x: x, - y: node.y, - angle: 0.9 * Math.PI - }; - } - else { - x = node.x + radius; - y = node.y - node.height * 0.5; - arrow = { - x: node.x, - y: y, - angle: 0.6 * Math.PI - }; - } - ctx.beginPath(); - // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); - - // draw all arrows - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrow.x, arrow.y, arrow.angle, length); - ctx.fill(); - ctx.stroke(); + util.removeClassName(this.dom.inner, 'hidden'); + } - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + // update className + var className = data && data.className || null; + if (className != this.className) { + if (this.className) { + util.removeClassName(this.dom.label, className); + util.removeClassName(this.dom.foreground, className); + util.removeClassName(this.dom.background, className); + util.removeClassName(this.dom.axis, 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); } }; + /** + * Get the width of the group label + * @return {number} width + */ + Group.prototype.getLabelWidth = function() { + return this.props.label.width; + }; /** - * Calculate the distance between a point (x3,y3) and a line segment from - * (x1,y1) to (x2,y2). - * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment - * @param {number} x1 - * @param {number} y1 - * @param {number} x2 - * @param {number} y2 - * @param {number} x3 - * @param {number} y3 - * @private + * 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 */ - Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point - if (this.from != this.to) { - if (this.options.smoothCurves.enabled == true) { - var xVia, yVia; - if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { - xVia = this.via.x; - yVia = this.via.y; - } - else { - var via = this._getViaCoordinates(); - xVia = via.x; - yVia = via.y; - } - var minDistance = 1e9; - var distance; - var i,t,x,y, lastX, lastY; - for (i = 0; i < 10; i++) { - t = 0.1*i; - x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2; - y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2; - if (i > 0) { - distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3); - minDistance = distance < minDistance ? distance : minDistance; - } - lastX = x; lastY = y; - } - return minDistance - } - else { - return this._getDistanceToLine(x1,y1,x2,y2,x3,y3); + Group.prototype.redraw = function(range, margin, restack) { + var resized = false; + + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + + // force recalculation of the height of the items when the marker height changed + // (due to the Timeline being attached to the DOM or changed from display:none to visible) + var markerHeight = this.dom.marker.clientHeight; + if (markerHeight != this.lastMarkerHeight) { + this.lastMarkerHeight = markerHeight; + + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); + + restack = true; + } + + // reposition visible items vertically + if (this.itemSet.options.stack) { // TODO: ugly way to access options... + stack.stack(this.visibleItems, margin, restack); + } + else { // no stacking + stack.nostack(this.visibleItems, margin); + } + + // recalculate the height of the group + var height; + var visibleItems = this.visibleItems; + if (visibleItems.length) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, (item.top + item.height)); + }); + 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 { - var x, y, dx, dy; - var radius = 0.25 * this.physics.springLength; - var node = this.from; - if (node.width > node.height) { - x = node.x + 0.5 * node.width; - y = node.y - radius; - } - else { - x = node.x + radius; - y = node.y - 0.5 * node.height; - } - dx = x - x3; - dy = y - y3; - return Math.abs(Math.sqrt(dx*dx + dy*dy) - radius); + height = margin.axis + margin.item.vertical; } - }; + height = Math.max(height, this.props.label.height); - Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) { - var px = x2-x1, - py = y2-y1, - something = px*px + py*py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; + // 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; - if (u > 1) { - u = 1; - } - else if (u < 0) { - u = 0; - } + // 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 x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; + // apply new height + this.dom.background.style.height = height + 'px'; + this.dom.foreground.style.height = height + 'px'; + this.dom.label.style.height = height + 'px'; - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance + // 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(); + } - return Math.sqrt(dx*dx + dy*dy); - } + return resized; + }; /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale + * Show this group: attach to the DOM */ - Edge.prototype.setScale = function(scale) { - this.networkScaleInv = 1.0/scale; + Group.prototype.show = function() { + if (!this.dom.label.parentNode) { + this.itemSet.dom.labelSet.appendChild(this.dom.label); + } + + if (!this.dom.foreground.parentNode) { + this.itemSet.dom.foreground.appendChild(this.dom.foreground); + } + + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); + } + + if (!this.dom.axis.parentNode) { + this.itemSet.dom.axis.appendChild(this.dom.axis); + } }; + /** + * Hide this group: remove from the DOM + */ + Group.prototype.hide = function() { + var label = this.dom.label; + if (label.parentNode) { + label.parentNode.removeChild(label); + } - Edge.prototype.select = function() { - this.selected = true; - }; + var foreground = this.dom.foreground; + if (foreground.parentNode) { + foreground.parentNode.removeChild(foreground); + } - Edge.prototype.unselect = function() { - this.selected = false; + var background = this.dom.background; + if (background.parentNode) { + background.parentNode.removeChild(background); + } + + var axis = this.dom.axis; + if (axis.parentNode) { + axis.parentNode.removeChild(axis); + } }; - Edge.prototype.positionBezierNode = function() { - if (this.via !== null && this.from !== null && this.to !== null) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); + /** + * Add an item to the group + * @param {Item} item + */ + Group.prototype.add = function(item) { + this.items[item.id] = item; + item.setParent(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); } }; /** - * This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true. - * @param ctx + * Remove an item from the group + * @param {Item} item */ - Edge.prototype._drawControlNodes = function(ctx) { - if (this.controlNodesEnabled == true) { - if (this.controlNodes.from === null && this.controlNodes.to === null) { - var nodeIdFrom = "edgeIdFrom:".concat(this.id); - var nodeIdTo = "edgeIdTo:".concat(this.id); - var constants = { - nodes:{group:'', radius:8}, - physics:{damping:0}, - clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}} - }; - this.controlNodes.from = new Node( - {id:nodeIdFrom, - shape:'dot', - color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}} - },{},{},constants); - this.controlNodes.to = new Node( - {id:nodeIdTo, - shape:'dot', - color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}} - },{},{},constants); - } + Group.prototype.remove = function(item) { + delete this.items[item.id]; + item.setParent(this.itemSet); - if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) { - this.controlNodes.positions = this.getControlNodePositions(ctx); - this.controlNodes.from.x = this.controlNodes.positions.from.x; - this.controlNodes.from.y = this.controlNodes.positions.from.y; - this.controlNodes.to.x = this.controlNodes.positions.to.x; - this.controlNodes.to.y = this.controlNodes.positions.to.y; - } + // remove from visible items + var index = this.visibleItems.indexOf(item); + if (index != -1) this.visibleItems.splice(index, 1); - this.controlNodes.from.draw(ctx); - this.controlNodes.to.draw(ctx); - } - else { - this.controlNodes = {from:null, to:null, positions:{}}; - } + // TODO: also remove from ordered items? }; /** - * Enable control nodes. - * @private + * Remove an item from the corresponding DataSet + * @param {Item} item */ - Edge.prototype._enableControlNodes = function() { - this.controlNodesEnabled = true; + Group.prototype.removeFromDataSet = function(item) { + this.itemSet.removeItem(item.id); }; /** - * disable control nodes + * Reorder the items + */ + Group.prototype.order = function() { + var array = util.toArray(this.items); + this.orderedItems.byStart = array; + this.orderedItems.byEnd = this._constructByEndArray(array); + + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); + }; + + /** + * Create an array containing all items being a range (having an end date) + * @param {Item[]} array + * @returns {ItemRange[]} * @private */ - Edge.prototype._disableControlNodes = function() { - this.controlNodesEnabled = false; + Group.prototype._constructByEndArray = function(array) { + var endArray = []; + + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof ItemRange) { + endArray.push(array[i]); + } + } + return endArray; }; /** - * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. - * @param x - * @param y - * @returns {null} + * 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 */ - Edge.prototype._getSelectedControlNode = function(x,y) { - var positions = this.controlNodes.positions; - var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2)); - var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2)); + Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) { + var initialPosByStart, + newVisibleItems = [], + i; - if (fromDistance < 15) { - this.connectedNode = this.from; - this.from = this.controlNodes.from; - return this.controlNodes.from; + // first check if the items that were in view previously are still in view. + // this handles the case for the ItemRange that is both before and after the current one. + if (visibleItems.length > 0) { + for (i = 0; i < visibleItems.length; i++) { + this._checkIfVisible(visibleItems[i], newVisibleItems, range); + } } - else if (toDistance < 15) { - this.connectedNode = this.to; - this.to = this.controlNodes.to; - return this.controlNodes.to; + + // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime) + if (newVisibleItems.length == 0) { + initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start'); } else { - return null; + initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); } - }; + // use visible search to find a visible ItemRange (only based on endTime) + var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end'); - /** - * this resets the control nodes to their original position. - * @private - */ - Edge.prototype._restoreControlNodes = function() { - if (this.controlNodes.from.selected == true) { - this.from = this.connectedNode; - this.connectedNode = null; - this.controlNodes.from.unselect(); + // if we found a initial ID to use, trace it up and down until we meet an invisible item. + if (initialPosByStart != -1) { + for (i = initialPosByStart; i >= 0; i--) { + if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} + } + for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) { + if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;} + } } - if (this.controlNodes.to.selected == true) { - this.to = this.connectedNode; - this.connectedNode = null; - this.controlNodes.to.unselect(); + + // if we found a initial ID to use, trace it up and down until we meet an invisible item. + if (initialPosByEnd != -1) { + for (i = initialPosByEnd; i >= 0; i--) { + if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} + } + for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) { + if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;} + } } + + return newVisibleItems; }; + + /** - * this calculates the position of the control nodes on the edges of the parent nodes. + * this function checks if an item is invisible. If it is NOT we make it visible + * and add it to the global visible items. If it is, return true. * - * @param ctx - * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} + * @param {Item} item + * @param {Item[]} visibleItems + * @param {{start:number, end:number}} range + * @returns {boolean} + * @private */ - Edge.prototype.getControlNodePositions = function(ctx) { - var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x)); - var dx = (this.to.x - this.from.x); - var dy = (this.to.y - this.from.y); - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); - var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; - var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x; - var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y; - - var via; - if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) { - via = this.via; - } - else if (this.options.smoothCurves.enabled == true) { - via = this._getViaCoordinates(); + Group.prototype._checkIfInvisible = function(item, visibleItems, range) { + if (item.isVisible(range)) { + if (!item.displayed) item.show(); + item.repositionX(); + if (visibleItems.indexOf(item) == -1) { + visibleItems.push(item); + } + return false; } - - if (this.options.smoothCurves.enabled == true && via.x != null) { - angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x)); - dx = (this.to.x - via.x); - dy = (this.to.y - via.y); - edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + else { + if (item.displayed) item.hide(); + return true; } - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + }; - var xTo,yTo; - if (this.options.smoothCurves.enabled == true && via.x != null) { - xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y; + /** + * 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 { - xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; + if (item.displayed) item.hide(); } - - return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}}; }; - module.exports = Edge; + module.exports = Group; + /***/ }, -/* 34 */ +/* 31 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); + // Utility functions for ordering and stacking of items + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors /** - * @class Groups - * This class can store groups and properties specific for groups. + * Order items by their start data + * @param {Item[]} items */ - function Groups() { - this.clear(); - this.defaultIndex = 0; - } - + exports.orderByStart = function(items) { + items.sort(function (a, b) { + return a.data.start - b.data.start; + }); + }; /** - * default constants for group colors + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} items */ - Groups.DEFAULT = [ - {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue - {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow - {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red - {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green - {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta - {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple - {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange - {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue - {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink - {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint - ]; + 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; + return aTime - bTime; + }); + }; /** - * Clear all groups + * 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 */ - Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function() - { - var i = 0; - for ( var p in this ) { - if (this.hasOwnProperty(p)) { - i++; - } + exports.stack = function(items, margin, force) { + var i, iMax; + + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; } - return i; } - }; - - /** - * get group properties of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group properties - */ - Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; - } + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + if (item.top === null) { + // initialize top position + item.top = margin.axis; - return group; - }; + 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 && exports.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } - /** - * Add a custom group style - * @param {String} groupname - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object - */ - Groups.prototype.add = function (groupname, style) { - this.groups[groupname] = style; - if (style.color) { - style.color = util.parseColor(style.color); + 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); + } } - return style; }; - module.exports = Groups; - - -/***/ }, -/* 35 */ -/***/ function(module, exports, __webpack_require__) { - /** - * @class Images - * This class loads images and keeps them stored. + * 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. */ - function Images() { - this.images = {}; - - this.callback = undefined; - } + exports.nostack = function(items, margin) { + var i, iMax; - /** - * Set an onload callback function. This will be called each time an image - * is loaded - * @param {function} callback - */ - Images.prototype.setOnloadCallback = function(callback) { - this.callback = callback; + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = margin.axis; + } }; /** - * - * @param {string} url Url of the image - * @return {Image} img The image object + * 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 */ - Images.prototype.load = function(url) { - var img = this.images[url]; - if (img == undefined) { - // create the image - var images = this; - img = new Image(); - this.images[url] = img; - img.onload = function() { - if (images.callback) { - images.callback(this); - } - }; - img.src = url; - } - - return img; + 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); }; - module.exports = Images; - /***/ }, -/* 36 */ +/* 32 */ /***/ function(module, exports, __webpack_require__) { - var util = __webpack_require__(1); + var Hammer = __webpack_require__(18); + var Item = __webpack_require__(33); /** - * @class Node - * A node. A node can be connected to other nodes via one or multiple edges. - * @param {object} properties An object containing properties for the node. All - * properties are optional, except for the id. - * {number} id Id of the node. Required - * {string} label Text label for the node - * {number} x Horizontal position of the node - * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square" - * {string} image An image url - * {string} title An title text, can be HTML - * {anytype} group A group name or number - * @param {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 properties - * @param {Object} constants An object with default values for - * example for the color - * + * @constructor ItemRange + * @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 Node(properties, imagelist, grouplist, networkConstants) { - var constants = util.selectiveBridgeObject(['nodes'],networkConstants); - this.options = constants.nodes; + function ItemRange (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); + } + } - this.selected = false; - this.hover = false; + Item.call(this, data, conversion, options); + } - this.edges = []; // all edges connected to this node - this.dynamicEdges = []; - this.reroutedEdges = {}; + ItemRange.prototype = new Item (null, null, null); - this.fontDrawThreshold = 3; + ItemRange.prototype.baseClassName = 'item range'; - // set defaults for the properties - this.id = undefined; - this.x = null; - this.y = null; - this.xFixed = false; - this.yFixed = false; - this.horizontalAlignLeft = true; // these are for the navigation controls - this.verticalAlignTop = true; // these are for the navigation controls - this.baseRadiusValue = networkConstants.nodes.radius; - this.radiusFixed = false; - this.level = -1; - this.preassignedLevel = 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 + */ + ItemRange.prototype.isVisible = function(range) { + // determine visibility + return (this.data.start < range.end) && (this.data.end > range.start); + }; + /** + * Repaint the item + */ + ItemRange.prototype.redraw = function() { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - this.imagelist = imagelist; - this.grouplist = grouplist; + // background box + dom.box = document.createElement('div'); + // className is updated in redraw() - // physics properties - this.fx = 0.0; // external force x - this.fy = 0.0; // external force y - this.vx = 0.0; // velocity x - this.vy = 0.0; // velocity y - this.damping = networkConstants.physics.damping; // written every time gravity is calculated - this.fixedData = {x:null,y:null}; + // contents box + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + // attach this item as attribute + dom.box['timeline-item'] = this; + } - this.setProperties(properties, constants); + // 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 time axis: parent has no foreground container element'); + } + foreground.appendChild(dom.box); + } + this.displayed = true; - // creating the variables for clustering - this.resetCluster(); - this.dynamicEdgesLength = 0; - this.clusterSession = 0; - this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width; - this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height; - this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius; - this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements; - this.growthIndicator = 0; + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } - // variables to tell the node about the network. - this.networkScaleInv = 1; - this.networkScale = 1; - this.canvasTopLeft = {"x": -300, "y": -300}; - this.canvasBottomRight = {"x": 300, "y": 300}; - this.parentEdgeId = null; - } + this.dirty = true; + } - /** - * (re)setting the clustering variables and objects - */ - Node.prototype.resetCluster = function() { - // clustering variables - this.formationScale = undefined; // this is used to determine when to open the cluster - this.clusterSize = 1; // this signifies the total amount of nodes in this cluster - this.containedNodes = {}; - this.containedEdges = {}; - this.clusterSessions = []; - }; + // update title + if (this.data.title != this.title) { + dom.box.title = this.data.title; + this.title = this.data.title; + } - /** - * Attach a edge to the node - * @param {Edge} edge - */ - Node.prototype.attachEdge = function(edge) { - if (this.edges.indexOf(edge) == -1) { - this.edges.push(edge); + // update class + var className = (this.data.className ? (' ' + this.data.className) : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.box.className = this.baseClassName + className; + + this.dirty = true; } - if (this.dynamicEdges.indexOf(edge) == -1) { - this.dynamicEdges.push(edge); + + // recalculate size + if (this.dirty) { + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden'; + + this.props.content.width = this.dom.content.offsetWidth; + this.height = this.dom.box.offsetHeight; + + this.dirty = false; } - this.dynamicEdgesLength = this.dynamicEdges.length; + + this._repaintDeleteButton(dom.box); + this._repaintDragLeft(); + this._repaintDragRight(); }; /** - * Detach a edge from the node - * @param {Edge} edge + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - Node.prototype.detachEdge = function(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); - this.dynamicEdges.splice(index, 1); + ItemRange.prototype.show = function() { + if (!this.displayed) { + this.redraw(); } - this.dynamicEdgesLength = this.dynamicEdges.length; }; - /** - * Set or overwrite properties for the node - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties + * Hide the item from the DOM (when visible) + * @return {Boolean} changed */ - Node.prototype.setProperties = function(properties, constants) { - if (!properties) { - return; - } - - var fields = ['borderWidth','borderWidthSelected','shape','image','radius','fontColor', - 'fontSize','fontFace','group','mass' - ]; - util.selectiveDeepExtend(fields, this.options, properties); + ItemRange.prototype.hide = function() { + if (this.displayed) { + var box = this.dom.box; - this.originalLabel = undefined; - // basic properties - if (properties.id !== undefined) {this.id = properties.id;} - if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;} - if (properties.title !== undefined) {this.title = properties.title;} - if (properties.x !== undefined) {this.x = properties.x;} - if (properties.y !== undefined) {this.y = properties.y;} - if (properties.value !== undefined) {this.value = properties.value;} - if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;} + if (box.parentNode) { + box.parentNode.removeChild(box); + } - // navigation controls properties - if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} - if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} - if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;} + this.top = null; + this.left = null; - if (this.id === undefined) { - throw "Node must have an id"; + this.displayed = false; } + }; - // copy group properties - if (typeof this.options.group === 'number' || (typeof this.options.group === 'string' && this.options.group != '')) { - var groupObj = this.grouplist.get(this.options.group); - for (var prop in groupObj) { - if (groupObj.hasOwnProperty(prop)) { - this.options[prop] = groupObj[prop]; - } - } - } + /** + * Reposition the item horizontally + * @Override + */ + ItemRange.prototype.repositionX = function() { + var props = this.props, + parentWidth = this.parent.width, + start = this.conversion.toScreen(this.data.start), + end = this.conversion.toScreen(this.data.end), + padding = this.options.padding, + contentLeft; + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } + var boxWidth = Math.max(end - start, 1); - // individual shape properties - if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;} - if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);} + if (this.overflow) { + // when range exceeds left of the window, position the contents at the left of the visible area + contentLeft = Math.max(-start, 0); - if (this.options.image!== undefined && this.options.image!= "") { - if (this.imagelist) { - this.imageObj = this.imagelist.load(this.options.image); + this.left = start; + this.width = boxWidth + this.props.content.width; + // Note: The calculation of width is an optimistic calculation, giving + // a width which will not change when moving the Timeline + // So no restacking needed, which is nicer for the eye; + } + else { // no overflow + // when range exceeds left of the window, position the contents at the left of the visible area + if (start < 0) { + contentLeft = Math.min(-start, + (end - start - props.content.width - 2 * padding)); + // TODO: remove the need for options.padding. it's terrible. } else { - throw "No imagelist provided"; + contentLeft = 0; } - } - - this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX); - this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY); - this.radiusFixed = this.radiusFixed || (properties.radius !== undefined); - if (this.options.shape == 'image') { - this.options.radiusMin = constants.nodes.widthMin; - this.options.radiusMax = constants.nodes.widthMax; + this.left = start; + this.width = boxWidth; } - - // choose draw method depending on the shape - switch (this.options.shape) { - case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break; - case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break; - case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break; - case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; - // TODO: add diamond shape - case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break; - case 'text': this.draw = this._drawText; this.resize = this._resizeText; break; - case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break; - case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break; - case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break; - case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break; - case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; - default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; - } - // reset the size of the node, this can be changed - this._reset(); + this.dom.box.style.left = this.left + 'px'; + this.dom.box.style.width = boxWidth + 'px'; + this.dom.content.style.left = contentLeft + 'px'; }; /** - * select this node + * Reposition the item vertically + * @Override */ - Node.prototype.select = function() { - this.selected = true; - this._reset(); - }; + ItemRange.prototype.repositionY = function() { + var orientation = this.options.orientation, + box = this.dom.box; - /** - * unselect this node - */ - Node.prototype.unselect = function() { - this.selected = false; - this._reset(); + if (orientation == 'top') { + box.style.top = this.top + 'px'; + } + else { + box.style.top = (this.parent.height - this.top - this.height) + 'px'; + } }; - /** - * Reset the calculated size of the node, forces it to recalculate its size + * Repaint a drag area on the left side of the range when the range is selected + * @protected */ - Node.prototype.clearSizeCache = function() { - this._reset(); - }; + ItemRange.prototype._repaintDragLeft = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { + // create and show drag area + var dragLeft = document.createElement('div'); + dragLeft.className = 'drag-left'; + dragLeft.dragLeftItem = this; - /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private - */ - Node.prototype._reset = function() { - this.width = undefined; - this.height = undefined; - }; + // TODO: this should be redundant? + Hammer(dragLeft, { + preventDefault: true + }).on('drag', function () { + //console.log('drag left') + }); - /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. - */ - Node.prototype.getTitle = function() { - return typeof this.title === "function" ? this.title() : this.title; + 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; + } }; /** - * Calculate the distance to the border of the Node - * @param {CanvasRenderingContext2D} ctx - * @param {Number} angle Angle in radians - * @returns {number} distance Distance to the border in pixels + * Repaint a drag area on the right side of the range when the range is selected + * @protected */ - Node.prototype.distanceToBorder = function (ctx, angle) { - var borderWidth = 1; + ItemRange.prototype._repaintDragRight = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { + // create and show drag area + var dragRight = document.createElement('div'); + dragRight.className = 'drag-right'; + dragRight.dragRightItem = this; - if (!this.width) { - this.resize(ctx); - } + // TODO: this should be redundant? + Hammer(dragRight, { + preventDefault: true + }).on('drag', function () { + //console.log('drag right') + }); - switch (this.options.shape) { - case 'circle': - case 'dot': - return this.options.radius+ borderWidth; + 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; + } + }; - case 'ellipse': - var a = this.width / 2; - var b = this.height / 2; - var w = (Math.sin(angle) * a); - var h = (Math.cos(angle) * b); - return a * b / Math.sqrt(w * w + h * h); + module.exports = ItemRange; - // TODO: implement distanceToBorder for database - // TODO: implement distanceToBorder for triangle - // TODO: implement distanceToBorder for triangleDown - case 'box': - case 'image': - case 'text': - default: - if (this.width) { - return Math.min( - Math.abs(this.width / 2 / Math.cos(angle)), - Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - // TODO: reckon with border radius too in case of box - } - else { - return 0; - } +/***/ }, +/* 33 */ +/***/ function(module, exports, __webpack_require__) { - } - // TODO: implement calculation of distance to border for all shapes - }; + var Hammer = __webpack_require__(18); /** - * Set forces acting on the node - * @param {number} fx Force in horizontal direction - * @param {number} fy Force in vertical direction + * @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 */ - Node.prototype._setForce = function(fx, fy) { - this.fx = fx; - this.fy = fy; - }; + 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; + + this.top = null; + this.left = null; + this.width = null; + this.height = null; + } /** - * Add forces acting on the node - * @param {number} fx Force in horizontal direction - * @param {number} fy Force in vertical direction - * @private + * Select current item */ - Node.prototype._addForce = function(fx, fy) { - this.fx += fx; - this.fy += fy; + Item.prototype.select = function() { + this.selected = true; + if (this.displayed) this.redraw(); }; /** - * Perform one discrete step for the node - * @param {number} interval Time interval in seconds + * Unselect current item */ - Node.prototype.discreteStep = function(interval) { - if (!this.xFixed) { - var dx = this.damping * this.vx; // damping force - var ax = (this.fx - dx) / this.options.mass; // acceleration - this.vx += ax * interval; // velocity - this.x += this.vx * interval; // position - } - - if (!this.yFixed) { - var dy = this.damping * this.vy; // damping force - var ay = (this.fy - dy) / this.options.mass; // acceleration - this.vy += ay * interval; // velocity - this.y += this.vy * interval; // position - } + Item.prototype.unselect = function() { + this.selected = false; + if (this.displayed) this.redraw(); }; - - /** - * Perform one discrete step for the node - * @param {number} interval Time interval in seconds - * @param {number} maxVelocity The speed limit imposed on the velocity + * Set a parent for the item + * @param {ItemSet | Group} parent */ - Node.prototype.discreteStepLimited = function(interval, maxVelocity) { - if (!this.xFixed) { - var dx = this.damping * this.vx; // damping force - var ax = (this.fx - dx) / this.options.mass; // acceleration - this.vx += ax * interval; // velocity - this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx; - this.x += this.vx * interval; // position - } - else { - this.fx = 0; - } - - if (!this.yFixed) { - var dy = this.damping * this.vy; // damping force - var ay = (this.fy - dy) / this.options.mass; // acceleration - this.vy += ay * interval; // velocity - this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy; - this.y += this.vy * interval; // position + Item.prototype.setParent = function(parent) { + if (this.displayed) { + this.hide(); + this.parent = parent; + if (this.parent) { + this.show(); + } } else { - this.fy = 0; + this.parent = parent; } }; /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not + * 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 */ - Node.prototype.isFixed = function() { - return (this.xFixed && this.yFixed); + Item.prototype.isVisible = function(range) { + // Should be implemented by Item implementations + return false; }; /** - * Check if this node is moving - * @param {number} vmin the minimum velocity considered as "moving" - * @return {boolean} true if moving, false if it has no velocity + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed */ - Node.prototype.isMoving = function(vmin) { - var velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2)); - // this.velocity = Math.sqrt(Math.pow(this.vx,2) + Math.pow(this.vy,2)) - return (velocity > vmin); + Item.prototype.show = function() { + return false; }; /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed */ - Node.prototype.isSelected = function() { - return this.selected; + Item.prototype.hide = function() { + return false; }; /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value + * Repaint the item */ - Node.prototype.getValue = function() { - return this.value; + Item.prototype.redraw = function() { + // should be implemented by the item }; /** - * Calculate the distance from the nodes location to the given location (x,y) - * @param {Number} x - * @param {Number} y - * @return {Number} value + * Reposition the Item horizontally */ - Node.prototype.getDistance = function(x, y) { - var dx = this.x - x, - dy = this.y - y; - return Math.sqrt(dx * dx + dy * dy); + Item.prototype.repositionX = function() { + // should be implemented by the item }; + /** + * Reposition the Item vertically + */ + Item.prototype.repositionY = function() { + // should be implemented by the item + }; /** - * Adjust the value range of the node. The node will adjust it's radius - * based on its value. - * @param {Number} min - * @param {Number} max + * Repaint a delete button on the top right of the item when the item is selected + * @param {HTMLElement} anchor + * @protected */ - Node.prototype.setValueRange = function(min, max) { - if (!this.radiusFixed && this.value !== undefined) { - if (max == min) { - this.options.radius= (this.options.radiusMin + this.options.radiusMax) / 2; - } - else { - var scale = (this.options.radiusMax - this.options.radiusMin) / (max - min); - this.options.radius= (this.value - min) * scale + this.options.radiusMin; + Item.prototype._repaintDeleteButton = function (anchor) { + if (this.selected && this.options.editable.remove && !this.dom.deleteButton) { + // create and show button + var me = this; + + var deleteButton = document.createElement('div'); + deleteButton.className = 'delete'; + deleteButton.title = 'Delete this item'; + + Hammer(deleteButton, { + preventDefault: true + }).on('tap', function (event) { + me.parent.removeFromDataSet(me); + event.stopPropagation(); + }); + + anchor.appendChild(deleteButton); + this.dom.deleteButton = deleteButton; + } + else if (!this.selected && this.dom.deleteButton) { + // remove button + if (this.dom.deleteButton.parentNode) { + this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); } + this.dom.deleteButton = null; } - this.baseRadiusValue = this.options.radius; }; + module.exports = Item; + + +/***/ }, +/* 34 */ +/***/ function(module, exports, __webpack_require__) { + + var Item = __webpack_require__(33); + /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * @constructor ItemBox + * @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 */ - Node.prototype.draw = function(ctx) { - throw "Draw method not initialized for node"; - }; + function ItemBox (data, conversion, options) { + this.props = { + dot: { + width: 0, + height: 0 + }, + line: { + width: 0, + height: 0 + } + }; + + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); + } + } + + Item.call(this, data, conversion, options); + } + + ItemBox.prototype = new Item (null, null, null); /** - * 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 + * 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 */ - Node.prototype.resize = function(ctx) { - throw "Resize method not initialized for node"; + ItemBox.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); }; /** - * 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 + * Repaint the item */ - Node.prototype.isOverlappingWith = function(obj) { - return (this.left < obj.right && - this.left + this.width > obj.left && - this.top < obj.bottom && - this.top + this.height > obj.top); - }; + ItemBox.prototype.redraw = function() { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - Node.prototype._resizeImage = function (ctx) { - // TODO: pre calculate the image size + // create main box + dom.box = document.createElement('DIV'); - if (!this.width || !this.height) { // undefined or 0 - var width, height; - if (this.value) { - this.options.radius= this.baseRadiusValue; - var scale = this.imageObj.height / this.imageObj.width; - if (scale !== undefined) { - width = this.options.radius|| this.imageObj.width; - height = this.options.radius* scale || this.imageObj.height; - } - else { - width = 0; - height = 0; - } + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; + + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; + + // attach this item as attribute + dom.box['timeline-item'] = this; + } + + // append DOM to parent DOM + if (!this.parent) { + throw new Error('Cannot redraw item: no parent attached'); + } + if (!dom.box.parentNode) { + var foreground = this.parent.dom.foreground; + if (!foreground) throw new Error('Cannot redraw time axis: 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 time axis: 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 time axis: parent has no axis container element'); + axis.appendChild(dom.dot); + } + this.displayed = true; + + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; } else { - width = this.imageObj.width; - height = this.imageObj.height; + throw new Error('Property "content" missing in item ' + this.data.id); } - this.width = width; - this.height = height; - this.growthIndicator = 0; - if (this.width > 0 && this.height > 0) { - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - width; - } + this.dirty = true; + } + + // update title + if (this.data.title != this.title) { + dom.box.title = this.data.title; + this.title = this.data.title; + } + + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.box.className = 'item box' + className; + dom.line.className = 'item line' + className; + dom.dot.className = 'item dot' + className; + + this.dirty = true; + } + + // recalculate size + if (this.dirty) { + this.props.dot.height = dom.dot.offsetHeight; + this.props.dot.width = dom.dot.offsetWidth; + this.props.line.width = dom.line.offsetWidth; + this.width = dom.box.offsetWidth; + this.height = dom.box.offsetHeight; + + this.dirty = false; } + this._repaintDeleteButton(dom.box); }; - Node.prototype._drawImage = function (ctx) { - this._resizeImage(ctx); + /** + * Show the item in the DOM (when not already displayed). The items DOM will + * be created when needed. + */ + ItemBox.prototype.show = function() { + if (!this.displayed) { + this.redraw(); + } + }; - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + /** + * Hide the item from the DOM (when visible) + */ + ItemBox.prototype.hide = function() { + if (this.displayed) { + var dom = this.dom; - var yLabel; - if (this.imageObj.width != 0 ) { - // draw the shade - if (this.clusterSize > 1) { - var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0); - lineWidth *= this.networkScaleInv; - lineWidth = Math.min(0.2 * this.width,lineWidth); + 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); - ctx.globalAlpha = 0.5; - ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth); - } + this.top = null; + this.left = null; - // draw the image - ctx.globalAlpha = 1.0; - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); - yLabel = this.y + this.height / 2; + this.displayed = false; + } + }; + + /** + * Reposition the item horizontally + * @Override + */ + ItemBox.prototype.repositionX = function() { + var start = this.conversion.toScreen(this.data.start), + align = this.options.align, + left, + box = this.dom.box, + line = this.dom.line, + dot = this.dom.dot; + + // calculate left position of the box + if (align == 'right') { + this.left = start - this.width; + } + else if (align == 'left') { + this.left = start; } else { - // image still loading... just draw the label for now - yLabel = this.y; + // default or 'center' + this.left = start - this.width / 2; } - this._label(ctx, this.label, this.x, yLabel, undefined, "top"); + // reposition box + box.style.left = this.left + 'px'; + + // reposition line + line.style.left = (start - this.props.line.width / 2) + 'px'; + + // reposition dot + dot.style.left = (start - this.props.dot.width / 2) + 'px'; }; + /** + * Reposition the item vertically + * @Override + */ + ItemBox.prototype.repositionY = function() { + var orientation = this.options.orientation, + box = this.dom.box, + line = this.dom.line, + dot = this.dom.dot; - Node.prototype._resizeBox = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + if (orientation == 'top') { + box.style.top = (this.top || 0) + 'px'; - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.growthIndicator = this.width - (textSize.width + 2 * margin); - // this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; + 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'; }; - Node.prototype._drawBox = function (ctx) { - this._resizeBox(ctx); + module.exports = ItemBox; - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; +/***/ }, +/* 35 */ +/***/ function(module, exports, __webpack_require__) { - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + var Item = __webpack_require__(33); - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + /** + * @constructor ItemPoint + * @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 ItemPoint (data, conversion, options) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; - ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius); - ctx.stroke(); + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); + } } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.options.color.background; + Item.call(this, data, conversion, options); + } - ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius); - ctx.fill(); - ctx.stroke(); + ItemPoint.prototype = new Item (null, null, null); - this._label(ctx, this.label, this.x, this.y); + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + ItemPoint.prototype.isVisible = function(range) { + // determine visibility + // TODO: account for the real width of the item. Right now we just add 1/4 to the window + var interval = (range.end - range.start) / 4; + return (this.data.start > range.start - interval) && (this.data.start < range.end + interval); }; + /** + * Repaint the item + */ + ItemPoint.prototype.redraw = function() { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - Node.prototype._resizeDatabase = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; + // background box + dom.point = document.createElement('div'); + // className is updated in redraw() + + // contents box, right from the dot + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.point.appendChild(dom.content); + + // dot at start + dom.dot = document.createElement('div'); + dom.point.appendChild(dom.dot); + + // attach this item as attribute + dom.point['timeline-item'] = this; + } + + // 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 time axis: parent has no foreground container element'); + } + foreground.appendChild(dom.point); + } + this.displayed = true; + + // update contents + if (this.data.content != this.content) { + this.content = this.data.content; + if (this.content instanceof Element) { + dom.content.innerHTML = ''; + dom.content.appendChild(this.content); + } + else if (this.data.content != undefined) { + dom.content.innerHTML = this.content; + } + else { + throw new Error('Property "content" missing in item ' + this.data.id); + } - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - size; + this.dirty = true; } - }; - - Node.prototype._drawDatabase = function (ctx) { - this._resizeDatabase(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + // update title + if (this.data.title != this.title) { + dom.point.title = this.data.title; + this.title = this.data.title; + } - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + // update class + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); + if (this.className != className) { + this.className = className; + dom.point.className = 'item point' + className; + dom.dot.className = 'item dot' + className; - ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth); - ctx.stroke(); + this.dirty = true; } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); - ctx.fill(); - ctx.stroke(); + // recalculate size + if (this.dirty) { + this.width = dom.point.offsetWidth; + this.height = dom.point.offsetHeight; + this.props.dot.width = dom.dot.offsetWidth; + this.props.dot.height = dom.dot.offsetHeight; + this.props.content.height = dom.content.offsetHeight; - this._label(ctx, this.label, this.x, this.y); - }; + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; + dom.dot.style.left = (this.props.dot.width / 2) + 'px'; - Node.prototype._resizeCircle = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.options.radius = diameter / 2; + this.dirty = false; + } - this.width = diameter; - this.height = diameter; + this._repaintDeleteButton(dom.point); + }; - // scaling used for clustering - // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor; - // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.options.radius- 0.5*diameter; + /** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + */ + ItemPoint.prototype.show = function() { + if (!this.displayed) { + this.redraw(); } }; - Node.prototype._drawCircle = function (ctx) { - this._resizeCircle(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + /** + * Hide the item from the DOM (when visible) + */ + ItemPoint.prototype.hide = function() { + if (this.displayed) { + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); + } - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + this.top = null; + this.left = null; - ctx.circle(this.x, this.y, this.options.radius+2*ctx.lineWidth); - ctx.stroke(); + this.displayed = false; } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + }; - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(this.x, this.y, this.options.radius); - ctx.fill(); - ctx.stroke(); + /** + * Reposition the item horizontally + * @Override + */ + ItemPoint.prototype.repositionX = function() { + var start = this.conversion.toScreen(this.data.start); - this._label(ctx, this.label, this.x, this.y); - }; + this.left = start - this.props.dot.width; - Node.prototype._resizeEllipse = function (ctx) { - if (!this.width) { - var textSize = this.getTextSize(ctx); + // reposition point + this.dom.point.style.left = this.left + 'px'; + }; - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } - var defaultSize = this.width; + /** + * Reposition the item vertically + * @Override + */ + ItemPoint.prototype.repositionY = function() { + var orientation = this.options.orientation, + point = this.dom.point; - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - defaultSize; + if (orientation == 'top') { + point.style.top = this.top + 'px'; + } + else { + point.style.top = (this.parent.height - this.top - this.height) + 'px'; } }; - Node.prototype._drawEllipse = function (ctx) { - this._resizeEllipse(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + module.exports = ItemPoint; - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; +/***/ }, +/* 36 */ +/***/ function(module, exports, __webpack_require__) { - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + var Emitter = __webpack_require__(10); + var Hammer = __webpack_require__(18); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(8); + var Range = __webpack_require__(20); + var Core = __webpack_require__(23); + var TimeAxis = __webpack_require__(24); + var CurrentTime = __webpack_require__(26); + var CustomTime = __webpack_require__(28); + var LineGraph = __webpack_require__(37); - ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth); - ctx.stroke(); + /** + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Graph2d.setOptions for the available options. + * @constructor + */ + function Graph2d (container, items, options, groups) { + for (var coreProp in Core.prototype) { + if (Core.prototype.hasOwnProperty(coreProp) && !Graph2d.prototype.hasOwnProperty(coreProp)) { + Graph2d.prototype[coreProp] = Core.prototype[coreProp]; + } } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + var me = this; + this.defaultOptions = { + start: null, + end: null, - ctx.ellipse(this.left, this.top, this.width, this.height); - ctx.fill(); - ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); - }; + autoResize: true, - Node.prototype._drawDot = function (ctx) { - this._drawShape(ctx, 'circle'); - }; + orientation: 'bottom', + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - Node.prototype._drawTriangle = function (ctx) { - this._drawShape(ctx, 'triangle'); - }; + // Create the DOM, props, and emitter + this._create(container); - Node.prototype._drawTriangleDown = function (ctx) { - this._drawShape(ctx, 'triangleDown'); - }; + // all components listed here will be repainted automatically + this.components = []; - Node.prototype._drawSquare = function (ctx) { - this._drawShape(ctx, 'square'); - }; + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + util: { + snap: null, // will be specified after TimeAxis is created + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime : me._toGlobalTime.bind(me) + } + }; - Node.prototype._drawStar = function (ctx) { - this._drawShape(ctx, 'star'); - }; + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - Node.prototype._resizeShape = function (ctx) { - if (!this.width) { - this.options.radius= this.baseRadiusValue; - var size = 2 * this.options.radius; - this.width = size; - this.height = size; + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - size; - } - }; + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - Node.prototype._drawShape = function (ctx, shape) { - this._resizeShape(ctx); + // custom time bar + // Note: time bar will be attached in this.setOptions when selected + this.customTime = new CustomTime(this.body); + this.components.push(this.customTime); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + // item set + this.linegraph = new LineGraph(this.body); + this.components.push(this.linegraph); - var clusterLineWidth = 2.5; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - var radiusMultiplier = 2; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - // choose draw method depending on the shape - switch (shape) { - case 'dot': radiusMultiplier = 2; break; - case 'square': radiusMultiplier = 2; break; - case 'triangle': radiusMultiplier = 3; break; - case 'triangleDown': radiusMultiplier = 3; break; - case 'star': radiusMultiplier = 4; break; + // apply options + if (options) { + this.setOptions(options); } - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - // draw the outer border - if (this.clusterSize > 1) { - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } - ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth); - ctx.stroke(); + // create itemset + if (items) { + this.setItems(items); } - ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width,ctx.lineWidth); + else { + this.redraw(); + } + } - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](this.x, this.y, this.options.radius); - ctx.fill(); - ctx.stroke(); + /** + * Set options. Options will be passed to all components loaded in the Graph2d. + * @param {Object} [options] + * {String} orientation + * Vertical orientation for the Graph2d, + * 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 Graph2d, a number in pixels or + * a css string like '400px' or '75%'. If undefined, + * The Graph2d will automatically size such that + * its contents fit. + * {String | Number} minHeight + * Minimum height for the Graph2d, a number in pixels or + * a css string like '400px' or '75%'. + * {String | Number} maxHeight + * Maximum height for the Graph2d, 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 + */ + Graph2d.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; + util.selectiveExtend(fields, this.options, options); - if (this.label) { - this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top',true); + // enable/disable autoResize + this._initAutoResize(); } - }; - Node.prototype._resizeText = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + // propagate options to all components + this.components.forEach(function (component) { + component.setOptions(options); + }); - // scaling used for clustering - this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor; - this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor; - this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor; - this.growthIndicator = this.width - (textSize.width + 2 * margin); + // TODO: remove deprecation error one day (deprecated since version 0.8.0) + if (options && options.order) { + throw new Error('Option order is deprecated. There is no replacement for this feature.'); } - }; - Node.prototype._drawText = function (ctx) { - this._resizeText(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - this._label(ctx, this.label, this.x, this.y); + // redraw everything + this.redraw(); }; - Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) { - if (text && Number(this.options.fontSize) * this.networkScale > this.fontDrawThreshold) { - ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace; - ctx.fillStyle = this.options.fontColor || "black"; - ctx.textAlign = align || "center"; - ctx.textBaseline = baseline || "middle"; - - var lines = text.split('\n'); - var lineCount = lines.length; - var fontSize = (Number(this.options.fontSize) + 4); - var yLine = y + (1 - lineCount) / 2 * fontSize; - if (labelUnderNode == true) { - yLine = y + (1 - lineCount) / (2 * fontSize); - } + /** + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + */ + Graph2d.prototype.setItems = function(items) { + var initialLoad = (this.itemsData == null); - for (var i = 0; i < lineCount; i++) { - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } + // 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' + } + }); } - }; - - Node.prototype.getTextSize = function(ctx) { - if (this.label !== undefined) { - ctx.font = (this.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace; + // set items + this.itemsData = newDataSet; + this.linegraph && this.linegraph.setItems(newDataSet); - var lines = this.label.split('\n'), - height = (Number(this.options.fontSize) + 4) * lines.length, - width = 0; + if (initialLoad && ('start' in this.options || 'end' in this.options)) { + this.fit(); - for (var i = 0, iMax = lines.length; i < iMax; i++) { - width = Math.max(width, ctx.measureText(lines[i]).width); - } + var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null; + var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null; - return {"width": width, "height": height}; - } - else { - return {"width": 0, "height": 0}; + this.setWindow(start, end); } }; /** - * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. - * there is a safety margin of 0.3 * width; - * - * @returns {boolean} + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - Node.prototype.inArea = function() { - if (this.width !== undefined) { - return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x && - this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x && - this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y && - this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y); + 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 { - return true; + // turn an array into a dataset + newDataSet = new DataSet(groups); } + + this.groupsData = newDataSet; + this.linegraph.setGroups(newDataSet); }; /** - * checks if the core of the node is in the display area, this is used for opening clusters around zoom - * @returns {boolean} + * 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 */ - Node.prototype.inView = function() { - return (this.x >= this.canvasTopLeft.x && - this.x < this.canvasBottomRight.x && - this.y >= this.canvasTopLeft.y && - this.y < this.canvasBottomRight.y); - }; + 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; + } + } /** - * This allows the zoom level of the network to influence the rendering - * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas - * - * @param scale - * @param canvasTopLeft - * @param canvasBottomRight + * This checks if the visible option of the supplied group (by ID) is true or false. + * @param groupId + * @returns {*} */ - Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { - this.networkScaleInv = 1.0/scale; - this.networkScale = scale; - this.canvasTopLeft = canvasTopLeft; - this.canvasBottomRight = canvasBottomRight; - }; + Graph2d.prototype.isGroupVisible = function(groupId) { + if (this.linegraph.groups[groupId] !== undefined) { + return this.linegraph.groups[groupId].visible; + } + else { + return false; + } + } /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale + * 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 */ - Node.prototype.setScale = function(scale) { - this.networkScaleInv = 1.0/scale; - this.networkScale = scale; - }; - + Graph2d.prototype.getItemRange = function() { + var min = null; + var max = null; + // calculate min from start filed + for (var groupId in this.linegraph.groups) { + if (this.linegraph.groups.hasOwnProperty(groupId)) { + if (this.linegraph.groups[groupId].visible == true) { + for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) { +