From 77be45d5f1427c20ac8fe299c0b2af51fc39cc75 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 26 Mar 2015 14:23:47 +0100 Subject: [PATCH] all of the manipulation has been moved to 4.0, redone and nicely commented and cleaned up :) --- dist/vis.js | 55737 ++++++++++---------- examples/network/01_basic_usage.html | 12 +- lib/network/locales.js | 6 +- lib/network/modules/ManipulationSystem.js | 1017 +- 4 files changed, 28440 insertions(+), 28332 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 94c6b829..f16e39c0 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -5,7 +5,7 @@ * A dynamic, browser-based visualization library. * * @version 4.0.0-SNAPSHOT - * @date 2015-03-25 + * @date 2015-03-26 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -85,63 +85,63 @@ return /******/ (function(modules) { // webpackBootstrap // utils exports.util = __webpack_require__(1); - exports.DOMutil = __webpack_require__(6); + exports.DOMutil = __webpack_require__(2); // data - exports.DataSet = __webpack_require__(7); - exports.DataView = __webpack_require__(9); - exports.Queue = __webpack_require__(8); + exports.DataSet = __webpack_require__(3); + exports.DataView = __webpack_require__(4); + exports.Queue = __webpack_require__(5); // Graph3d - exports.Graph3d = __webpack_require__(10); + exports.Graph3d = __webpack_require__(6); exports.graph3d = { - Camera: __webpack_require__(14), - Filter: __webpack_require__(15), - Point2d: __webpack_require__(13), - Point3d: __webpack_require__(12), - Slider: __webpack_require__(16), - StepNumber: __webpack_require__(17) + Camera: __webpack_require__(7), + Filter: __webpack_require__(8), + Point2d: __webpack_require__(9), + Point3d: __webpack_require__(10), + Slider: __webpack_require__(11), + StepNumber: __webpack_require__(12) }; // Timeline - exports.Timeline = __webpack_require__(18); - exports.Graph2d = __webpack_require__(44); + exports.Timeline = __webpack_require__(13); + exports.Graph2d = __webpack_require__(14); exports.timeline = { - DateUtil: __webpack_require__(26), - DataStep: __webpack_require__(47), - Range: __webpack_require__(23), - stack: __webpack_require__(31), - TimeStep: __webpack_require__(29), + DateUtil: __webpack_require__(15), + DataStep: __webpack_require__(16), + Range: __webpack_require__(17), + stack: __webpack_require__(18), + TimeStep: __webpack_require__(19), components: { items: { - Item: __webpack_require__(33), - BackgroundItem: __webpack_require__(37), - BoxItem: __webpack_require__(35), - PointItem: __webpack_require__(36), - RangeItem: __webpack_require__(32) + Item: __webpack_require__(20), + BackgroundItem: __webpack_require__(21), + BoxItem: __webpack_require__(22), + PointItem: __webpack_require__(23), + RangeItem: __webpack_require__(24) }, Component: __webpack_require__(25), - CurrentTime: __webpack_require__(43), - CustomTime: __webpack_require__(40), - DataAxis: __webpack_require__(46), - GraphGroup: __webpack_require__(48), + CurrentTime: __webpack_require__(26), + CustomTime: __webpack_require__(27), + DataAxis: __webpack_require__(28), + GraphGroup: __webpack_require__(29), Group: __webpack_require__(30), - BackgroundGroup: __webpack_require__(34), - ItemSet: __webpack_require__(28), - Legend: __webpack_require__(52), - LineGraph: __webpack_require__(45), - TimeAxis: __webpack_require__(42) + BackgroundGroup: __webpack_require__(31), + ItemSet: __webpack_require__(32), + Legend: __webpack_require__(33), + LineGraph: __webpack_require__(34), + TimeAxis: __webpack_require__(35) } }; // Network - exports.Network = __webpack_require__(53); + exports.Network = __webpack_require__(36); exports.network = { - Images: __webpack_require__(57), - dotparser: __webpack_require__(55), - gephiParser: __webpack_require__(56) + Images: __webpack_require__(37), + dotparser: __webpack_require__(38), + gephiParser: __webpack_require__(39) }; // Deprecated since v3.0.0 @@ -150,9 +150,9 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(2); - exports.hammer = __webpack_require__(19); // TODO: deprecate exports.hammer some day - exports.Hammer = __webpack_require__(19); + exports.moment = __webpack_require__(40); + exports.hammer = __webpack_require__(41); // TODO: deprecate exports.hammer some day + exports.Hammer = __webpack_require__(41); /***/ }, /* 1 */ @@ -164,7 +164,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__(2); + var moment = __webpack_require__(40); /** * Test whether given object is a number @@ -1427,6828 +1427,6716 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - // 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); + // DOM utility methods + + /** + * 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 = []; + } + } + } + }; + + /** + * 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; + }; + + + /** + * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer + * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. + * + * @param elementType + * @param JSONcontainer + * @param DOMContainer + * @returns {*} + * @private + */ + exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { + var element; + // allocate DOM element, if it doesnt yet exist, create one. + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before + // check if there is an redundant element + if (JSONcontainer[elementType].redundant.length > 0) { + element = JSONcontainer[elementType].redundant[0]; + JSONcontainer[elementType].redundant.shift(); + } else { + // create a new element and add it to the SVG + element = document.createElement(elementType); + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); + } else { + DOMContainer.appendChild(element); + } + } + } else { + // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. + element = document.createElement(elementType); + JSONcontainer[elementType] = { used: [], redundant: [] }; + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); + } else { + DOMContainer.appendChild(element); + } + } + JSONcontainer[elementType].used.push(element); + return element; + }; + + + + + /** + * 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 + * @param labelObj + * @returns {*} + */ + exports.drawPoint = function (x, y, group, JSONcontainer, svgContainer, labelObj) { + 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); + } 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); + } + + if (group.options.drawPoints.styles !== undefined) { + point.setAttributeNS(null, "style", group.group.options.drawPoints.styles); + } + point.setAttributeNS(null, "class", group.className + " point"); + //handle label + var label = exports.getSVGElement("text", JSONcontainer, svgContainer); + if (labelObj) { + if (labelObj.xOffset) { + x = x + labelObj.xOffset; + } + + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; + } + + if (labelObj.className) { + label.setAttributeNS(null, "class", labelObj.className + " label"); + } + + } + label.setAttributeNS(null, "x", x); + label.setAttributeNS(null, "y", y); + return point; + }; + + /** + * 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) { + if (height < 0) { + height *= -1; + y -= height; + } + var rect = exports.getSVGElement("rect", JSONcontainer, svgContainer); + rect.setAttributeNS(null, "x", x - 0.5 * width); + rect.setAttributeNS(null, "y", y); + rect.setAttributeNS(null, "width", width); + rect.setAttributeNS(null, "height", height); + rect.setAttributeNS(null, "class", className); + } + }; /***/ }, /* 3 */ /***/ function(module, exports, __webpack_require__) { - var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {//! moment.js - //! version : 2.9.0 - //! authors : Tim Wood, Iskren Chernev, Moment.js contributors - //! license : MIT - //! momentjs.com + "use strict"; - (function (undefined) { - /************************************ - Constants - ************************************/ + var util = __webpack_require__(1); + var Queue = __webpack_require__(5); - var moment, - VERSION = '2.9.0', - // the global-scope this is NOT the global object in Node.js - globalScope = (typeof global !== 'undefined' && (typeof window === 'undefined' || window === global.window)) ? global : this, - oldGlobalMoment, - round = Math.round, - hasOwnProperty = Object.prototype.hasOwnProperty, - i, + /** + * 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, + /** + * Trigger an event + * @param {String} event + * @param {Object | null} params + * @param {String} [senderId] Optional id of the sender. + * @private + */ + DataSet.prototype._trigger = function (event, params, senderId) { + if (event == "*") { + throw new Error("Cannot trigger event *"); + } - // 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 - }, + var subscribers = []; + if (event in this._subscribers) { + subscribers = subscribers.concat(this._subscribers[event]); + } + if ("*" in this._subscribers) { + subscribers = subscribers.concat(this._subscribers["*"]); + } - 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' - }, + for (var i = 0; i < subscribers.length; i++) { + var subscriber = subscribers[i]; + if (subscriber.callback) { + subscriber.callback(event, params, senderId || null); + } + } + }; - camelFunctions = { - dayofyear : 'dayOfYear', - isoweekday : 'isoWeekday', - isoweek : 'isoWeek', - weekyear : 'weekYear', - isoweekyear : 'isoWeekYear' - }, + /** + * Add data. + * Adding an item will fail when there already is an item with the same id. + * @param {Object | Array | DataTable} data + * @param {String} [senderId] Optional sender id + * @return {Array} addedIds Array with the ids of the added items + */ + DataSet.prototype.add = function (data, senderId) { + var addedIds = [], + id, + me = this; - // format function strings - formatFunctions = {}, + 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); + } - // default relative time thresholds - relativeTimeThresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }, - - // tokens to ordinalize and pad - ordinalizeTokens = 'DDD w W M D d'.split(' '), - paddedTokens = 'M D H h m s w W'.split(' '), - - formatTokenFunctions = { - M : function () { - return this.month() + 1; - }, - MMM : function (format) { - return this.localeData().monthsShort(this, format); - }, - MMMM : function (format) { - return this.localeData().months(this, format); - }, - D : function () { - return this.date(); - }, - DDD : function () { - return this.dayOfYear(); - }, - d : function () { - return this.day(); - }, - dd : function (format) { - return this.localeData().weekdaysMin(this, format); - }, - ddd : function (format) { - return this.localeData().weekdaysShort(this, format); - }, - dddd : function (format) { - return this.localeData().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.localeData().meridiem(this.hours(), this.minutes(), true); - }, - A : function () { - return this.localeData().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.utcOffset(), - b = '+'; - if (a < 0) { - a = -a; - b = '-'; - } - return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2); - }, - ZZ : function () { - var a = this.utcOffset(), - 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.valueOf(); - }, - X : function () { - return this.unix(); - }, - Q : function () { - return this.quarter(); - } - }, - - deprecations = {}, - - lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'], + 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"); + } - updateInProgress = false; + if (addedIds.length) { + this._trigger("add", { items: addedIds }, senderId); + } - // 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'); - } - } + return addedIds; + }; - function hasOwnProp(a, b) { - return hasOwnProperty.call(a, b); - } + /** + * 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 = []; + var updatedIds = []; + var updatedData = []; + var me = this; + var fieldId = me._fieldId; - 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 - }; + var addOrUpdate = function (item) { + var id = item[fieldId]; + if (me._data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); + updatedData.push(item); + } else { + // add new item + id = me._addItem(item); + addedIds.push(id); } + }; - function printMsg(msg) { - if (moment.suppressDeprecationWarnings === false && - typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); - } + 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); + } - function deprecate(msg, fn) { - var firstTime = true; - return extend(function () { - if (firstTime) { - printMsg(msg); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); + addOrUpdate(item); } + } else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } else { + throw new Error("Unknown dataType"); + } - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - printMsg(msg); - deprecations[name] = true; - } - } + if (addedIds.length) { + this._trigger("add", { items: addedIds }, senderId); + } + if (updatedIds.length) { + this._trigger("update", { items: updatedIds, data: updatedData }, senderId); + } - function padToken(func, count) { - return function (a) { - return leftZeroFill(func.call(this, a), count); - }; - } - function ordinalizeToken(func, period) { - return function (a) { - return this.localeData().ordinal(func.call(this, a), period); - }; - } + return addedIds.concat(updatedIds); + }; - function monthDiff(a, b) { - // difference in months - var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), - // b is in (anchor - 1 month, anchor + 1 month) - anchor = a.clone().add(wholeMonthDiff, 'months'), - anchor2, adjust; + /** + * 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; - if (b - anchor < 0) { - anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor - anchor2); - } else { - anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor2 - anchor); - } + // 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]; + } - return -(wholeMonthDiff + adjust); - } + // determine the return type + var returnType; + if (options && options.returnType) { + var allowedValues = ["DataTable", "Array", "Object"]; + returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; - while (ordinalizeTokens.length) { - i = ordinalizeTokens.pop(); - formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); + 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 + ")"); } - while (paddedTokens.length) { - i = paddedTokens.pop(); - formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + if (returnType == "DataTable" && !util.isDataTable(data)) { + throw new Error("Parameter \"data\" must be a DataTable " + "when options.type is \"DataTable\""); } - formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); - + } else if (data) { + returnType = util.getType(data) == "DataTable" ? "DataTable" : "Array"; + } else { + returnType = "Array"; + } - function meridiemFixWrap(locale, hour, meridiem) { - var isPm; + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], + item, + itemId, + i, + len; - if (meridiem == null) { - // nothing to do - return hour; - } - if (locale.meridiemHour != null) { - return locale.meridiemHour(hour, meridiem); - } else if (locale.isPM != null) { - // Fallback - isPm = locale.isPM(meridiem); - if (isPm && hour < 12) { - hour += 12; - } - if (!isPm && hour === 12) { - hour = 0; - } - return hour; - } else { - // thie is not supposed to happen - return hour; + // 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); } + } } + } - /************************************ - Constructors - ************************************/ + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } - function Locale() { + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; + if (id != undefined) { + item = this._filterFields(item, fields); + } else { + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } } + } - // Moment prototype object - function Moment(config, skipOverflow) { - if (skipOverflow !== false) { - checkOverflow(config); - } - copyConfig(this, config); - this._d = new Date(+config._d); - // Prevent infinite loop in case updateOffset creates new moment - // objects. - if (updateInProgress === false) { - updateInProgress = true; - moment.updateOffset(this); - updateInProgress = false; + // 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; + } } + } + }; - // 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; + /** + * 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 = []; - this._data = {}; + 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._locale = moment.localeData(); + this._sort(items, order); - this._bubble(); + 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]); + } + } + } } - - /************************************ - Helpers - ************************************/ - - - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } + } else { + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); } + } - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } + this._sort(items, order); - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; + 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 a; + } } + } - function copyConfig(to, from) { - var i, prop, val; + return ids; + }; - if (typeof from._isAMomentObject !== 'undefined') { - to._isAMomentObject = from._isAMomentObject; - } - if (typeof from._i !== 'undefined') { - to._i = from._i; - } - if (typeof from._f !== 'undefined') { - to._f = from._f; - } - if (typeof from._l !== 'undefined') { - to._l = from._l; - } - if (typeof from._strict !== 'undefined') { - to._strict = from._strict; - } - if (typeof from._tzm !== 'undefined') { - to._tzm = from._tzm; - } - if (typeof from._isUTC !== 'undefined') { - to._isUTC = from._isUTC; - } - if (typeof from._offset !== 'undefined') { - to._offset = from._offset; - } - if (typeof from._pf !== 'undefined') { - to._pf = from._pf; - } - if (typeof from._locale !== 'undefined') { - to._locale = from._locale; - } + /** + * 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; + }; - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } - } + /** + * 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; - return to; - } + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); - function absRound(number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); + 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); } + } } + } + }; - // 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; + /** + * 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; - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; + // 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)); + } } + } - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; - - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; - } + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); + return mappedItems; + }; - return res; - } + /** + * Filter the fields of an item + * @param {Object | null} item + * @param {String[]} fields Field names + * @return {Object | null} filteredItem or null if no item is provided + * @private + */ + DataSet.prototype._filterFields = function (item, fields) { + if (!item) { + // item is null + return item; + } - function momentsDifference(base, other) { - var res; - other = makeAs(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } + var filteredItem = {}; - return res; + for (var field in item) { + if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { + filteredItem[field] = item[field]; } + } - // TODO: remove 'name' arg after deprecation is removed - function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); - tmp = val; val = period; period = tmp; - } + return filteredItem; + }; - val = typeof val === 'string' ? +val : val; - dur = moment.duration(val, period); - addOrSubtractDurationFromMoment(this, dur, direction); - return this; - }; - } + /** + * 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"); + } + }; - function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; + /** + * 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 (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); - } - if (days) { - rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding); - } - if (months) { - rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding); - } - if (updateOffset) { - moment.updateOffset(mom, days || months); - } + if (Array.isArray(id)) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } } - - // check if is an array - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; + } else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); } + } - function isDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || - input instanceof Date; - } + if (removedIds.length) { + this._trigger("remove", { items: removedIds }, senderId); + } - // 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; - } + return removedIds; + }; - function normalizeUnits(units) { - if (units) { - var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); - units = unitAliases[units] || camelFunctions[lowered] || lowered; - } - return units; + /** + * Remove an item by its id + * @param {Number | String | Object} id id or item + * @returns {Number | String | null} id + * @private + */ + DataSet.prototype._remove = function (id) { + if (util.isNumber(id) || util.isString(id)) { + if (this._data[id]) { + delete this._data[id]; + this.length--; + return id; } - - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; - - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } - - return normalizedInput; + } else if (id instanceof Object) { + var itemId = id[this._fieldId]; + if (itemId && this._data[itemId]) { + delete this._data[itemId]; + this.length--; + return itemId; } + } + return null; + }; - function makeList(field) { - var count, setter; + /** + * 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); - if (field.indexOf('week') === 0) { - count = 7; - setter = 'day'; - } - else if (field.indexOf('month') === 0) { - count = 12; - setter = 'month'; - } - else { - return; - } + this._data = {}; + this.length = 0; - moment[field] = function (format, index) { - var i, getter, - method = moment._locale[field], - results = []; + this._trigger("remove", { items: ids }, senderId); - if (typeof format === 'number') { - index = format; - format = undefined; - } + return ids; + }; - getter = function (i) { - var m = moment().utc().set(setter, i); - return method.call(moment._locale, m, format || ''); - }; + /** + * 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; - if (index != null) { - return getter(index); - } - else { - for (i = 0; i < count; i++) { - results.push(getter(i)); - } - return results; - } - }; + 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; + } } + } - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; + return max; + }; - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } + /** + * 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; - return value; + 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; + } } + } - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } + return min; + }; - function weeksInYear(year, dow, doy) { - return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; - } + /** + * 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; - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; + 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++; + } } + } - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); } + } - 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] > 24 || - (m._a[HOUR] === 24 && (m._a[MINUTE] !== 0 || - m._a[SECOND] !== 0 || - m._a[MILLISECOND] !== 0)) ? 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; + return values; + }; - if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } + /** + * 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]; - m._pf.overflow = overflow; - } + 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; + } - 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 && - m._pf.bigHour === undefined; - } - } - return m._isValid; + var d = {}; + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); } + } + this._data[id] = d; + this.length++; - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; - } + return id; + }; - // pick the locale from the array - // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each - // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root - function chooseLocale(names) { - var i = 0, j, next, locale, split; + /** + * 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; - while (i < names.length) { - split = normalizeLocale(names[i]).split('-'); - j = split.length; - next = normalizeLocale(names[i + 1]); - next = next ? next.split('-') : null; - while (j > 0) { - locale = loadLocale(split.slice(0, j).join('-')); - if (locale) { - return locale; - } - if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { - //the next array item is better than a shallower substring of this one - break; - } - j--; - } - i++; - } - return null; - } + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; + } - function loadLocale(name) { - var oldLocale = null; - if (!locales[name] && hasModule) { - try { - oldLocale = moment.locale(); - !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); - // because defineLocale currently also sets the global locale, we want to undo that for lazy loaded locales - moment.locale(oldLocale); - } catch (e) { } - } - return locales[name]; + // 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]); + } } - - // Return a moment from input, that is local/utc/utcOffset equivalent to - // model. - function makeAs(input, model) { - var res, diff; - if (model._isUTC) { - res = model.clone(); - diff = (moment.isMoment(input) || isDate(input) ? - +input : +moment(input)) - (+res); - // Use low-level api, because this fn is low-level api. - res._d.setTime(+res._d + diff); - moment.updateOffset(res, false); - return res; - } else { - return moment(input).local(); - } + } else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = value; + } } + } + return converted; + }; - /************************************ - Locale - ************************************/ + /** + * 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); + } + } - extend(Locale.prototype, { + return id; + }; - set : function (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; - } else { - this['_' + i] = prop; - } - } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + /\d{1,2}/.source); - }, + /** + * 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; + }; - _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), - months : function (m) { - return this._months[m.month()]; - }, + /** + * 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(); - _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), - monthsShort : function (m) { - return this._monthsShort[m.month()]; - }, + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + dataTable.setValue(row, col, item[field]); + } + }; - monthsParse : function (monthName, format, strict) { - var i, mom, regex; + module.exports = DataSet; - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { - for (i = 0; i < 12; i++) { - // make the regex if we don't have it already - mom = moment.utc([2000, i]); - if (strict && !this._longMonthsParse[i]) { - this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); - this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); - } - if (!strict && !this._monthsParse[i]) { - regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); - this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { - return i; - } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { - return i; - } else if (!strict && this._monthsParse[i].test(monthName)) { - return i; - } - } - }, + "use strict"; - _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), - weekdays : function (m) { - return this._weekdays[m.day()]; - }, + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); - _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), - weekdaysShort : function (m) { - return this._weekdaysShort[m.day()]; - }, + /** + * DataView + * + * a dataview offers a filtered view on a dataset or an other dataview. + * + * @param {DataSet | DataView} data + * @param {Object} [options] Available options: see method get + * + * @constructor DataView + */ + function DataView(data, options) { + this._data = null; + this._ids = {}; // ids of the items currently in memory (just contains a boolean true) + this.length = 0; // number of items in the DataView + this._options = options || {}; + this._fieldId = "id"; // name of the field containing id + this._subscribers = {}; // event subscribers - _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), - weekdaysMin : function (m) { - return this._weekdaysMin[m.day()]; - }, + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); + }; - weekdaysParse : function (weekdayName) { - var i, mom, regex; + this.setData(data); + } - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly - 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; - } - } - }, + /** + * Set a data source for the view + * @param {DataSet | DataView} data + */ + DataView.prototype.setData = function (data) { + var ids, i, len; - _longDateFormat : { - LTS : 'h:mm:ss A', - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }, - 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; - }, + if (this._data) { + // unsubscribe from current dataset + if (this._data.unsubscribe) { + this._data.unsubscribe("*", this.listener); + } - 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'); - }, + // trigger a remove of all items in memory + ids = []; + for (var id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + ids.push(id); + } + } + this._ids = {}; + this.length = 0; + this._trigger("remove", { items: ids }); + } - _meridiemParse : /[ap]\.?m?\.?/i, - meridiem : function (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } - }, + this._data = data; + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || "id"; - _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, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.apply(mom, [now]) : output; - }, + // trigger an add of all added items + ids = this._data.getIds({ filter: this._options && this._options.filter }); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this._ids[id] = true; + } + this.length = ids.length; + this._trigger("add", { items: ids }); - _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' - }, + // subscribe to new dataset + if (this._data.on) { + this._data.on("*", this.listener); + } + } + }; - 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); - }, + /** + * Refresh the DataView. Useful when the DataView has a filter function + * containing a variable parameter. + */ + DataView.prototype.refresh = function () { + var id; + var ids = this._data.getIds({ filter: this._options && this._options.filter }); + var newIds = {}; + var added = []; + var removed = []; - pastFuture : function (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - }, + // check for additions + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + newIds[id] = true; + if (!this._ids[id]) { + added.push(id); + this._ids[id] = true; + this.length++; + } + } - ordinal : function (number) { - return this._ordinal.replace('%d', number); - }, - _ordinal : '%d', - _ordinalParse : /\d{1,2}/, + // check for removals + for (id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + if (!newIds[id]) { + removed.push(id); + delete this._ids[id]; + this.length--; + } + } + } - preparse : function (string) { - return string; - }, + // trigger events + if (added.length) { + this._trigger("add", { items: added }); + } + if (removed.length) { + this._trigger("remove", { items: removed }); + } + }; - postformat : function (string) { - return string; - }, + /** + * 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; - week : function (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - }, + // 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]; + } - _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. - }, + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); - firstDayOfWeek : function () { - return this._week.dow; - }, + // 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); + }; + } - firstDayOfYear : function () { - return this._week.doy; - }, + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); + } + getArguments.push(viewOptions); + getArguments.push(data); - _invalidDate: 'Invalid date', - invalidDate: function () { - return this._invalidDate; - } + 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 = []; + } - /************************************ - Formatting - ************************************/ + 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; + }; - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); + /** + * 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); + } } - return input.replace(/\\/g, ''); - } - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; + break; - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; + 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 { - array[i] = removeFormattingTokens(array[i]); + this._ids[id] = true; + added.push(id); } + } else { + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } else {} + } } - 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; - }; - } + break; - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); + 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); + } } - format = expandFormat(format, m.localeData()); + break; + } - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } + this.length += added.length - removed.length; - return formatFunctions[format](m); + 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); } + } + }; - function expandFormat(format, locale) { - var i = 5; + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } + // 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; - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } + module.exports = DataView; + // nothing interesting for me :-( - return format; - } +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; - /************************************ - Parsing - ************************************/ + /** + * A queue + * @param {Object} options + * Available options: + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @constructor + */ + function Queue(options) { + // options + this.delay = null; + this.max = Infinity; + // properties + this._queue = []; + this._timeout = null; + this._extended = null; - // 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 config._locale._meridiemParse; - case 'x': - return parseTokenOffsetMs; - 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 strict ? config._locale._ordinalParse : config._locale._ordinalParseLenient; - default : - a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), 'i')); - return a; - } + this.setOptions(options); + } + + /** + * Update the configuration of the queue + * @param {Object} options + * Available options: + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @param options + */ + Queue.prototype.setOptions = function (options) { + if (options && typeof options.delay !== "undefined") { + this.delay = options.delay; + } + if (options && typeof options.max !== "undefined") { + this.max = options.max; + } + + this._flushIfNeeded(); + }; + + /** + * Extend an object with queuing functionality. + * The object will be extended with a function flush, and the methods provided + * in options.replace will be replaced with queued ones. + * @param {Object} object + * @param {Object} options + * Available options: + * - replace: Array. + * A list with method names of the methods + * on the object to be replaced with queued ones. + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @return {Queue} Returns the created queue + */ + Queue.extend = function (object, options) { + var queue = new Queue(options); + + if (object.flush !== undefined) { + throw new Error("Target object already has a property flush"); + } + object.flush = function () { + queue.flush(); + }; + + var methods = [{ + name: "flush", + original: undefined + }]; + + if (options && options.replace) { + for (var i = 0; i < options.replace.length; i++) { + var name = options.replace[i]; + methods.push({ + name: name, + original: object[name] + }); + queue.replace(object, name); } + } - function utcOffsetFromString(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]); + queue._extended = { + object: object, + methods: methods + }; - return parts[0] === '+' ? minutes : -minutes; + return queue; + }; + + /** + * Destroy the queue. The queue will first flush all queued actions, and in + * case it has extended an object, will restore the original object. + */ + Queue.prototype.destroy = function () { + this.flush(); + + if (this._extended) { + var object = this._extended.object; + var methods = this._extended.methods; + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + if (method.original) { + object[method.name] = method.original; + } else { + delete object[method.name]; + } } + this._extended = null; + } + }; - // function to convert string input to date - function addTimeToArrayFromToken(token, input, config) { - var a, datePartArray = config._a; + /** + * Replace a method on an object with a queued version + * @param {Object} object Object having the method + * @param {string} method The method name + */ + Queue.prototype.replace = function (object, method) { + var me = this; + var original = object[method]; + if (!original) { + throw new Error("Method " + method + " undefined"); + } - 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 = config._locale.monthsParse(input, token, config._strict); - // 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.match(/\d{1,2}/)[0], 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._meridiem = input; - // config._isPm = config._locale.isPM(input); - break; - // HOUR - case 'h' : // fall through to hh - case 'hh' : - config._pf.bigHour = true; - /* falls through */ - 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 OFFSET (MILLISECONDS) - case 'x': - config._d = new Date(toInt(input)); - 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 = utcOffsetFromString(input); - break; - // WEEKDAY - human - case 'dd': - case 'ddd': - case 'dddd': - a = config._locale.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); - } + object[method] = function () { + // create an Array with the arguments + var args = []; + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; } - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; - - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; - - // 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 { - dow = config._locale._week.dow; - doy = config._locale._week.doy; - - weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); - week = dfl(w.w, 1); + // add this call to the queue + me.queue({ + args: args, + fn: original, + context: this + }); + }; + }; - 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); + /** + * Queue a call + * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry + */ + Queue.prototype.queue = function (entry) { + if (typeof entry === "function") { + this._queue.push({ fn: entry }); + } else { + this._queue.push(entry); + } - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } + this._flushIfNeeded(); + }; - // 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; + /** + * Check whether the queue needs to be flushed + * @private + */ + Queue.prototype._flushIfNeeded = function () { + // flush when the maximum is exceeded. + if (this._queue.length > this.max) { + this.flush(); + } - if (config._d) { - return; - } + // flush after a period of inactivity when a delay is configured + clearTimeout(this._timeout); + if (this.queue.length > 0 && typeof this.delay === "number") { + var me = this; + this._timeout = setTimeout(function () { + me.flush(); + }, this.delay); + } + }; - currentDate = currentDateArray(config); + /** + * Flush all queued calls + */ + Queue.prototype.flush = function () { + while (this._queue.length > 0) { + var entry = this._queue.shift(); + entry.fn.apply(entry.context || entry.fn, entry.args || []); + } + }; - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } + module.exports = Queue; - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { - if (config._dayOfYear > daysInYear(yearToUse)) { - config._pf._overflowDayOfYear = true; - } + "use strict"; - date = makeUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } + var Emitter = __webpack_require__(62); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var util = __webpack_require__(1); + var Point3d = __webpack_require__(10); + var Point2d = __webpack_require__(9); + var Camera = __webpack_require__(7); + var Filter = __webpack_require__(8); + var Slider = __webpack_require__(11); + var StepNumber = __webpack_require__(12); - // 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]; - } + /** + * @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"); + } - // 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]; - } + // 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%"; - // Check for 24:00:00.000 - if (config._a[HOUR] === 24 && - config._a[MINUTE] === 0 && - config._a[SECOND] === 0 && - config._a[MILLISECOND] === 0) { - config._nextDay = true; - config._a[HOUR] = 0; - } + this.xLabel = "x"; + this.yLabel = "y"; + this.zLabel = "z"; - config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); - // Apply timezone offset from input. The actual utcOffset can be changed - // with parseZone. - if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); - } + var passValueFn = function (v) { + return v; + }; + this.xValueLabel = passValueFn; + this.yValueLabel = passValueFn; + this.zValueLabel = passValueFn; - if (config._nextDay) { - config._a[HOUR] = 24; - } - } + this.filterLabel = "time"; + this.legendLabel = "value"; - function dateFromObject(config) { - var normalizedInput; + 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' - if (config._d) { - return; - } + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - normalizedInput = normalizeObjectUnits(config._i); - config._a = [ - normalizedInput.year, - normalizedInput.month, - normalizedInput.day || normalizedInput.date, - normalizedInput.hour, - normalizedInput.minute, - normalizedInput.second, - normalizedInput.millisecond - ]; + this.camera = new Camera(); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - dateFromConfig(config); - } + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects - 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()]; - } - } + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; - // date from string and format string - function makeDateFromStringAndFormat(config) { - if (config._f === moment.ISO_8601) { - parseISO(config); - return; - } + 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 - config._a = []; - config._pf.empty = true; + // constants + this.colorAxis = "#4D4D4D"; + this.colorGrid = "#D3D3D3"; + this.colorDot = "#7DC1FF"; + this.colorDotBorder = "#3267D2"; - // This array is used to make a Date, either with `new Date` or `Date.UTC` - var string = '' + config._i, - i, parsedInput, tokens, token, skipped, - stringLength = string.length, - totalParsedInputLength = 0; + // create a frame and canvas + this.create(); - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + // apply options (also when undefined) + this.setOptions(options); - 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); - } - } + // apply data + if (data) { + this.setData(data); + } + } - // add remaining unparsed input length to the string - config._pf.charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - config._pf.unusedInput.push(string); - } + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); - // clear _12h flag if hour is <= 12 - if (config._pf.bigHour === true && config._a[HOUR] <= 12) { - config._pf.bigHour = undefined; - } - // handle meridiem - config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], - config._meridiem); - dateFromConfig(config); - checkOverflow(config); - } + /** + * 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)); - function unescapeFormat(s) { - return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }); + // 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; } + } - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function regexpEscape(s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? - // date from string and array of format strings - function makeDateFromStringAndArray(config) { - var tempConfig, - bestMoment, + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); - scoreToBeat, - i, - currentScore; + // 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); + }; - 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 = copyConfig({}, config); - if (config._useUTC != null) { - tempConfig._useUTC = config._useUTC; - } - tempConfig._pf = defaultParsingFlags(); - tempConfig._f = config._f[i]; - makeDateFromStringAndFormat(tempConfig); + /** + * 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); + }; - if (!isValid(tempConfig)) { - continue; - } + /** + * 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, - // if there is any input that was not parsed add a penalty for that format - currentScore += tempConfig._pf.charsLeftOver; - //or tokens - currentScore += tempConfig._pf.unusedTokens.length * 10; + // 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), - tempConfig._pf.score = currentScore; - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } - } + // 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)); - extend(config, bestMoment || tempConfig); - } + return new Point3d(dx, dy, dz); + }; - // date from iso format - function parseISO(config) { - var i, l, - string = config._i, - match = isoRegex.exec(string); + /** + * 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; - 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; - } - } + // 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()); + } - // date from iso format or fallback - function makeDateFromString(config) { - parseISO(config); - if (config._isValid === false) { - delete config._isValid; - moment.createFromInputFallback(config); - } - } + // shift and scale the point to the center of the screen + // use the width of the graph to scale both horizontally and vertically. + return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); + }; - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } + /** + * 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; - function makeDateFromInput(config) { - var input = config._i, matched; - if (input === undefined) { - config._d = new Date(); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if ((matched = aspNetJsonRegex.exec(input)) !== null) { - config._d = new Date(+matched[1]); - } else if (typeof input === 'string') { - makeDateFromString(config); - } else if (isArray(input)) { - config._a = map(input.slice(0), function (obj) { - return parseInt(obj, 10); - }); - dateFromConfig(config); - } else if (typeof(input) === 'object') { - dateFromObject(config); - } else if (typeof(input) === 'number') { - // from milliseconds - config._d = new Date(input); - } else { - moment.createFromInputFallback(config); - } - } + if (typeof backgroundColor === "string") { + fill = backgroundColor; + stroke = "none"; + strokeWidth = 0; + } else if (typeof backgroundColor === "object") { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } else if (backgroundColor === undefined) {} else { + throw "Unsupported type of backgroundColor"; + } - 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); + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + "px"; + this.frame.style.borderStyle = "solid"; + }; - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } - function makeUTCDate(y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; + /// 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; - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; } + } else { + throw "Unknown style \"" + this.style + "\""; + } + }; - /************************************ - Relative Time - ************************************/ + Graph3d.prototype.getNumberOfRows = function (data) { + return data.length; + }; - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + Graph3d.prototype.getNumberOfColumns = function (data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; } + } + return counter; + }; - function relativeTime(posNegDuration, withoutSuffix, locale) { - var duration = moment.duration(posNegDuration).abs(), - seconds = round(duration.as('s')), - minutes = round(duration.as('m')), - hours = round(duration.as('h')), - days = round(duration.as('d')), - months = round(duration.as('M')), - years = round(duration.as('y')), - - 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.d && ['dd', days] || - months === 1 && ['M'] || - months < relativeTimeThresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; - args[2] = withoutSuffix; - args[3] = +posNegDuration > 0; - args[4] = locale; - return substituteTimeAgo.apply({}, args); + 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; + }; - /************************************ - Week of Year - ************************************/ + 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; - // 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; + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off("*", this._onChange); + } + if (rawData === undefined) return; - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; - } + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); + } - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; - } + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } else { + throw new Error("Array, DataSet, or DataView expected"); + } - adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; - } + if (data.length == 0) return; - //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; + this.dataSet = rawData; + this.dataTable = data; - 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; + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on("*", this._onChange); - return { - year: dayOfYear > 0 ? year : year - 1, - dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange - /************************************ - Top Level Functions - ************************************/ + // 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"; - function makeMoment(config) { - var input = config._i, - format = config._f, - res; - config._locale = config._locale || moment.localeData(config._l); - if (input === null || (format === undefined && input === '')) { - return moment.invalid({nullInput: true}); - } + // 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(); + }); + } + } - if (typeof input === 'string') { - config._i = input = config._locale.preparse(input); - } - if (moment.isMoment(input)) { - return new Moment(input, true); - } else if (format) { - if (isArray(format)) { - makeDateFromStringAndArray(config); - } else { - makeDateFromStringAndFormat(config); - } - } else { - makeDateFromInput(config); - } + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; - res = new Moment(config); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; - } + // 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; + } - return res; + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } else { + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 1; } + } - moment = function (input, format, locale, strict) { - var c; + // 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; - if (typeof(locale) === 'boolean') { - strict = locale; - locale = 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 = locale; - c._strict = strict; - c._isUTC = false; - c._pf = defaultParsingFlags(); + 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; - return makeMoment(c); - }; + 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; - moment.suppressDeprecationWarnings = false; + 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; + } - 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 + (config._useUTC ? ' UTC' : '')); - } - ); - - // 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; - } + // set the scale dependent on the ranges. + this._setScale(); + }; - moment.min = function () { - var args = [].slice.call(arguments, 0); - return pickBy('isBefore', args); - }; - moment.max = function () { - var args = [].slice.call(arguments, 0); + /** + * 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; - return pickBy('isAfter', args); - }; + var dataPoints = []; - // creating with utc - moment.utc = function (input, format, locale, strict) { - var c; + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { + // copy all values from the google data table to a matrix + // the provided values are supposed to form a grid of (x,y) positions - if (typeof(locale) === 'boolean') { - strict = locale; - locale = 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 = locale; - c._i = input; - c._f = format; - c._strict = strict; - c._pf = defaultParsingFlags(); + // 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; - return makeMoment(c).utc(); - }; + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } + } - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); + var sortNumber = function (a, b) { + return a - b; }; + dataX.sort(sortNumber); + dataY.sort(sortNumber); - // duration - moment.duration = function (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - parseIso, - diffRes; + // 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; - 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]) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && - ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(moment(duration.from), moment(duration.to)); + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } + if (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - ret = new Duration(duration); + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - if (moment.isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; - } + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); - return ret; - }; + dataMatrix[xIndex][yIndex] = obj; - // version number - moment.version = VERSION; + dataPoints.push(obj); + } - // default format - moment.defaultFormat = isoFormat; + // 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; - // constant that refers to the ISO standard - moment.ISO_8601 = function () {}; + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; + } - // Plugins that add properties should also add the key here (null value), - // so we can properly clone ourselves. - moment.momentProperties = momentProperties; + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; - // 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 () {}; + dataPoints.push(obj); + } + } - // This function allows you to set a threshold for relative time strings - moment.relativeTimeThreshold = function (threshold, limit) { - if (relativeTimeThresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return relativeTimeThresholds[threshold]; - } - relativeTimeThresholds[threshold] = limit; - return true; - }; + return dataPoints; + }; - moment.lang = deprecate( - 'moment.lang is deprecated. Use moment.locale instead.', - function (key, value) { - return moment.locale(key, value); - } - ); + /** + * 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 function will load locale and then set the global locale. If - // no arguments are passed in, it will simply return the current global - // locale key. - moment.locale = function (key, values) { - var data; - if (key) { - if (typeof(values) !== 'undefined') { - data = moment.defineLocale(key, values); - } - else { - data = moment.localeData(key); - } + this.frame = document.createElement("div"); + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; - if (data) { - moment.duration._locale = moment._locale = data; - } - } + // 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); + } - return moment._locale._abbr; - }; + 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); - moment.defineLocale = function (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + // 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' - // backwards compat for now: also set the locale - moment.locale(name); + 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); - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } - }; + // add the new graph to the container element + this.containerElement.appendChild(this.frame); + }; - moment.langData = deprecate( - 'moment.langData is deprecated. Use moment.localeData instead.', - function (key) { - return moment.localeData(key); - } - ); - // returns locale data - moment.localeData = function (key) { - var locale; + /** + * 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; - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; - } + this._resizeCanvas(); + }; - if (!key) { - return moment._locale; - } + /** + * 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%"; - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; - return chooseLocale(key); - }; + // adjust with for margin + this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + "px"; + }; - // compare moment object - moment.isMoment = function (obj) { - return obj instanceof Moment || - (obj != null && hasOwnProp(obj, '_isAMomentObject')); - }; + /** + * Start animation + */ + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw "No animation available"; - // for typechecking Duration objects - moment.isDuration = function (obj) { - return obj instanceof Duration; - }; + this.frame.filter.slider.play(); + }; - for (i = lists.length - 1; i >= 0; --i) { - makeList(lists[i]); - } - moment.normalizeUnits = function (units) { - return normalizeUnits(units); - }; + /** + * Stop animation + */ + Graph3d.prototype.animationStop = function () { + if (!this.frame.filter || !this.frame.filter.slider) return; - moment.invalid = function (flags) { - var m = moment.utc(NaN); - if (flags != null) { - extend(m._pf, flags); - } - else { - m._pf.userInvalidated = true; - } + this.frame.filter.slider.stop(); + }; - return m; - }; - moment.parseZone = function () { - return moment.apply(null, arguments).parseZone(); - }; + /** + * 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 + } - moment.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; + // 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 + } + }; - moment.isDate = isDate; + /** + * 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; + } - /************************************ - Moment Prototype - ************************************/ + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); + } + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); + } - extend(moment.fn = Moment.prototype, { + this.redraw(); + }; - clone : function () { - return moment(this); - }, - valueOf : function () { - return +this._d - ((this._offset || 0) * 60000); - }, + /** + * 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; + }; - unix : function () { - return Math.floor(+this / 1000); - }, + /** + * Load data into the 3D Graph + */ + Graph3d.prototype._readData = function (data) { + // read the data + this._dataInitialize(data, this.style); - toString : function () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); - }, - toDate : function () { - return this._offset ? new Date(+this) : this._d; - }, + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); + } - toISOString : function () { - var m = moment(this).utc(); - if (0 < m.year() && m.year() <= 9999) { - if ('function' === typeof Date.prototype.toISOString) { - // native implementation is ~50x faster, use it when we can - return this.toDate().toISOString(); - } else { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - }, + // draw the filter + this._redrawFilter(); + }; - toArray : function () { - var m = this; - return [ - m.year(), - m.month(), - m.date(), - m.hours(), - m.minutes(), - m.seconds(), - m.milliseconds() - ]; - }, + /** + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data + */ + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); - isValid : function () { - return isValid(this); - }, + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + }; - isDSTShifted : function () { - if (this._a) { - return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; - } + /** + * Update the options. Options will be merged with current options + * @param {Object} options + */ + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - return false; - }, + this.animationStop(); - parsingFlags : function () { - return extend({}, this._pf); - }, + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; - invalidAt: function () { - return this._pf.overflow; - }, + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - utc : function (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - }, + 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; - local : function (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; + if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; + if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; + if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; - if (keepLocalTime) { - this.subtract(this._dateUtcOffset(), 'm'); - } - } - return this; - }, + 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; - format : function (inputString) { - var output = formatMoment(this, inputString || moment.defaultFormat); - return this.localeData().postformat(output); - }, + if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; - add : createAdder(1, 'add'), + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - subtract : createAdder(-1, 'subtract'), + 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; - diff : function (input, units, asFloat) { - var that = makeAs(input, this), - zoneDiff = (that.utcOffset() - this.utcOffset()) * 6e4, - anchor, diff, output, daysAdjust; + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - units = normalizeUnits(units); + if (cameraPosition !== undefined) { + this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); + this.camera.setArmLength(cameraPosition.distance); + } else { + this.camera.setArmRotation(1, 0.5); + this.camera.setArmLength(1.7); + } + } - if (units === 'year' || units === 'month' || units === 'quarter') { - output = monthDiff(this, that); - if (units === 'quarter') { - output = output / 3; - } else if (units === 'year') { - output = output / 12; - } - } else { - 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._setBackgroundColor(options && options.backgroundColor); - from : function (time, withoutSuffix) { - return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - }, + this.setSize(this.width, this.height); - fromNow : function (withoutSuffix) { - return this.from(moment(), withoutSuffix); - }, + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); + } - calendar : function (time) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're locat/utc/offset - // 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.localeData().calendar(format, this, moment(now))); - }, + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + }; - isLeapYear : function () { - return isLeapYear(this.year()); - }, + /** + * Redraw the Graph. + */ + Graph3d.prototype.redraw = function () { + if (this.dataPoints === undefined) { + throw "Error: graph data not initialized"; + } - isDST : function () { - return (this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset()); - }, + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); - day : function (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; - } - }, + 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(); + } - month : makeAccessor('Month', true), + this._redrawInfo(); + this._redrawLegend(); + }; - 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 */ - } + /** + * Clear the canvas before redrawing + */ + Graph3d.prototype._redrawClear = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } + ctx.clearRect(0, 0, canvas.width, canvas.height); + }; - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } - return this; - }, + /** + * Redraw the legend showing the colors + */ + Graph3d.prototype._redrawLegend = function () { + var y; - endOf: function (units) { - units = normalizeUnits(units); - if (units === undefined || units === 'millisecond') { - return this; - } - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); - }, + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + var dotSize = this.frame.clientWidth * 0.02; - isAfter: function (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = moment.isMoment(input) ? input : moment(input); - return +this > +input; - } else { - inputMs = moment.isMoment(input) ? +input : +moment(input); - return inputMs < +this.clone().startOf(units); - } - }, + 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 + } - isBefore: function (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = moment.isMoment(input) ? input : moment(input); - return +this < +input; - } else { - inputMs = moment.isMoment(input) ? +input : +moment(input); - return +this.clone().endOf(units) < inputMs; - } - }, + 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; + } - isBetween: function (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - }, + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + ctx.lineWidth = 1; + ctx.font = "14px arial"; // TODO: put in options - isSame: function (input, units) { - var inputMs; - units = normalizeUnits(units || 'millisecond'); - if (units === 'millisecond') { - input = moment.isMoment(input) ? input : moment(input); - return +this === +input; - } else { - inputMs = +moment(input); - return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); - } - }, + 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); - 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; - } - ), + //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); - 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; - } - ), + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); + } - zone : deprecate( - 'moment().zone is deprecated, use moment().utcOffset instead. ' + - 'https://github.com/moment/moment/issues/1779', - function (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } + ctx.strokeStyle = this.colorAxis; + ctx.strokeRect(left, top, widthMax, height); + } - this.utcOffset(input, keepLocalTime); + 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(); + } - return this; - } else { - return -this.utcOffset(); - } - } - ), + 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; - // keepLocalTime = true means only change the timezone, without - // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> - // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset - // +0200, so we adjust the time as needed, to be valid. - // - // Keeping the time actually adds/subtracts (one hour) - // from the actual represented time. That is why we call updateOffset - // a second time. In case it wants us to change the offset again - // _changeInProgress == true case, then we have to adjust, because - // there is no such time in the given timezone. - utcOffset : function (input, keepLocalTime) { - var offset = this._offset || 0, - localAdjust; - if (input != null) { - if (typeof input === 'string') { - input = utcOffsetFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = this._dateUtcOffset(); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.add(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - addOrSubtractDurationFromMoment(this, - moment.duration(input - offset, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - moment.updateOffset(this, true); - this._changeInProgress = null; - } - } + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); - return this; - } else { - return this._isUTC ? offset : this._dateUtcOffset(); - } - }, + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - isLocal : function () { - return !this._isUTC; - }, + step.next(); + } - isUtcOffset : function () { - return this._isUTC; - }, + ctx.textAlign = "right"; + ctx.textBaseline = "top"; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); + } + }; - isUtc : function () { - return this._isUTC && this._offset === 0; - }, + /** + * Redraw the filter + */ + Graph3d.prototype._redrawFilter = function () { + this.frame.filter.innerHTML = ""; - zoneAbbr : function () { - return this._isUTC ? 'UTC' : ''; - }, + if (this.dataFilter) { + var options = { + visible: this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; - zoneName : function () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - }, + // TODO: css here is not nice here... + this.frame.filter.style.padding = "10px"; + //this.frame.filter.style.backgroundColor = '#EFEFEF'; - parseZone : function () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(utcOffsetFromString(this._i)); - } - return this; - }, + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); - hasAlignedHourOffset : function (input) { - if (!input) { - input = 0; - } - else { - input = moment(input).utcOffset(); - } - - return (this.utcOffset() - input) % 60 === 0; - }, - - 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((input - dayOfYear), 'd'); - }, - - quarter : function (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - }, - - weekYear : function (input) { - var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; - return input == null ? year : this.add((input - year), 'y'); - }, - - isoWeekYear : function (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - }, - - week : function (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - }, - - isoWeek : function (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - }, - - weekday : function (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - }, + // create an event handler + var me = this; + var onchange = function () { + var index = slider.getIndex(); - 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); - }, + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); - isoWeeksInYear : function () { - return weeksInYear(this.year(), 1, 4); - }, + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } else { + this.frame.filter.slider = undefined; + } + }; - weeksInYear : function () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - }, + /** + * Redraw the slider + */ + Graph3d.prototype._redrawSlider = function () { + if (this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); + } + }; - get : function (units) { - units = normalizeUnits(units); - return this[units](); - }, - set : function (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } - } - else { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - this[units](value); - } - } - return this; - }, + /** + * Redraw common information + */ + Graph3d.prototype._redrawInfo = function () { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); - // If passed a locale key, it will set the locale for this - // instance. Otherwise, it will return the locale configuration - // variables for this instance. - locale : function (key) { - var newLocaleData; + ctx.font = "14px arial"; // TODO: put in options + ctx.lineStyle = "gray"; + ctx.fillStyle = "gray"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = moment.localeData(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } - }, + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); + } + }; - lang : deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - return this.locale(key); - } - } - ), - localeData : function () { - return this._locale; - }, + /** + * 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; - _dateUtcOffset : function () { - // On Firefox.24 Date#getTimezoneOffset returns a floating point. - // https://github.com/moment/moment/pull/1871 - return -Math.round(this._d.getTimezoneOffset() / 15) * 15; - } + // 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; - function rawMonthSetter(mom, value) { - var dayOfMonth; + // 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(); - // TODO: Move this out of here! - if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; - } - } + 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(); - dayOfMonth = Math.min(mom.date(), - daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; + 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(); } - function rawGetter(mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + 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(" " + this.xValueLabel(step.getCurrent()) + " ", text.x, text.y); - function rawSetter(mom, unit, value) { - if (unit === 'Month') { - return rawMonthSetter(mom, value); - } else { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } + 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(); } - 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); - } - }; + 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(" " + this.yValueLabel(step.getCurrent()) + " ", text.x, text.y); - 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)); + step.next(); + } - // 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; + // 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(); - // add aliased format methods - moment.fn.toJSON = moment.fn.toISOString; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(this.zValueLabel(step.getCurrent()) + " ", from.x - 5, from.y); - // alias isUtc for dev-friendliness - moment.fn.isUTC = moment.fn.isUtc; + 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(); - /************************************ - Duration Prototype - ************************************/ + // 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(); - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; + // 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); + } - function yearsToDays (years) { - // years * 365 + absRound(years / 4) - - // absRound(years / 100) + absRound(years / 400); - return years * 146097 / 400; + // draw y-label + var yLabel = this.yLabel; + if (yLabel.length > 0) { + xOffset = 0.1 / this.scale.x; + xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset; + yText = (this.yMin + this.yMax) / 2; + text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + if (Math.cos(armAngle * 2) < 0) { + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + } else if (Math.sin(armAngle * 2) > 0) { + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + } else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; } + ctx.fillStyle = this.colorAxis; + ctx.fillText(yLabel, text.x, text.y); + } - extend(moment.duration.fn = Duration.prototype, { + // 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); + } + }; - _bubble : function () { - var milliseconds = this._milliseconds, - days = this._days, - months = this._months, - data = this._data, - seconds, minutes, hours, years = 0; + /** + * 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; - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + C = V * S; + Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(H / 60 % 2 - 1)); - seconds = absRound(milliseconds / 1000); - data.seconds = seconds % 60; + 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; - minutes = absRound(seconds / 60); - data.minutes = minutes % 60; + default: + R = 0;G = 0;B = 0;break; + } - hours = absRound(minutes / 60); - data.hours = hours % 24; + return "RGB(" + parseInt(R * 255) + "," + parseInt(G * 255) + "," + parseInt(B * 255) + ")"; + }; - days += absRound(hours / 24); - // Accurately convert days to years, assume start from year 0. - years = absRound(daysToYears(days)); - days -= absRound(yearsToDays(years)); - - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absRound(days / 30); - days %= 30; - - // 12 months -> 1 year - years += absRound(months / 12); - months %= 12; - - data.days = days; - data.months = months; - data.years = years; - }, - - abs : function () { - this._milliseconds = Math.abs(this._milliseconds); - this._days = Math.abs(this._days); - this._months = Math.abs(this._months); - - this._data.milliseconds = Math.abs(this._data.milliseconds); - this._data.seconds = Math.abs(this._data.seconds); - this._data.minutes = Math.abs(this._data.minutes); - this._data.hours = Math.abs(this._data.hours); - this._data.months = Math.abs(this._data.months); - this._data.years = Math.abs(this._data.years); + /** + * 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; - return this; - }, - weeks : function () { - return absRound(this.days() / 7); - }, + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - valueOf : function () { - return this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6; - }, + // 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); - humanize : function (withSuffix) { - var output = relativeTime(this, !withSuffix, this.localeData()); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - if (withSuffix) { - output = this.localeData().pastFuture(+this, output); - } + // 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; + } - return this.localeData().postformat(output); - }, + // 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); - add : function (input, val) { - // supports only 2.0-style add(1, 's') or add(moment) - var dur = moment.duration(input, val); + if (this.style === Graph3d.STYLE.SURFACE) { + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + cross = this.dataPoints[i].pointCross; - this._milliseconds += dur._milliseconds; - this._days += dur._days; - this._months += dur._months; + if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { + 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) - this._bubble(); + topSideVisible = crossproduct.z > 0; + } else { + topSideVisible = true; + } - return this; - }, + 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 - subtract : function (input, val) { - var dur = moment.duration(input, val); + 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; - this._milliseconds -= dur._milliseconds; - this._days -= dur._days; - this._months -= dur._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(); + } + } + } 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; - this._bubble(); + if (point !== undefined) { + if (this.showPerspective) { + lineWidth = 2 / -point.trans.z; + } else { + lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); + } + } - return this; - }, + 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; - get : function (units) { - units = normalizeUnits(units); - return this[units.toLowerCase() + 's'](); - }, + 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(); + } - as : function (units) { - var days, months; - units = normalizeUnits(units); + if (point !== undefined && top !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + top.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - if (units === 'month' || units === 'year') { - days = this._days + this._milliseconds / 864e5; - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(yearsToDays(this._months / 12)); - switch (units) { - case 'week': return days / 7 + this._milliseconds / 6048e5; - case 'day': return days + this._milliseconds / 864e5; - case 'hour': return days * 24 + this._milliseconds / 36e5; - case 'minute': return days * 24 * 60 + this._milliseconds / 6e4; - case 'second': return days * 24 * 60 * 60 + this._milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 24 * 60 * 60 * 1000) + this._milliseconds; - default: throw new Error('Unknown unit ' + units); - } - } - }, + 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(); + } + } + } + }; - lang : moment.fn.lang, - locale : moment.fn.locale, - toIsoString : deprecate( - 'toIsoString() is deprecated. Please use toISOString() instead ' + - '(notice the capitals)', - function () { - return this.toISOString(); - } - ), + /** + * 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; - 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); + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - if (!this.asSeconds()) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } + // 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; - 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' : ''); - }, + // 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; + } - localeData : function () { - return this._locale; - }, + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - toJSON : function () { - return this.toISOString(); - } - }); + // 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]; - moment.duration.fn.toString = moment.duration.fn.toISOString; + 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(); + } - function makeDurationGetter(name) { - moment.duration.fn[name] = function () { - return this._data[name]; - }; + // 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; } - for (i in unitMillisecondFactors) { - if (hasOwnProp(unitMillisecondFactors, i)) { - makeDurationGetter(i.toLowerCase()); - } + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; } - moment.duration.fn.asMilliseconds = function () { - return this.as('ms'); - }; - moment.duration.fn.asSeconds = function () { - return this.as('s'); - }; - moment.duration.fn.asMinutes = function () { - return this.as('m'); - }; - moment.duration.fn.asHours = function () { - return this.as('h'); - }; - moment.duration.fn.asDays = function () { - return this.as('d'); - }; - moment.duration.fn.asWeeks = function () { - return this.as('weeks'); - }; - moment.duration.fn.asMonths = function () { - return this.as('M'); - }; - moment.duration.fn.asYears = function () { - return this.as('y'); - }; + 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 { + // 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); + } - /************************************ - Default Locale - ************************************/ + // draw the circle + ctx.lineWidth = 1; + 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(); + } + }; + /** + * 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; - // Set default locale, other locale will inherit from English. - moment.locale('en', { - ordinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; - } - }); + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - /* EMBED_LOCALES */ + // 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; - /************************************ - Exposing Moment - ************************************/ + // 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; + } - 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; - } - } + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - // 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; - } + // 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]; - return moment; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - makeGlobal(true); + // 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 { - makeGlobal(); + // 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); } - }).call(this); - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(5)(module))) - -/***/ }, -/* 4 */ -/***/ function(module, exports, __webpack_require__) { + // 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); + } - function webpackContext(req) { - throw new Error("Cannot find module '" + req + "'."); - } - webpackContext.keys = function() { return []; }; - webpackContext.resolve = webpackContext; - module.exports = webpackContext; - webpackContext.id = 4; + // 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); + }); -/***/ }, -/* 5 */ -/***/ 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; - module.exports = function(module) { - if(!module.webpackPolyfill) { - module.deprecate = function() {}; - module.paths = []; - // module.parent = undefined by default - module.children = []; - module.webpackPolyfill = 1; - } - return module; - } + // calculate the distance of each of the surface centers to the camera + for (j = 0; j < surfaces.length; j++) { + surface = surfaces[j]; + var transCenter = this._convertPointToTranslation(surface.center); + surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z; + // TODO: this dept calculation doesn't work 100% of the cases due to perspective, + // but the current solution is fast/simple and works in 99.9% of all cases + // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9}) + } + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; - // DOM utility methods + // both are equal + return 0; + }); - /** - * 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 = []; + // 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(); } } }; + /** - * 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 + * Draw a line through all datapoints. + * This function can be used when the style is 'line' */ - 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 = []; - } - } + Graph3d.prototype._redrawDataLine = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext("2d"), + point, + 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; + } + + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; + + ctx.lineWidth = 1; // TODO: make customizable + ctx.strokeStyle = "blue"; // TODO: make customizable + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + } + + // 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); + } + + // finish the line + if (this.dataPoints.length > 0) { + ctx.stroke(); } }; /** - * 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 + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) */ - 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); + Graph3d.prototype._onMouseDown = function (event) { + event = event || window.event; + + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); } - JSONcontainer[elementType].used.push(element); - return element; + + // only react on left mouse button down + this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; + if (!this.leftButtonDown && !this.touchDown) return; + + // get mouse position (different code for IE and all other browsers) + this.startMouseX = getMouseX(event); + this.startMouseY = getMouseY(event); + + this.startStart = new Date(this.start); + this.startEnd = new Date(this.end); + this.startArmRotation = this.camera.getArmRotation(); + + this.frame.style.cursor = "move"; + + // add event listeners to handle moving the contents + // we store the function onmousemove and onmouseup in the graph, so we can + // remove the eventlisteners lateron in the function mouseUp() + var me = this; + this.onmousemove = function (event) { + me._onMouseMove(event); + }; + this.onmouseup = function (event) { + me._onMouseUp(event); + }; + util.addEventListener(document, "mousemove", me.onmousemove); + util.addEventListener(document, "mouseup", me.onmouseup); + util.preventDefault(event); }; /** - * 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 + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the event */ - exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { - var element; - // allocate DOM element, if it doesnt yet exist, create one. - if (JSONcontainer.hasOwnProperty(elementType)) { - // this element has been created before - // check if there is an redundant element - if (JSONcontainer[elementType].redundant.length > 0) { - element = JSONcontainer[elementType].redundant[0]; - JSONcontainer[elementType].redundant.shift(); - } else { - // create a new element and add it to the SVG - element = document.createElement(elementType); - if (insertBefore !== undefined) { - DOMContainer.insertBefore(element, insertBefore); - } else { - DOMContainer.appendChild(element); - } - } - } else { - // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. - element = document.createElement(elementType); - JSONcontainer[elementType] = { used: [], redundant: [] }; - if (insertBefore !== undefined) { - DOMContainer.insertBefore(element, insertBefore); - } else { - DOMContainer.appendChild(element); - } - } - JSONcontainer[elementType].used.push(element); - return element; - }; + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; + // calculate change in mouse position + var diffX = parseFloat(getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(getMouseY(event)) - this.startMouseY; + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); - /** - * 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 - * @param labelObj - * @returns {*} - */ - exports.drawPoint = function (x, y, group, JSONcontainer, svgContainer, labelObj) { - 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); - } 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); + // 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 (group.options.drawPoints.styles !== undefined) { - point.setAttributeNS(null, "style", group.group.options.drawPoints.styles); + // 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; } - point.setAttributeNS(null, "class", group.className + " point"); - //handle label - var label = exports.getSVGElement("text", JSONcontainer, svgContainer); - if (labelObj) { - if (labelObj.xOffset) { - x = x + labelObj.xOffset; - } - if (labelObj.yOffset) { - y = y + labelObj.yOffset; - } - if (labelObj.content) { - label.textContent = labelObj.content; - } + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); - if (labelObj.className) { - label.setAttributeNS(null, "class", labelObj.className + " label"); - } + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit("cameraPositionChange", parameters); - } - label.setAttributeNS(null, "x", x); - label.setAttributeNS(null, "y", y); - return point; + util.preventDefault(event); }; + /** - * draw a bar SVG element centered on the X coordinate - * - * @param x - * @param y - * @param className + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event */ - exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) { - if (height != 0) { - if (height < 0) { - height *= -1; - y -= height; - } - var rect = exports.getSVGElement("rect", JSONcontainer, svgContainer); - rect.setAttributeNS(null, "x", x - 0.5 * width); - rect.setAttributeNS(null, "y", y); - rect.setAttributeNS(null, "width", width); - rect.setAttributeNS(null, "height", height); - rect.setAttributeNS(null, "class", className); - } - }; - -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = "auto"; + this.leftButtonDown = false; - var util = __webpack_require__(1); - var Queue = __webpack_require__(8); + // remove event listeners here + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); + util.preventDefault(event); + }; /** - * DataSet - * - * Usage: - * var dataSet = new DataSet({ - * fieldId: '_id', - * type: { - * // ... - * } - * }); - * - * dataSet.add(item); - * dataSet.add(data); - * dataSet.update(item); - * dataSet.update(data); - * dataSet.remove(id); - * dataSet.remove(ids); - * var data = dataSet.get(); - * var data = dataSet.get(id); - * var data = dataSet.get(ids); - * var data = dataSet.get(ids, options, data); - * dataSet.clear(); - * - * A data set can: - * - add/remove/update data - * - gives triggers upon changes in the data - * - can import/export data in various data formats - * - * @param {Array | 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. 0 ? 1 : x < 0 ? -1 : 0; } - return addedIds; + 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)); + + // 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); }; /** - * 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 + * 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 */ - DataSet.prototype.update = function (data, senderId) { - var addedIds = []; - var updatedIds = []; - var updatedData = []; - var me = this; - var fieldId = me._fieldId; + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, + // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); - var addOrUpdate = function (item) { - var id = item[fieldId]; - if (me._data[id]) { - // update item - id = me._updateItem(item); - updatedIds.push(id); - updatedData.push(item); - } else { - // add new item - id = me._addItem(item); - addedIds.push(id); + if (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); - 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); + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } } - - 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, data: updatedData }, senderId); - } - return addedIds.concat(updatedIds); + return closestDataPoint; }; /** - * 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 + * Display a tooltip for given data point + * @param {Object} dataPoint + * @private */ - 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]; - } + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - // 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 (!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 (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"; - } + line = document.createElement("div"); + line.style.position = "absolute"; + line.style.height = "40px"; + line.style.width = "0"; + line.style.borderLeft = "1px solid #4d4d4d"; - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], - item, - itemId, - i, - len; + 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"; - // 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); + this.tooltip = { + dataPoint: null, + dom: { + content: content, + line: line, + dot: dot } - } + }; } 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); + content = this.tooltip.dom.content; + line = this.tooltip.dom.line; + dot = this.tooltip.dom.dot; } - // 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); - } - } - } + this._hideTooltip(); - // 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; + this.tooltip.dataPoint = dataPoint; + if (typeof this.showTooltip === "function") { + content.innerHTML = this.showTooltip(dataPoint.point); } 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; - } - } + content.innerHTML = "" + "" + "" + "" + "
x:" + dataPoint.point.x + "
y:" + dataPoint.point.y + "
z:" + dataPoint.point.z + "
"; } - }; - /** - * 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 = []; + content.style.left = "0"; + content.style.top = "0"; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); - 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); - } - } - } + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; - this._sort(items, order); + var left = dataPoint.screen.x - contentWidth / 2; + left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); - 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]); - } - } + 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._sort(items, order); + /** + * Hide the tooltip when displayed + * @private + */ + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; - 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]); + 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 ids; }; + /**--------------------------------------------------------------------------**/ + + /** - * Returns the DataSet itself. Is overwritten for example by the DataView, - * which returns the DataSet it is connected to instead. + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x */ - DataSet.prototype.getDataSet = function () { - return this; - }; + function getMouseX(event) { + if ("clientX" in event) { + return event.clientX; + }return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + } /** - * 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. + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse y */ - DataSet.prototype.forEach = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - data = this._data, - item, - id; + function getMouseY(event) { + if ("clientY" in event) { + return event.clientY; + }return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + } - if (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); + module.exports = Graph3d; + // use use defaults - 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); - } - } - } - } - }; +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var Point3d = __webpack_require__(10); /** - * 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 + * @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 */ - DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - mappedItems = [], - data = this._data, - item; + function Camera() { + this.armLocation = new Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; - // 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)); - } - } - } + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } + this.calculateCameraOrientation(); + } - return mappedItems; + /** + * Set the location (origin) of the arm + * @param {Number} x Normalized value of x + * @param {Number} y Normalized value of y + * @param {Number} z Normalized value of z + */ + Camera.prototype.setArmLocation = function (x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; + + this.calculateCameraOrientation(); }; /** - * Filter the fields of an item - * @param {Object | null} item - * @param {String[]} fields Field names - * @return {Object | null} filteredItem or null if no item is provided - * @private + * 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. */ - DataSet.prototype._filterFields = function (item, fields) { - if (!item) { - // item is null - return item; + Camera.prototype.setArmRotation = function (horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; } - var filteredItem = {}; - - for (var field in item) { - if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { - filteredItem[field] = item[field]; - } + 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; } - return filteredItem; + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); + } }; /** - * Sort the provided array with items - * @param {Object[]} items - * @param {String | function} order A field name or custom sort function. - * @private + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical */ - 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"); - } + Camera.prototype.getArmRotation = function () { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; + + return rot; }; /** - * 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 + * Set the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 */ - DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, - len, - removedId; + Camera.prototype.setArmLength = function (length) { + if (length === undefined) return; - 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); - } - } + this.armLength = length; - if (removedIds.length) { - this._trigger("remove", { items: removedIds }, senderId); - } + // Radius must be larger than the corner of the graph, + // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the + // graph + if (this.armLength < 0.71) this.armLength = 0.71; + if (this.armLength > 5) this.armLength = 5; - return removedIds; + this.calculateCameraOrientation(); }; /** - * Remove an item by its id - * @param {Number | String | Object} id id or item - * @returns {Number | String | null} id - * @private + * Retrieve the arm length + * @return {Number} length */ - DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this._data[id]) { - delete this._data[id]; - this.length--; - return id; - } - } else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - this.length--; - return itemId; - } - } - return null; + Camera.prototype.getArmLength = function () { + return this.armLength; }; /** - * Clear the data - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds The ids of all removed items + * Retrieve the camera location + * @return {Point3d} cameraLocation */ - DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this._data); - - this._data = {}; - this.length = 0; - - this._trigger("remove", { items: ids }, senderId); - - return ids; + Camera.prototype.getCameraLocation = function () { + return this.cameraLocation; }; /** - * 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 + * Retrieve the camera rotation + * @return {Point3d} cameraRotation */ - 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; + Camera.prototype.getCameraRotation = function () { + return this.cameraRotation; }; /** - * 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 + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm */ - 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; - } - } - } + 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 min; + // 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; + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var DataView = __webpack_require__(4); + /** - * 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. + * @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 */ - 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; + function Filter(data, column, graph) { + this.data = data; + this.column = column; + this.graph = graph; // the parent graph - 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++; - } - } - } + this.index = undefined; + this.value = undefined; - if (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); - } + // 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); } - return values; - }; + // create an array with the filtered datapoints. this will be loaded afterwards + this.dataPoints = []; - /** - * 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]; + this.loaded = false; + this.onLoadCallback = undefined; - 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"); - } + if (graph.animationPreload) { + this.loaded = false; + this.loadInBackground(); } else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; + this.loaded = true; } + }; - var d = {}; - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); - } - } - this._data[id] = d; - this.length++; - return id; + /** + * Return the label + * @return {string} label + */ + Filter.prototype.isLoaded = function () { + return this.loaded; }; + /** - * 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 + * Return the loaded progress + * @return {Number} percentage between 0 and 100 */ - DataSet.prototype._getItem = function (id, types) { - var field, value; + Filter.prototype.getLoadedProgress = function () { + var len = this.values.length; - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; + var i = 0; + while (this.dataPoints[i]) { + i++; } - // 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; + return Math.round(i / len * 100); }; + /** - * 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 + * Return the label + * @return {string} label */ - 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"); - } + Filter.prototype.getLabel = function () { + return this.graph.filterLabel; + }; - // 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; + /** + * Return the columnIndex of the filter + * @return {Number} columnIndex + */ + Filter.prototype.getColumn = function () { + return this.column; }; /** - * Get an array with the column names of a Google DataTable - * @param {DataTable} dataTable - * @return {String[]} columnNames - * @private + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value */ - 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; + Filter.prototype.getSelectedValue = function () { + if (this.index === undefined) return undefined; + + return this.values[this.index]; }; /** - * Append an item as a row to the dataTable - * @param dataTable - * @param columns - * @param item - * @private + * Retrieve all values of the filter + * @return {Array} values */ - 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]); - } + Filter.prototype.getValues = function () { + return this.values; }; - module.exports = DataSet; + /** + * 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"; -/***/ }, -/* 8 */ -/***/ function(module, exports, __webpack_require__) { + return this.values[index]; + }; - "use strict"; /** - * A queue - * @param {Object} options - * Available options: - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @constructor + * Retrieve the (filtered) dataPoints for the currently selected filter index + * @param {Number} [index] (optional) + * @return {Array} dataPoints */ - function Queue(options) { - // options - this.delay = null; - this.max = Infinity; + Filter.prototype._getDataPoints = function (index) { + if (index === undefined) index = this.index; - // properties - this._queue = []; - this._timeout = null; - this._extended = null; + if (index === undefined) return []; - this.setOptions(options); - } + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; + } else { + var f = {}; + f.column = this.column; + f.value = this.values[index]; - /** - * Update the configuration of the queue - * @param {Object} options - * Available options: - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @param options - */ - Queue.prototype.setOptions = function (options) { - if (options && typeof options.delay !== "undefined") { - this.delay = options.delay; - } - if (options && typeof options.max !== "undefined") { - this.max = options.max; + var dataView = new DataView(this.data, { filter: function (item) { + return item[f.column] == f.value; + } }).get(); + dataPoints = this.graph._getDataPoints(dataView); + + this.dataPoints[index] = dataPoints; } - this._flushIfNeeded(); + return dataPoints; }; - /** - * Extend an object with queuing functionality. - * The object will be extended with a function flush, and the methods provided - * in options.replace will be replaced with queued ones. - * @param {Object} object - * @param {Object} options - * Available options: - * - replace: Array. - * A list with method names of the methods - * on the object to be replaced with queued ones. - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @return {Queue} Returns the created queue - */ - Queue.extend = function (object, options) { - var queue = new Queue(options); - if (object.flush !== undefined) { - throw new Error("Target object already has a property flush"); - } - object.flush = function () { - queue.flush(); - }; - var methods = [{ - name: "flush", - original: undefined - }]; + /** + * Set a callback function when the filter is fully loaded. + */ + Filter.prototype.setOnLoadCallback = function (callback) { + this.onLoadCallback = callback; + }; - if (options && options.replace) { - for (var i = 0; i < options.replace.length; i++) { - var name = options.replace[i]; - methods.push({ - name: name, - original: object[name] - }); - queue.replace(object, name); - } - } - queue._extended = { - object: object, - methods: methods - }; + /** + * 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"; - return queue; + this.index = index; + this.value = this.values[index]; }; /** - * Destroy the queue. The queue will first flush all queued actions, and in - * case it has extended an object, will restore the original object. + * Load all filtered rows in the background one by one + * Start this method without providing an index! */ - Queue.prototype.destroy = function () { - this.flush(); + Filter.prototype.loadInBackground = function (index) { + if (index === undefined) index = 0; - if (this._extended) { - var object = this._extended.object; - var methods = this._extended.methods; - for (var i = 0; i < methods.length; i++) { - var method = methods[i]; - if (method.original) { - object[method.name] = method.original; - } else { - delete object[method.name]; - } + var frame = this.graph.frame; + + if (index < this.values.length) { + var dataPointsTemp = this._getDataPoints(index); + //this.graph.redrawInfo(); // TODO: not neat + + // create a progress box + if (frame.progress === undefined) { + frame.progress = document.createElement("DIV"); + frame.progress.style.position = "absolute"; + frame.progress.style.color = "gray"; + frame.appendChild(frame.progress); } - this._extended = null; + var progress = this.getLoadedProgress(); + frame.progress.innerHTML = "Loading animation... " + progress + "%"; + // TODO: this is no nice solution... + frame.progress.style.bottom = 60 + "px"; // TODO: use height of slider + frame.progress.style.left = 10 + "px"; + + var me = this; + setTimeout(function () { + me.loadInBackground(index + 1); + }, 10); + this.loaded = false; + } else { + this.loaded = true; + + // remove the progress box + if (frame.progress !== undefined) { + frame.removeChild(frame.progress); + frame.progress = undefined; + } + + if (this.onLoadCallback) this.onLoadCallback(); } }; + module.exports = Filter; + +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + /** - * Replace a method on an object with a queued version - * @param {Object} object Object having the method - * @param {string} method The method name + * @prototype Point2d + * @param {Number} [x] + * @param {Number} [y] */ - Queue.prototype.replace = function (object, method) { - var me = this; - var original = object[method]; - if (!original) { - throw new Error("Method " + method + " undefined"); - } + function Point2d(x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + } - object[method] = function () { - // create an Array with the arguments - var args = []; - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; - } + module.exports = Point2d; - // add this call to the queue - me.queue({ - args: args, - fn: original, - context: this - }); - }; +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + /** + * @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; }; /** - * Queue a call - * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry + * Subtract the two provided points, returns a-b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a-b */ - Queue.prototype.queue = function (entry) { - if (typeof entry === "function") { - this._queue.push({ fn: entry }); - } else { - this._queue.push(entry); - } + 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; + }; - this._flushIfNeeded(); + /** + * 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; }; /** - * Check whether the queue needs to be flushed - * @private + * Calculate the average of two 3d points + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} The average, (a+b)/2 */ - Queue.prototype._flushIfNeeded = function () { - // flush when the maximum is exceeded. - if (this._queue.length > this.max) { - this.flush(); - } + Point3d.avg = function (a, b) { + return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); + }; - // flush after a period of inactivity when a delay is configured - clearTimeout(this._timeout); - if (this.queue.length > 0 && typeof this.delay === "number") { - var me = this; - this._timeout = setTimeout(function () { - me.flush(); - }, this.delay); - } + /** + * 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(); + + crossproduct.x = a.y * b.z - a.z * b.y; + crossproduct.y = a.z * b.x - a.x * b.z; + crossproduct.z = a.x * b.y - a.y * b.x; + + return crossproduct; }; + /** - * Flush all queued calls + * Rtrieve the length of the vector (or the distance from this point to the origin + * @return {Number} length */ - Queue.prototype.flush = function () { - while (this._queue.length > 0) { - var entry = this._queue.shift(); - entry.fn.apply(entry.context || entry.fn, entry.args || []); - } + Point3d.prototype.length = function () { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); }; - module.exports = Queue; + module.exports = Point3d; /***/ }, -/* 9 */ +/* 11 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); /** - * 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 Slider * - * @constructor DataView + * An html slider control with start/stop/prev/next buttons + * @param {Element} container The element where the slider will be created + * @param {Object} options Available options: + * {boolean} visible If true (default) the + * slider is visible. */ - function DataView(data, options) { - this._data = null; - this._ids = {}; // ids of the items currently in memory (just contains a boolean true) - this.length = 0; // number of items in the DataView - this._options = options || {}; - this._fieldId = "id"; // name of the field containing id - this._subscribers = {}; // event subscribers + 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; - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + 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); - this.setData(data); - } + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); - /** - * Set a data source for the view - * @param {DataSet | DataView} data - */ - DataView.prototype.setData = function (data) { - var ids, i, len; + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); - if (this._data) { - // unsubscribe from current dataset - if (this._data.unsubscribe) { - this._data.unsubscribe("*", this.listener); - } + 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); - // trigger a remove of all items in memory - ids = []; - for (var id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - ids.push(id); - } - } - this._ids = {}; - this.length = 0; - this._trigger("remove", { items: ids }); + this.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); + }; } - this._data = data; + this.onChangeCallback = undefined; - if (this._data) { - // update fieldId - this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || "id"; + this.values = []; + this.index = undefined; - // trigger an add of all added items - ids = this._data.getIds({ filter: this._options && this._options.filter }); - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - this._ids[id] = true; - } - this.length = ids.length; - this._trigger("add", { items: ids }); + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } - // subscribe to new dataset - if (this._data.on) { - this._data.on("*", this.listener); - } + /** + * Select the previous index + */ + Slider.prototype.prev = function () { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); } }; /** - * Refresh the DataView. Useful when the DataView has a filter function - * containing a variable parameter. + * Select the next index */ - DataView.prototype.refresh = function () { - var id; - var ids = this._data.getIds({ filter: this._options && this._options.filter }); - var newIds = {}; - var added = []; - var removed = []; - - // check for additions - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - newIds[id] = true; - if (!this._ids[id]) { - added.push(id); - this._ids[id] = true; - this.length++; - } - } - - // check for removals - for (id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - if (!newIds[id]) { - removed.push(id); - delete this._ids[id]; - this.length--; - } - } + Slider.prototype.next = function () { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); } - - // trigger events - if (added.length) { - this._trigger("add", { items: added }); - } - if (removed.length) { - this._trigger("remove", { items: removed }); - } - }; + }; /** - * 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 + * Select the next index */ - DataView.prototype.get = function (args) { - var me = this; + Slider.prototype.playNext = function () { + var start = new Date(); - // 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]; + 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); } - // 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); - }; - } + var end = new Date(); + var diff = end - start; - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); + // 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 - return this._data && this._data.get.apply(this._data, getArguments); + var me = this; + this.playTimeout = setTimeout(function () { + me.playNext(); + }, interval); }; /** - * 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 + * Toggle start or stop playing */ - 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 - }); + Slider.prototype.togglePlay = function () { + if (this.playTimeout === undefined) { + this.play(); } else { - ids = []; + this.stop(); } - - 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 + * Start playing */ - DataView.prototype.getDataSet = function () { - var dataSet = this; - while (dataSet instanceof DataView) { - dataSet = dataSet._data; + Slider.prototype.play = function () { + // Test whether already playing + if (this.playTimeout) return; + + this.playNext(); + + if (this.frame) { + this.frame.play.value = "Stop"; } - 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 + * Stop playing */ - DataView.prototype._onEvent = function (event, params, senderId) { - var i, - len, - id, - item, - ids = params && params.items, - data = this._data, - added = [], - updated = [], - removed = []; + Slider.prototype.stop = function () { + clearInterval(this.playTimeout); + this.playTimeout = undefined; - if (ids && data) { - switch (event) { - case "add": - // filter the ids of the added items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - if (item) { - this._ids[id] = true; - added.push(id); - } - } + if (this.frame) { + this.frame.play.value = "Play"; + } + }; - break; + /** + * 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; + }; - 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); + /** + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds + */ + Slider.prototype.setPlayInterval = function (interval) { + this.playInterval = interval; + }; - 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 {} - } - } + /** + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds + */ + Slider.prototype.getPlayInterval = function (interval) { + return this.playInterval; + }; - break; + /** + * Set looping on or off + * @pararm {boolean} doLoop If true, the slider will jump to the start when + * the end is passed, and will jump to the end + * when the start is passed. + */ + Slider.prototype.setPlayLoop = function (doLoop) { + this.playLoop = doLoop; + }; - 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; - } + /** + * Execute the onchange callback function + */ + Slider.prototype.onChange = function () { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } + }; - this.length += added.length - removed.length; + /** + * 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"; - 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); - } + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = left + "px"; } }; - // 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; - // nothing interesting for me :-( + /** + * 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; -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { + if (this.values.length > 0) this.setIndex(0);else this.index = undefined; + }; - "use strict"; + /** + * Select a value by its index + * @param {Number} index + */ + Slider.prototype.setIndex = function (index) { + if (index < this.values.length) { + this.index = index; - var Emitter = __webpack_require__(11); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); - var util = __webpack_require__(1); - var Point3d = __webpack_require__(12); - var Point2d = __webpack_require__(13); - var Camera = __webpack_require__(14); - var Filter = __webpack_require__(15); - var Slider = __webpack_require__(16); - var StepNumber = __webpack_require__(17); + this.redraw(); + this.onChange(); + } else { + throw "Error: index out of range"; + } + }; /** - * @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] + * retrieve the index of the currently selected vaue + * @return {Number} index */ - function Graph3d(container, data, options) { - if (!(this instanceof Graph3d)) { - throw new SyntaxError("Constructor must be called with the new operator"); - } + Slider.prototype.getIndex = function () { + return this.index; + }; - // 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"; + /** + * retrieve the currently selected value + * @return {*} value + */ + Slider.prototype.get = function () { + return this.values[this.index]; + }; - var passValueFn = function (v) { - return v; - }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; - this.filterLabel = "time"; - this.legendLabel = "value"; + 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; - 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.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + this.frame.style.cursor = "move"; - this.camera = new Camera(); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + // 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); + }; - 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; + Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + var x = left - 3; - 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 + 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; - // constants - this.colorAxis = "#4D4D4D"; - this.colorGrid = "#D3D3D3"; - this.colorDot = "#7DC1FF"; - this.colorDotBorder = "#3267D2"; + return index; + }; - // create a frame and canvas - this.create(); + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - // apply options (also when undefined) - this.setOptions(options); + var x = index / (this.values.length - 1) * width; + var left = x + 3; - // apply data - if (data) { - this.setData(data); - } - } + return left; + }; - // 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; - } - } + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; - // scale the vertical axis - this.scale.z *= this.verticalRatio; - // TODO: can this be automated? verticalRatio? + var index = this.leftToIndex(x); - // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); + this.setIndex(index); - // 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); + util.preventDefault(); }; - /** - * 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, + Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = "auto"; + // remove event listeners + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); - // 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), + util.preventDefault(); + }; + module.exports = Slider; - // 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)); +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { - return new Point3d(dx, dy, dz); - }; + "use strict"; /** - * 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 + * @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, ...) */ - 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()); - } + function StepNumber(start, end, step, prettyStep) { + // set default values + this._start = 0; + this._end = 0; + this._step = 1; + this.prettyStep = true; + this.precision = 5; - // 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); + this._current = 0; + this.setRange(start, end, step, prettyStep); }; /** - * Set the background styling for the graph - * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor + * 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, ...) */ - 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) {} 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"; - }; - + StepNumber.prototype.setRange = function (start, end, step, prettyStep) { + this._start = start ? start : 0; + this._end = end ? end : 0; - /// 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 + this.setStep(step, prettyStep); }; /** - * 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 + * 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, ...) */ - 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; - } + StepNumber.prototype.setStep = function (step, prettyStep) { + if (step === undefined || step <= 0) return; - return -1; + if (prettyStep !== undefined) this.prettyStep = prettyStep; + + if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; }; /** - * Determine the indexes of the data columns, based on the given style and data - * @param {DataSet} data - * @param {Number} style + * 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 */ - 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; + StepNumber.calculatePrettyStep = function (step) { + var log10 = function (x) { + return Math.log(x) / Math.LN10; + }; - 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; + // 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))); - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; - } - } else { - throw "Unknown style \"" + this.style + "\""; + // choose the best step (closest to minimum step) + var prettyStep = step1; + if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; + if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; + + // for safety + if (prettyStep <= 0) { + prettyStep = 1; } - }; - Graph3d.prototype.getNumberOfRows = function (data) { - return data.length; + return prettyStep; }; - - Graph3d.prototype.getNumberOfColumns = function (data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; - } - } - return counter; + /** + * returns the current value of the step + * @return {Number} current value + */ + StepNumber.prototype.getCurrent = function () { + return parseFloat(this._current.toPrecision(this.precision)); }; - - 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; + /** + * returns the current step size + * @return {Number} current step size + */ + StepNumber.prototype.getStep = function () { + return this._step; }; + /** + * 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; + }; - 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; + /** + * Do a step, add the step size to the current value + */ + StepNumber.prototype.next = function () { + this._current += this._step; }; /** - * 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 + * Returns true whether the end is reached + * @return {boolean} True if the current value has passed the end value. */ - Graph3d.prototype._dataInitialize = function (rawData, style) { - var me = this; + StepNumber.prototype.end = function () { + return this._current > this._end; + }; - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off("*", this._onChange); - } + module.exports = StepNumber; - if (rawData === undefined) return; +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); + "use strict"; + + var Emitter = __webpack_require__(62); + var Hammer = __webpack_require__(41); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var Range = __webpack_require__(17); + var Core = __webpack_require__(42); + var TimeAxis = __webpack_require__(35); + var CurrentTime = __webpack_require__(26); + var CustomTime = __webpack_require__(27); + var ItemSet = __webpack_require__(32); + + /** + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [items] + * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [groups] + * @param {Object} [options] See Timeline.setOptions for the available options. + * @constructor + * @extends Core + */ + function Timeline(container, items, groups, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError("Constructor must be called with the new operator"); } - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } else { - throw new Error("Array, DataSet, or DataView expected"); + // if the third element is options, the forth is groups (optionally); + if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { + var forthArgument = options; + options = groups; + groups = forthArgument; } - if (data.length == 0) return; + var me = this; + this.defaultOptions = { + start: null, + end: null, - this.dataSet = rawData; - this.dataTable = data; + autoResize: true, - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); + orientation: "bottom", + width: null, + height: null, + maxHeight: null, + minHeight: null }; - this.dataSet.on("*", this._onChange); - - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange + this.options = util.deepExtend({}, this.defaultOptions); - // 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"; + // Create the DOM, props, and emitter + this._create(container); + // all components listed here will be repainted automatically + this.components = []; + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + hiddenDates: [], + util: { + getScale: function () { + return me.timeAxis.step.scale; + }, + getStep: function () { + return me.timeAxis.step.step; + }, - // 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(); - }); + 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 withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - // 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; - } + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; - } else { - var dataY = this.getDistinctValues(data, this.colY); - this.yBarWidth = dataY[1] - dataY[0] || 1; - } - } + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - // 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; + // 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); - 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; + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); - 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; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - 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; + // apply options + if (options) { + this.setOptions(options); } - // set the scale dependent on the ranges. - this._setScale(); - }; + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } + } + // Extend the functionality from Core + Timeline.prototype = new Core(); /** - * Filter the data based on the current filter - * @param {Array} data - * @return {Array} dataPoints Array with point objects which can be drawn on screen + * Force a redraw. The size of all items will be recalculated. + * Can be useful to manually redraw when option autoResize=false and the window + * has been resized, or when the items CSS has been changed. */ - Graph3d.prototype._getDataPoints = function (data) { - // TODO: store the created matrix dataPoints in the filters instead of reloading each time - var x, y, i, z, obj, point; + Timeline.prototype.redraw = function () { + this.itemSet && this.itemSet.markDirty({ refreshItems: true }); + this._redraw(); + }; - var dataPoints = []; + /** + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + */ + Timeline.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - 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 + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; + } else { + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: "Date", + end: "Date" + } + }); + } - // create 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; + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + if (this.options.start == undefined || this.options.end == undefined) { + var dataRange = this._getDataRange(); } - } - - var sortNumber = function (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; + var start = this.options.start != undefined ? this.options.start : dataRange.start; + var end = this.options.end != undefined ? this.options.end : dataRange.end; - dataPoints.push(obj); + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); } } - - 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. + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - 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); + 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); } - 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); + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); }; - /** - * 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%') + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {string[] | string} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + * @param {Object} [options] Available options: + * `focus: boolean` + * If true, focus will be set to the selected item(s) + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. + * Only applicable when option focus is true. */ - Graph3d.prototype.setSize = function (width, height) { - this.frame.style.width = width; - this.frame.style.height = height; + Timeline.prototype.setSelection = function (ids, options) { + this.itemSet && this.itemSet.setSelection(ids); - this._resizeCanvas(); + if (options && options.focus) { + this.focus(ids, options); + } }; /** - * Resize the canvas to the current size of the frame + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ - 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"; + Timeline.prototype.getSelection = function () { + return this.itemSet && this.itemSet.getSelection() || []; }; /** - * Start animation + * Adjust the visible window such that the selected item (or multiple items) + * are centered on screen. + * @param {String | String[]} id An item id or array with item ids + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. + * Only applicable when option focus is true */ - Graph3d.prototype.animationStart = function () { - if (!this.frame.filter || !this.frame.filter.slider) throw "No animation available"; + Timeline.prototype.focus = function (id, options) { + if (!this.itemsData || id == undefined) return; - this.frame.filter.slider.play(); - }; + var ids = Array.isArray(id) ? id : [id]; + // get the specified item(s) + var itemsData = this.itemsData.getDataSet().get(ids, { + type: { + start: "Date", + end: "Date" + } + }); - /** - * Stop animation - */ - Graph3d.prototype.animationStop = function () { - if (!this.frame.filter || !this.frame.filter.slider) return; + // calculate minimum start and maximum end of specified items + var start = null; + var end = null; + itemsData.forEach(function (itemData) { + var s = itemData.start.valueOf(); + var e = "end" in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); - this.frame.filter.slider.stop(); - }; + if (start === null || s < start) { + start = s; + } + if (end === null || e > end) { + end = e; + } + }); - /** - * Resize the center position based on the current values in this.defaultXCenter - * and this.defaultYCenter (which are strings with a percentage or a value - * in pixels). The center positions are the variables this.xCenter - * and this.yCenter - */ - Graph3d.prototype._resizeCenter = function () { - // calculate the horizontal center position - if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === "%") { - this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; - } else { - this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px - } + if (start !== null && end !== null) { + // calculate the new middle and interval for the window + var middle = (start + end) / 2; + var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); - // 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 + var animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(middle - interval / 2, middle + interval / 2, animate); } }; /** - * 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. + * 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 */ - Graph3d.prototype.setCameraPosition = function (pos) { - if (pos === undefined) { - return; - } + Timeline.prototype.getItemRange = function () { + // calculate min from start filed + var dataset = this.itemsData.getDataSet(), + min = null, + max = null; - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } + 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 - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); + // 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()); + } + } } - this.redraw(); + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; }; - /** - * 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); + module.exports = Timeline; +/***/ }, +/* 14 */ +/***/ 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); - } + "use strict"; - // draw the filter - this._redrawFilter(); - }; + var Emitter = __webpack_require__(62); + var Hammer = __webpack_require__(41); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var Range = __webpack_require__(17); + var Core = __webpack_require__(42); + var TimeAxis = __webpack_require__(35); + var CurrentTime = __webpack_require__(26); + var CustomTime = __webpack_require__(27); + var LineGraph = __webpack_require__(34); /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Graph2d.setOptions for the available options. + * @constructor + * @extends Core */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); - - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); + function Graph2d(container, items, groups, options) { + // if the third element is options, the forth is groups (optionally); + if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) { + var forthArgument = options; + options = groups; + groups = forthArgument; } - }; - /** - * Update the options. Options will be merged with current options - * @param {Object} options - */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; - - this.animationStop(); + var me = this; + this.defaultOptions = { + start: null, + end: null, - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + autoResize: true, - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + orientation: "bottom", + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - 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; + // Create the DOM, props, and emitter + this._create(container); - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; + // all components listed here will be repainted automatically + this.components = []; - if (options.style !== undefined) { - var styleNumber = this._getStyleNumber(options.style); - if (styleNumber !== -1) { - this.style = styleNumber; - } + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) + }, + hiddenDates: [], + util: { + toScreen: me._toScreen.bind(me), + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime: me._toGlobalTime.bind(me) } - 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; + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + //this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - 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; + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + // 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); - if (cameraPosition !== undefined) { - this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical); - this.camera.setArmLength(cameraPosition.distance); - } else { - this.camera.setArmRotation(1, 0.5); - this.camera.setArmLength(1.7); - } - } + // item set + this.linegraph = new LineGraph(this.body); + this.components.push(this.linegraph); - this._setBackgroundColor(options && options.backgroundColor); + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - this.setSize(this.width, this.height); + // apply options + if (options) { + this.setOptions(options); + } - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); } - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); } - }; + } + + // Extend the functionality from Core + Graph2d.prototype = new Core(); /** - * Redraw the Graph. + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - Graph3d.prototype.redraw = function () { - if (this.dataPoints === undefined) { - throw "Error: graph data not initialized"; - } - - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); + Graph2d.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - 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(); + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; } else { - // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE - this._redrawDataDot(); + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: "Date", + end: "Date" + } + }); } - this._redrawInfo(); - this._redrawLegend(); + // set items + this.itemsData = newDataSet; + this.linegraph && this.linegraph.setItems(newDataSet); + + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + var start = this.options.start != undefined ? this.options.start : null; + var end = this.options.end != undefined ? this.options.end : null; + + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); + } + } }; /** - * Clear the canvas before redrawing + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - Graph3d.prototype._redrawClear = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); + Graph2d.prototype.setGroups = function (groups) { + // convert to type DataSet when needed + var newDataSet; + if (!groups) { + newDataSet = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + newDataSet = groups; + } else { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } - ctx.clearRect(0, 0, canvas.width, canvas.height); + this.groupsData = newDataSet; + this.linegraph.setGroups(newDataSet); }; + /** + * Returns an object containing an SVG element with the icon of the group (size determined by iconWidth and iconHeight), the label of the group (content) and the yAxisOrientation of the group (left or right). + * @param groupId + * @param width + * @param height + */ + Graph2d.prototype.getLegend = function (groupId, width, height) { + if (width === undefined) { + width = 15; + } + if (height === undefined) { + height = 15; + } + if (this.linegraph.groups[groupId] !== undefined) { + return this.linegraph.groups[groupId].getLegend(width, height); + } else { + return "cannot find group:" + groupId; + } + }; /** - * Redraw the legend showing the colors + * This checks if the visible option of the supplied group (by ID) is true or false. + * @param groupId + * @returns {*} */ - Graph3d.prototype._redrawLegend = function () { - var y; + Graph2d.prototype.isGroupVisible = function (groupId) { + if (this.linegraph.groups[groupId] !== undefined) { + return this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true); + } else { + return false; + } + }; - if (this.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 - } + /** + * Get the data range of the item set. + * @returns {{min: Date, max: Date}} range A range with a start and end Date. + * When no minimum is found, min==null + * When no maximum is found, max==null + */ + Graph2d.prototype.getItemRange = function () { + var min = null; + var max = null; - 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; + // calculate min from start filed + for (var groupId in this.linegraph.groups) { + if (this.linegraph.groups.hasOwnProperty(groupId)) { + if (this.linegraph.groups[groupId].visible == true) { + for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) { + var item = this.linegraph.groups[groupId].itemsData[i]; + var value = util.convert(item.x, "Date").valueOf(); + min = min == null ? value : min > value ? value : min; + max = max == null ? value : max < value ? value : max; + } + } + } } - 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); + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; + }; - //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); - } + module.exports = Graph2d; - 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(); - } +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { - 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; + "use strict"; - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + /** + * Created by Alex on 10/3/2014. + */ + var moment = __webpack_require__(40); - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.colorAxis; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - step.next(); + /** + * used in Core to convert the options into a volatile variable + * + * @param Core + */ + exports.convertHiddenOptions = function (body, hiddenDates) { + body.hiddenDates = []; + if (hiddenDates) { + if (Array.isArray(hiddenDates) == true) { + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].repeat === undefined) { + var dateItem = {}; + dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); + dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); + body.hiddenDates.push(dateItem); + } + } + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time } - - ctx.textAlign = "right"; - ctx.textBaseline = "top"; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); } }; + /** - * Redraw the filter + * create new entrees for the repeating hidden dates + * @param body + * @param hiddenDates */ - Graph3d.prototype._redrawFilter = function () { - this.frame.filter.innerHTML = ""; + exports.updateHiddenDates = function (body, hiddenDates) { + if (hiddenDates && body.domProps.centerContainer.width !== undefined) { + exports.convertHiddenOptions(body, hiddenDates); - if (this.dataFilter) { - var options = { - visible: this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; + var start = moment(body.range.start); + var end = moment(body.range.end); - // TODO: css here is not nice here... - this.frame.filter.style.padding = "10px"; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; + var totalRange = body.range.end - body.range.start; + var pixelTime = totalRange / body.domProps.centerContainer.width; - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].repeat !== undefined) { + var startDate = moment(hiddenDates[i].start); + var endDate = moment(hiddenDates[i].end); - // create an event handler - var me = this; - var onchange = function () { - var index = slider.getIndex(); + if (startDate._d == "Invalid Date") { + throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); + } + if (endDate._d == "Invalid Date") { + throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); + } - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + var duration = endDate - startDate; + if (duration >= 4 * pixelTime) { + var offset = 0; + var runUntil = end.clone(); + switch (hiddenDates[i].repeat) { + case "daily": + // case of time + if (startDate.day() != endDate.day()) { + offset = 1; + } + startDate.dayOfYear(start.dayOfYear()); + startDate.year(start.year()); + startDate.subtract(7, "days"); - me.redraw(); - }; - slider.setOnChangeCallback(onchange); - } else { - this.frame.filter.slider = undefined; + endDate.dayOfYear(start.dayOfYear()); + endDate.year(start.year()); + endDate.subtract(7 - offset, "days"); + + runUntil.add(1, "weeks"); + break; + case "weekly": + var dayOffset = endDate.diff(startDate, "days"); + var day = startDate.day(); + + // set the start date to the range.start + startDate.date(start.date()); + startDate.month(start.month()); + startDate.year(start.year()); + endDate = startDate.clone(); + + // force + startDate.day(day); + endDate.day(day); + endDate.add(dayOffset, "days"); + + startDate.subtract(1, "weeks"); + endDate.subtract(1, "weeks"); + + runUntil.add(1, "weeks"); + break; + case "monthly": + if (startDate.month() != endDate.month()) { + offset = 1; + } + startDate.month(start.month()); + startDate.year(start.year()); + startDate.subtract(1, "months"); + + endDate.month(start.month()); + endDate.year(start.year()); + endDate.subtract(1, "months"); + endDate.add(offset, "months"); + + runUntil.add(1, "months"); + break; + case "yearly": + if (startDate.year() != endDate.year()) { + offset = 1; + } + startDate.year(start.year()); + startDate.subtract(1, "years"); + endDate.year(start.year()); + endDate.subtract(1, "years"); + endDate.add(offset, "years"); + + runUntil.add(1, "years"); + break; + default: + console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); + return; + } + while (startDate < runUntil) { + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); + switch (hiddenDates[i].repeat) { + case "daily": + startDate.add(1, "days"); + endDate.add(1, "days"); + break; + case "weekly": + startDate.add(1, "weeks"); + endDate.add(1, "weeks"); + break; + case "monthly": + startDate.add(1, "months"); + endDate.add(1, "months"); + break; + case "yearly": + startDate.add(1, "y"); + endDate.add(1, "y"); + break; + default: + console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); + return; + } + } + body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); + } + } + } + // remove duplicates, merge where possible + exports.removeDuplicates(body); + // ensure the new positions are not on hidden dates + var startHidden = exports.isHidden(body.range.start, body.hiddenDates); + var endHidden = exports.isHidden(body.range.end, body.hiddenDates); + var rangeStart = body.range.start; + var rangeEnd = body.range.end; + if (startHidden.hidden == true) { + rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; + } + if (endHidden.hidden == true) { + rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; + } + if (startHidden.hidden == true || endHidden.hidden == true) { + body.range._applyRange(rangeStart, rangeEnd); + } } }; + /** - * Redraw the slider + * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. + * Scales with N^2 + * @param body */ - Graph3d.prototype._redrawSlider = function () { - if (this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); + exports.removeDuplicates = function (body) { + var hiddenDates = body.hiddenDates; + var safeDates = []; + for (var i = 0; i < hiddenDates.length; i++) { + for (var j = 0; j < hiddenDates.length; j++) { + if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { + // j inside i + if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[j].remove = true; + } + // j start inside i + else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { + hiddenDates[i].end = hiddenDates[j].end; + hiddenDates[j].remove = true; + } + // j end inside i + else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[i].start = hiddenDates[j].start; + hiddenDates[j].remove = true; + } + } + } + } + + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].remove !== true) { + safeDates.push(hiddenDates[i]); + } } + + body.hiddenDates = safeDates; + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time }; + exports.printDates = function (dates) { + for (var i = 0; i < dates.length; i++) { + console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); + } + }; /** - * Redraw common information + * Used in TimeStep to avoid the hidden times. + * @param timeStep + * @param previousTime */ - Graph3d.prototype._redrawInfo = function () { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); + exports.stepOverHiddenDates = function (timeStep, previousTime) { + var stepInHidden = false; + var currentValue = timeStep.current.valueOf(); + for (var i = 0; i < timeStep.hiddenDates.length; i++) { + var startDate = timeStep.hiddenDates[i].start; + var endDate = timeStep.hiddenDates[i].end; + if (currentValue >= startDate && currentValue < endDate) { + stepInHidden = true; + break; + } + } - ctx.font = "14px arial"; // TODO: put in options - ctx.lineStyle = "gray"; - ctx.fillStyle = "gray"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; + if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { + var prevValue = moment(previousTime); + var newValue = moment(endDate); + //check if the next step should be major + if (prevValue.year() != newValue.year()) { + timeStep.switchedYear = true; + } else if (prevValue.month() != newValue.month()) { + timeStep.switchedMonth = true; + } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { + timeStep.switchedDay = true; + } - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); + timeStep.current = newValue.toDate(); } }; + ///** + // * Used in TimeStep to avoid the hidden times. + // * @param timeStep + // * @param previousTime + // */ + //exports.checkFirstStep = function(timeStep) { + // var stepInHidden = false; + // var currentValue = timeStep.current.valueOf(); + // for (var i = 0; i < timeStep.hiddenDates.length; i++) { + // var startDate = timeStep.hiddenDates[i].start; + // var endDate = timeStep.hiddenDates[i].end; + // if (currentValue >= startDate && currentValue < endDate) { + // stepInHidden = true; + // break; + // } + // } + // + // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) { + // var newValue = moment(endDate); + // timeStep.current = newValue.toDate(); + // } + //}; + /** - * Redraw the axis + * replaces the Core toScreen methods + * @param Core + * @param time + * @param width + * @returns {number} */ - 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; + exports.toScreen = function (Core, time, width) { + if (Core.body.hiddenDates.length == 0) { + var conversion = Core.range.conversion(width); + return (time.valueOf() - conversion.offset) * conversion.scale; + } else { + var hidden = exports.isHidden(time, Core.body.hiddenDates); + if (hidden.hidden == true) { + time = hidden.startDate; + } - // 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(); + var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); - 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(); + var conversion = Core.range.conversion(width, duration); + return (time.valueOf() - conversion.offset) * conversion.scale; + } + }; - 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(" " + this.xValueLabel(step.getCurrent()) + " ", text.x, text.y); + /** + * Replaces the core toTime methods + * @param body + * @param range + * @param x + * @param width + * @returns {Date} + */ + exports.toTime = function (Core, x, width) { + if (Core.body.hiddenDates.length == 0) { + var conversion = Core.range.conversion(width); + return new Date(x / conversion.scale + conversion.offset); + } else { + var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + var totalDuration = Core.range.end - Core.range.start - hiddenDuration; + var partialDuration = totalDuration * x / width; + var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); - step.next(); + var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); + return newTime; } + }; - // 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(); + /** + * Support function + * + * @param hiddenDates + * @param range + * @returns {number} + */ + exports.getHiddenDurationBetween = function (hiddenDates, start, end) { + var duration = 0; + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= start && endDate < end) { + duration += endDate - startDate; } + } + return duration; + }; - 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(" " + this.yValueLabel(step.getCurrent()) + " ", text.x, text.y); - step.next(); - } + /** + * Support function + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} + */ + exports.correctTimeForHidden = function (hiddenDates, range, time) { + time = moment(time).toDate().valueOf(); + time -= exports.getHiddenDurationBefore(hiddenDates, range, time); + return time; + }; - // 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(); + exports.getHiddenDurationBefore = function (hiddenDates, range, time) { + var timeOffset = 0; + time = moment(time).toDate().valueOf(); - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.colorAxis; - ctx.fillText(this.zValueLabel(step.getCurrent()) + " ", from.x - 5, from.y); + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= range.start && endDate < range.end) { + if (time >= endDate) { + timeOffset += endDate - startDate; + } + } + } + return timeOffset; + }; - step.next(); + /** + * sum the duration from start to finish, including the hidden duration, + * until the required amount has been reached, return the accumulated hidden duration + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} + */ + exports.getAccumulatedHiddenDuration = function (hiddenDates, range, requiredDuration) { + var hiddenDuration = 0; + var duration = 0; + var previousPoint = range.start; + //exports.printDates(hiddenDates) + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= range.start && endDate < range.end) { + duration += startDate - previousPoint; + previousPoint = endDate; + if (duration >= requiredDuration) { + break; + } else { + hiddenDuration += endDate - startDate; + } + } } - 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(); + return hiddenDuration; + }; - // 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"; - } - 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"; - } else if (Math.sin(armAngle * 2) > 0) { - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; + /** + * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true + * @param hiddenDates + * @param time + * @param direction + * @param correctionEnabled + * @returns {*} + */ + exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { + var isHidden = exports.isHidden(time, hiddenDates); + if (isHidden.hidden == true) { + if (direction < 0) { + if (correctionEnabled == true) { + return isHidden.startDate - (isHidden.endDate - time) - 1; + } else { + return isHidden.startDate - 1; + } } else { - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; + if (correctionEnabled == true) { + return isHidden.endDate + (time - isHidden.startDate) + 1; + } else { + return isHidden.endDate + 1; + } } - ctx.fillStyle = this.colorAxis; - ctx.fillText(yLabel, text.x, text.y); + } else { + return time; } + }; - // 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); + + /** + * Check if a time is hidden + * + * @param time + * @param hiddenDates + * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} + */ + exports.isHidden = function (time, hiddenDates) { + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + + if (time >= startDate && time < endDate) { + // if the start is entering a hidden zone + return { hidden: true, startDate: startDate, endDate: endDate }; + break; + } } + return { hidden: false, startDate: startDate, endDate: endDate }; }; +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + /** - * 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 + * @constructor DataStep + * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an + * end data point. The class itself determines the best scale (step size) based on the + * provided start Date, end Date, and minimumStep. + * + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * + * Alternatively, you can set a scale by hand. + * After creation, you can initialize the class by executing first(). Then you + * can iterate from the start date to the end date via next(). You can check if + * the end date is reached with the function hasNext(). After each step, you can + * retrieve the current date via getCurrent(). + * The DataStep has scales ranging from milliseconds, seconds, minutes, hours, + * days, to years. + * + * Version: 1.2 + * + * @param {Date} [start] The start date, for example new Date(2010, 9, 21) + * or new Date(2010, 9, 21, 23, 45, 00) + * @param {Date} [end] The end date + * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ - Graph3d.prototype._hsv2rgb = function (H, S, V) { - var R, G, B, C, Hi, X; + function DataStep(start, end, minimumStep, containerHeight, customRange, alignZeros) { + // variables + this.current = 0; - C = V * S; - Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(H / 60 % 2 - 1)); + this.autoScale = true; + this.stepIndex = 0; + this.step = 1; + this.scale = 1; - 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; + this.marginStart; + this.marginEnd; + this.deadSpace = 0; - default: - R = 0;G = 0;B = 0;break; - } + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; + + this.alignZeros = alignZeros; + + this.setRange(start, end, minimumStep, containerHeight, customRange); + } - return "RGB(" + parseInt(R * 255) + "," + parseInt(G * 255) + "," + parseInt(B * 255) + ")"; - }; /** - * Draw all datapoints as a grid - * This function can be used when the style is 'grid' + * 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 */ - 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; + DataStep.prototype.setRange = function (start, end, minimumStep, containerHeight, customRange) { + this._start = customRange.min === undefined ? start : customRange.min; + this._end = customRange.max === undefined ? end : customRange.max; + if (this._start == this._end) { + this._start -= 0.75; + this._end += 1; + } - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + if (this.autoScale == true) { + this.setMinimumStep(minimumStep, containerHeight); + } - // 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.setFirst(customRange); + }; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + /** + * 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); - // calculate the translation of the point at the bottom (needed for sorting) - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + var minorStepIdx = -1; + var magnitudefactor = Math.pow(10, orderOfMagnitude); + + var start = 0; + if (orderOfMagnitude < 0) { + start = orderOfMagnitude; } - // 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 (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; + var solutionFound = false; + for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { + magnitudefactor = Math.pow(10, i); + for (var j = 0; j < this.minorSteps.length; j++) { + var stepSize = magnitudefactor * this.minorSteps[j]; + if (stepSize >= minimumStepValue) { + solutionFound = true; + minorStepIdx = j; + break; + } + } + if (solutionFound == true) { + break; + } + } + this.stepIndex = minorStepIdx; + this.scale = magnitudefactor; + this.step = magnitudefactor * this.minorSteps[minorStepIdx]; + }; - if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) { - 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) - topSideVisible = crossproduct.z > 0; - } else { - topSideVisible = true; - } - if (topSideVisible) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; - s = 1; // saturation + /** + * 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 = {}; + } - 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; + 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; - 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(); - } - } - } 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; + this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; + this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; - if (point !== undefined) { - if (this.showPerspective) { - lineWidth = 2 / -point.trans.z; - } else { - lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); - } - } + // if we need to align the zero's we need to make sure that there is a zero to use. + if (this.alignZeros == true && (this.marginEnd - this.marginStart) % this.step != 0) { + this.marginEnd += this.marginEnd % this.step; + } - 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; + this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; + this.marginRange = this.marginEnd - this.marginStart; - 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; + this.current = this.marginEnd; + }; - 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(); - } - } + DataStep.prototype.roundToMinor = function (value) { + var rounded = value - value % (this.scale * this.minorSteps[this.stepIndex]); + if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) { + return rounded + this.scale * this.minorSteps[this.stepIndex]; + } else { + return rounded; } }; /** - * Draw all datapoints as dots. - * This function can be used when the style is 'dot' or 'dot-line' + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date */ - 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? + DataStep.prototype.hasNext = function () { + return this.current >= this.marginStart; + }; - // 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; + /** + * Do the next step + */ + DataStep.prototype.next = function () { + var prev = this.current; + this.current -= this.step; - // 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; + // safety mechanism: if current time is still unchanged, move to the end + if (this.current == prev) { + this.current = this._end; } + }; - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + /** + * Do the next step + */ + DataStep.prototype.previous = function () { + this.current += this.step; + this.marginEnd += this.step; + this.marginRange = this.marginEnd - this.marginStart; + }; - // 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(); - } - // 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; - } + /** + * Get the current datetime + * @return {String} current The current date + */ + DataStep.prototype.getCurrent = function (decimals) { + // prevent round-off errors when close to zero + var current = Math.abs(this.current) < this.step / 2 ? 0 : this.current; + var toPrecision = "" + Number(current).toPrecision(5); - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } else { - radius = size * -(this.eye.z / this.camera.getArmLength()); + // If decimals is specified, then limit or extend the string as required + if (decimals !== undefined && !isNaN(Number(decimals))) { + // If string includes exponent, then we need to add it to the end + var exp = ""; + var index = toPrecision.indexOf("e"); + if (index != -1) { + // Get the exponent + exp = toPrecision.slice(index); + // Remove the exponent in case we need to zero-extend + toPrecision = toPrecision.slice(0, index); } - if (radius < 0) { - radius = 0; + index = Math.max(toPrecision.indexOf(","), toPrecision.indexOf(".")); + if (index === -1) { + // No decimal found - if we want decimals, then we need to add it + if (decimals !== 0) { + toPrecision += "."; + } + // Calculate how long the string should be + index = toPrecision.length + decimals; + } else if (decimals !== 0) { + // Calculate how long the string should be - accounting for the decimal place + index += decimals + 1; } - - 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; + if (index > toPrecision.length) { + // We need to add zeros! + for (var cnt = index - toPrecision.length; cnt > 0; cnt--) { + toPrecision += "0"; + } } 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); + // we need to remove characters + toPrecision = toPrecision.slice(0, index); + } + // Add the exponent if there is one + toPrecision += exp; + } else { + if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) { + // If no decimal is specified, and there are decimal places, remove trailing zeros + for (var i = toPrecision.length - 1; i > 0; i--) { + if (toPrecision[i] == "0") { + toPrecision = toPrecision.slice(0, i); + } else if (toPrecision[i] == "." || toPrecision[i] == ",") { + toPrecision = toPrecision.slice(0, i); + break; + } else { + break; + } + } } - - // draw the circle - ctx.lineWidth = 1; - 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(); } + + return toPrecision; }; /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' + * 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. */ - Graph3d.prototype._redrawDataBar = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); - var i, j, surface, corners; + DataStep.prototype.isMajor = function () { + return this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0; + }; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + module.exports = DataStep; - // 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; +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { - // calculate the distance from the point at the bottom to the camera - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } + "use strict"; - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + var util = __webpack_require__(1); + var hammerUtil = __webpack_require__(43); + var moment = __webpack_require__(40); + var Component = __webpack_require__(25); + var DateUtil = __webpack_require__(15); - // 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]; + /** + * @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(-3, "days").valueOf(); // Number + this.end = now.clone().add(4, "days").valueOf(); // Number - // 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); - } + this.body = body; + this.deltaDifference = 0; + this.scaleOffset = 0; + this.startToFront = false; + this.endToFront = true; - // 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); - } + // 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); - // 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) }]; + this.props = { + touch: {} + }; + this.animateTimer = null; - // 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); - }); + // drag listeners for dragging + this.body.emitter.on("panstart", this._onDragStart.bind(this)); + this.body.emitter.on("panmove", this._onDrag.bind(this)); + this.body.emitter.on("panend", this._onDragEnd.bind(this)); - // 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; + // ignore dragging when holding + this.body.emitter.on("press", this._onHold.bind(this)); - // 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}) - } + // mouse wheel for zooming + this.body.emitter.on("mousewheel", this._onMouseWheel.bind(this)); - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + // pinch to zoom + this.body.emitter.on("touch", this._onTouch.bind(this)); + this.body.emitter.on("pinch", this._onPinch.bind(this)); - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + this.setOptions(options); + } - // both are equal - return 0; - }); + Range.prototype = new Component(); - // 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(); + /** + * Set options for the range controller + * @param {Object} options Available options: + * {Number | Date | String} start Start date for the range + * {Number | Date | String} end End date for the range + * {Number} min Minimum value for start + * {Number} max Maximum value for end + * {Number} zoomMin Set a minimum value for + * (end - start). + * {Number} zoomMax Set a maximum value for + * (end - start). + * {Boolean} moveable Enable moving of the range + * by dragging. True by default + * {Boolean} zoomable Enable zooming of the range + * by pinching/scrolling. True by default + */ + Range.prototype.setOptions = function (options) { + if (options) { + // copy the options that we know + var fields = ["direction", "min", "max", "zoomMin", "zoomMax", "moveable", "zoomable", "activate", "hiddenDates"]; + util.selectiveExtend(fields, this.options, options); + + if ("start" in options || "end" in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); } } }; - /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - Graph3d.prototype._redrawDataLine = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext("2d"), - point, - 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; - } - - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; - - ctx.lineWidth = 1; // TODO: make customizable - ctx.strokeStyle = "blue"; // TODO: make customizable - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - } - - // 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); + function validateDirection(direction) { + if (direction != "horizontal" && direction != "vertical") { + throw new TypeError("Unknown direction \"" + direction + "\". " + "Choose \"horizontal\" or \"vertical\"."); } - - // finish the line - if (this.dataPoints.length > 0) { - ctx.stroke(); - } - }; + } /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) + * Set a new start and end range + * @param {Date | Number | String} [start] + * @param {Date | Number | String} [end] + * @param {boolean | number} [animate=false] If true, the range is animated + * smoothly to the new window. + * If animate is a number, the + * number is taken as duration + * Default duration is 500 ms. + * @param {Boolean} [byUser=false] + * */ - Graph3d.prototype._onMouseDown = function (event) { - event = event || window.event; - - // check if mouse is still down (may be up when focus is lost for example - // in an iframe) - if (this.leftButtonDown) { - this._onMouseUp(event); + Range.prototype.setRange = function (start, end, animate, byUser) { + if (byUser !== true) { + byUser = false; } + var _start = start != undefined ? util.convert(start, "Date").valueOf() : null; + var _end = end != undefined ? util.convert(end, "Date").valueOf() : null; + this._cancelAnimation(); - // only react on left mouse button down - this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!this.leftButtonDown && !this.touchDown) return; + if (animate) { + var me = this; + var initStart = this.start; + var initEnd = this.end; + var duration = typeof animate === "number" ? animate : 500; + var initTime = new Date().valueOf(); + var anyChanged = false; - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); + var next = function () { + if (!me.props.touch.dragging) { + var now = new Date().valueOf(); + var time = now - initTime; + var done = time > duration; + var s = done || _start === null ? _start : util.easeInOutQuad(time, initStart, _start, duration); + var e = done || _end === null ? _end : util.easeInOutQuad(time, initEnd, _end, duration); - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); + changed = me._applyRange(s, e); + DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); + anyChanged = anyChanged || changed; + if (changed) { + me.body.emitter.emit("rangechange", { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + } - this.frame.style.cursor = "move"; + if (done) { + if (anyChanged) { + me.body.emitter.emit("rangechanged", { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + } + } else { + // animate with as high as possible frame rate, leave 20 ms in between + // each to prevent the browser from blocking + me.animateTimer = setTimeout(next, 20); + } + } + }; - // 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); + return next(); + } else { + var changed = this._applyRange(_start, _end); + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + if (changed) { + var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; + this.body.emitter.emit("rangechange", params); + this.body.emitter.emit("rangechanged", params); + } + } }; + /** + * Stop an animation + * @private + */ + Range.prototype._cancelAnimation = function () { + if (this.animateTimer) { + clearTimeout(this.animateTimer); + this.animateTimer = null; + } + }; /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event + * Set a new start and end range. This method is the same as setRange, but + * does not trigger a range change and range changed event, and it returns + * true when the range is changed + * @param {Number} [start] + * @param {Number} [end] + * @return {Boolean} changed + * @private */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.event; + 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; - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; + // 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 + "\""); + } - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = min - newStart; + newStart += diff; + newEnd += diff; - // 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; + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } + } } - if (Math.abs(Math.cos(horizontalNew)) < snapValue) { - horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; + + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = newEnd - max; + newStart -= diff; + newEnd -= diff; + + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } + } } - // snap vertically to nice angles - if (Math.abs(Math.sin(verticalNew)) < snapValue) { - verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; + } + if (newEnd - newStart < zoomMin) { + if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } else { + // zoom to the minimum + diff = zoomMin - (newEnd - newStart); + newStart -= diff / 2; + newEnd += diff / 2; + } + } } - if (Math.abs(Math.cos(verticalNew)) < snapValue) { - verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; + + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } + + if (newEnd - newStart > zoomMax) { + if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } else { + // zoom to the maximum + diff = newEnd - newStart - zoomMax; + newStart += diff / 2; + newEnd -= diff / 2; + } + } } - this.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); + var changed = this.start != newStart || this.end != newEnd; - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit("cameraPositionChange", parameters); + // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) + if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { + this.body.emitter.emit("checkRangedItems"); + } - util.preventDefault(event); + this.start = newStart; + this.end = newEnd; + return changed; }; - /** - * Stop moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {event} event The event + * Retrieve the current range. + * @return {Object} An object with start and end properties */ - 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); + Range.prototype.getRange = function () { + return { + start: this.start, + end: this.end + }; }; /** - * 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 + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion */ - Graph3d.prototype._onTooltip = function (event) { - var delay = 300; // ms - var boundingRect = this.frame.getBoundingClientRect(); - var mouseX = getMouseX(event) - boundingRect.left; - var mouseY = getMouseY(event) - boundingRect.top; - - if (!this.showTooltip) { - return; - } - - if (this.tooltipTimeout) { - clearTimeout(this.tooltipTimeout); - } + Range.prototype.conversion = function (width, totalHidden) { + return Range.conversion(this.start, this.end, width, totalHidden); + }; - // (delayed) display of a tooltip only if no mouse button is down - if (this.leftButtonDown) { - this._hideTooltip(); - return; + /** + * Static method to calculate the conversion offset and scale for a range, + * based on the provided start, end, and width + * @param {Number} start + * @param {Number} end + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.conversion = function (start, end, width, totalHidden) { + if (totalHidden === undefined) { + totalHidden = 0; } - - if (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(); - } - } + if (width != 0 && end - start != 0) { + return { + offset: start, + scale: width / (end - start - totalHidden) + }; } 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); + return { + offset: 0, + scale: 1 + }; } }; /** - * Event handler for touchstart event on mobile devices + * Start dragging horizontally or vertically + * @param {Event} event + * @private */ - Graph3d.prototype._onTouchStart = function (event) { - this.touchDown = true; + Range.prototype._onDragStart = function (event) { + this.deltaDifference = 0; + this.previousDelta = 0; + // only allow dragging when configured as movable + if (!this.options.moveable) return; - 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); + // 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._onMouseDown(event); + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.dragging = true; + + if (this.body.dom.root) { + this.body.dom.root.style.cursor = "move"; + } + + event.preventDefault(); }; /** - * Event handler for touchmove event on mobile devices + * Perform dragging operation + * @param {Event} event + * @private */ - Graph3d.prototype._onTouchMove = function (event) { - this._onMouseMove(event); + Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; + + // TODO: this may be redundant in hammerjs2 + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; + + var direction = this.options.direction; + validateDirection(direction); + var delta = direction == "horizontal" ? event.deltaX : event.deltaY; + delta -= this.deltaDifference; + var interval = this.props.touch.end - this.props.touch.start; + + // normalize dragging speed if cutout is in between. + var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + interval -= duration; + + var width = direction == "horizontal" ? this.body.domProps.center.width : this.body.domProps.center.height; + var diffRange = -delta / width * interval; + var newStart = this.props.touch.start + diffRange; + var newEnd = this.props.touch.end + diffRange; + + // snapping times away from hidden zones + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); + if (safeStart != newStart || safeEnd != newEnd) { + this.deltaDifference += delta; + this.props.touch.start = safeStart; + this.props.touch.end = safeEnd; + this._onDrag(event); + return; + } + + this.previousDelta = delta; + this._applyRange(newStart, newEnd); + + // fire a rangechange event + this.body.emitter.emit("rangechange", { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); + + event.preventDefault(); }; /** - * Event handler for touchend event on mobile devices + * Stop dragging operation + * @param {event} event + * @private */ - Graph3d.prototype._onTouchEnd = function (event) { - this.touchDown = false; + Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - util.removeEventListener(document, "touchmove", this.ontouchmove); - util.removeEventListener(document, "touchend", this.ontouchend); + // TODO: this may be redundant in hammerjs2 + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - this._onMouseUp(event); - }; + this.props.touch.dragging = false; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = "auto"; + } + // fire a rangechanged event + this.body.emitter.emit("rangechanged", { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); + }; /** - * Event handler for mouse wheel event, used to zoom the graph + * Event handler for mouse wheel event, used to zoom * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {event} event The event + * @param {Event} event + * @private */ - Graph3d.prototype._onWheel = function (event) { - if (!event) /* For IE. */ - event = window.event; + Range.prototype._onMouseWheel = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; // retrieve delta var delta = 0; @@ -8266,4388 +8154,1730 @@ return /******/ (function(modules) { // webpackBootstrap // 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); + // perform the zoom action. Delta is normally 1 or -1 - this.camera.setArmLength(newLength); - this.redraw(); + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - delta / 5; + } else { + scale = 1 / (1 + delta / 5); + } - this._hideTooltip(); - } + // calculate center, the date to zoom around + var pointer = getPointer({ x: event.pageX, y: event.pageY }, this.body.dom.center); + var pointerDate = this._pointerToDate(pointer); - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit("cameraPositionChange", parameters); + this.zoom(scale, pointerDate, delta); + } - // 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); + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); }; /** - * 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 + * Start of a touch gesture * @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; - } - - 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)); - - // 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); + Range.prototype._onTouch = function (event) { + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.allowDragging = true; + this.props.touch.center = null; + this.scaleOffset = 0; + this.deltaDifference = 0; }; /** - * 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 + * On start of a hold gesture * @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); - - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; - } - } - } - } - - - return closestDataPoint; + Range.prototype._onHold = function () { + this.props.touch.allowDragging = false; }; /** - * Display a tooltip for given data point - * @param {Object} dataPoint + * Handle pinch event + * @param {Event} event * @private */ - Graph3d.prototype._showTooltip = function (dataPoint) { - var content, line, dot; + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - 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)"; + this.props.touch.allowDragging = false; - line = document.createElement("div"); - line.style.position = "absolute"; - line.style.height = "40px"; - line.style.width = "0"; - line.style.borderLeft = "1px solid #4d4d4d"; + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.center, this.body.dom.center); + } - 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"; + var scale = 1 / (event.scale + this.scaleOffset); + var centerDate = this._pointerToDate(this.props.touch.center); - 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; - } + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - this._hideTooltip(); + // calculate new start and end + var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; + var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; - 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 + "
"; - } + // snapping times away from hidden zones + this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times + this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times - content.style.left = "0"; - content.style.top = "0"; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); + if (safeStart != newStart || safeEnd != newEnd) { + this.props.touch.start = safeStart; + this.props.touch.end = safeEnd; + this.scaleOffset = 1 - event.scale; + newStart = safeStart; + newEnd = safeEnd; + } - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; + this.setRange(newStart, newEnd, false, true); - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default - 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"; + event.preventDefault(); }; /** - * Hide the tooltip when displayed + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date * @private */ - Graph3d.prototype._hideTooltip = function () { - if (this.tooltip) { - this.tooltip.dataPoint = null; + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - 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); - } - } - } + validateDirection(direction); + + if (direction == "horizontal") { + return this.body.util.toTime(pointer.x).valueOf(); + } else { + var height = this.body.domProps.center.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; } }; - /**--------------------------------------------------------------------------**/ - - /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x + * Get the pointer location relative to the location of the dom element + * @param {{x: Number, y: Number}} touch + * @param {Element} element HTML DOM element + * @return {{x: Number, y: Number}} pointer + * @private */ - function getMouseX(event) { - if ("clientX" in event) { - return event.clientX; - }return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + function getPointer(touch, element) { + return { + x: touch.x - util.getAbsoluteLeft(element), + y: touch.y - util.getAbsoluteTop(element) + }; } /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y + * 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. */ - function getMouseY(event) { - if ("clientY" in event) { - return event.clientY; - }return event.targetTouches[0] && event.targetTouches[0].clientY || 0; - } - - module.exports = Graph3d; - // use use defaults + Range.prototype.zoom = function (scale, center, delta) { + // if centerDate is not provided, take it half between start Date and end Date + if (center == null) { + center = (this.start + this.end) / 2; + } -/***/ }, -/* 11 */ -/***/ function(module, exports, __webpack_require__) { + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - - /** - * Expose `Emitter`. - */ + // calculate new start and end + var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; + var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; - module.exports = Emitter; + // snapping times away from hidden zones + this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); + if (safeStart != newStart || safeEnd != newEnd) { + newStart = safeStart; + newEnd = safeEnd; + } - /** - * Initialize a new `Emitter`. - * - * @api public - */ + this.setRange(newStart, newEnd, false, true); - function Emitter(obj) { - if (obj) return mixin(obj); + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default }; - /** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ - function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; - } - return obj; - } /** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * 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; - Emitter.prototype.on = - Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; + // 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; }; /** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ + Range.prototype.moveTo = function (moveTo) { + var center = (this.start + this.end) / 2; - Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; + var diff = center - moveTo; - function on() { - self.off(event, on); - fn.apply(this, arguments); - } + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; - on.fn = fn; - this.on(event, on); - return this; + this.setRange(newStart, newEnd); }; - /** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - - Emitter.prototype.off = - Emitter.prototype.removeListener = - Emitter.prototype.removeAllListeners = - Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; + module.exports = Range; - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } +/***/ }, +/* 18 */ +/***/ function(module, exports, __webpack_require__) { - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; + "use strict"; - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; - } + // Utility functions for ordering and stacking of items + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors - // 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; + /** + * 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; + }); }; /** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} items */ + exports.orderByEnd = function (items) { + items.sort(function (a, b) { + var aTime = "end" in a.data ? a.data.end : a.data.start, + bTime = "end" in b.data ? b.data.end : b.data.start; - Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; + return aTime - bTime; + }); + }; - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); + /** + * Adjust vertical positions of the items such that they don't overlap each + * other. + * @param {Item[]} items + * All visible items + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * Margins between items and between items and the axis. + * @param {boolean} [force=false] + * If true, all items will be repositioned. If false (default), only + * items having a top===null will be re-stacked + */ + exports.stack = function (items, margin, force) { + var i, iMax; + + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; } } - return this; + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + if (item.stack && item.top === null) { + // initialize top position + item.top = margin.axis; + + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + var collidingItem = null; + for (var j = 0, jj = items.length; j < jj; j++) { + var other = items[j]; + if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } + + 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 array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public + * Adjust vertical positions of the items without stacking them + * @param {Item[]} items + * All visible items + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * Margins between items and between items and the axis. */ + exports.nostack = function (items, margin, subgroups) { + var i, iMax, newTop; - Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + if (items[i].data.subgroup !== undefined) { + newTop = margin.axis; + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { + newTop += subgroups[subgroup].height + margin.item.vertical; + } + } + } + items[i].top = newTop; + } else { + items[i].top = margin.axis; + } + } }; /** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public + * 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 */ - - Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; + 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; }; - /***/ }, -/* 12 */ +/* 19 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - /** - * @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; - }; - - /** - * 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; - }; + var moment = __webpack_require__(40); + var DateUtil = __webpack_require__(15); + var util = __webpack_require__(1); /** - * 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; - }; - - /** - * 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); - }; - - /** - * 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(); - - crossproduct.x = a.y * b.z - a.z * b.y; - crossproduct.y = a.z * b.x - a.x * b.z; - crossproduct.z = a.x * b.y - a.y * b.x; - - return crossproduct; - }; - - - /** - * 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); - }; - - module.exports = Point3d; - -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - /** - * @prototype Point2d - * @param {Number} [x] - * @param {Number} [y] - */ - function Point2d(x, y) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - } - - module.exports = Point2d; - -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var Point3d = __webpack_require__(12); - - /** - * @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. + * @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. * - * Documentation: - * http://en.wikipedia.org/wiki/3D_projection + * 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 Camera() { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; - - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - - this.calculateCameraOrientation(); - } + function TimeStep(start, end, minimumStep, hiddenDates) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); - /** - * Set the location (origin) of the arm - * @param {Number} x Normalized value of x - * @param {Number} y Normalized value of y - * @param {Number} z Normalized value of z - */ - Camera.prototype.setArmLocation = function (x, y, z) { - this.armLocation.x = x; - this.armLocation.y = y; - this.armLocation.z = z; + this.autoScale = true; + this.scale = "day"; + this.step = 1; - this.calculateCameraOrientation(); - }; + // initialize the range + this.setRange(start, end, minimumStep); - /** - * 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; + // hidden Dates options + this.switchedDay = false; + this.switchedMonth = false; + this.switchedYear = false; + this.hiddenDates = hiddenDates; + if (hiddenDates === undefined) { + this.hiddenDates = []; } - 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; - } + this.format = TimeStep.FORMAT; // default formatting + } - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); + // Time formatting + TimeStep.FORMAT = { + minorLabels: { + millisecond: "SSS", + second: "s", + minute: "HH:mm", + hour: "HH:mm", + weekday: "ddd D", + day: "D", + month: "MMM", + year: "YYYY" + }, + majorLabels: { + millisecond: "HH:mm:ss", + second: "D MMMM HH:mm", + minute: "ddd D MMMM", + hour: "ddd D MMMM", + weekday: "MMMM YYYY", + day: "MMMM YYYY", + month: "YYYY", + year: "" } }; /** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical + * Set custom formatting for the minor an major labels of the TimeStep. + * Both `minorLabels` and `majorLabels` are an Object with properties: + * 'millisecond, 'second, 'minute', 'hour', 'weekday, 'day, 'month, 'year'. + * @param {{minorLabels: Object, majorLabels: Object}} format */ - Camera.prototype.getArmRotation = function () { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; - - return rot; + TimeStep.prototype.setFormat = function (format) { + var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); + this.format = util.deepExtend(defaultFormat, format); }; /** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 + * 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 */ - Camera.prototype.setArmLength = function (length) { - if (length === undefined) return; - - this.armLength = length; + TimeStep.prototype.setRange = function (start, end, minimumStep) { + if (!(start instanceof Date) || !(end instanceof Date)) { + throw "No legal start or end date in method setRange"; + } - // Radius must be larger than the corner of the graph, - // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the - // graph - if (this.armLength < 0.71) this.armLength = 0.71; - if (this.armLength > 5) this.armLength = 5; + this._start = start != undefined ? new Date(start.valueOf()) : new Date(); + this._end = end != undefined ? new Date(end.valueOf()) : new Date(); - this.calculateCameraOrientation(); + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } }; /** - * Retrieve the arm length - * @return {Number} length + * Set the range iterator to the start date. */ - Camera.prototype.getArmLength = function () { - return this.armLength; + TimeStep.prototype.first = function () { + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); }; /** - * Retrieve the camera location - * @return {Point3d} cameraLocation + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date */ - Camera.prototype.getCameraLocation = function () { - return this.cameraLocation; - }; + TimeStep.prototype.roundToMinor = function () { + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + // noinspection FallThroughInSwitchStatementJS + switch (this.scale) { + case "year": + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case "month": + this.current.setDate(1); + case "day": + // intentional fall through + case "weekday": + this.current.setHours(0); + case "hour": + this.current.setMinutes(0); + case "minute": + this.current.setSeconds(0); + case "second": + this.current.setMilliseconds(0); + //case 'millisecond': // nothing to do for milliseconds + } - /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation - */ - Camera.prototype.getCameraRotation = function () { - return this.cameraRotation; + if (this.step != 1) { + // round down to the first minor value that is a multiple of the current step size + switch (this.scale) { + case "millisecond": + this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);break; + case "second": + this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);break; + case "minute": + this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);break; + case "hour": + this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);break; + case "weekday": + // intentional fall through + case "day": + this.current.setDate(this.current.getDate() - 1 - (this.current.getDate() - 1) % this.step + 1);break; + case "month": + this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);break; + case "year": + this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step);break; + default: + break; + } + } }; /** - * Calculate the location and rotation of the camera based on the - * position and orientation of the camera arm + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date */ - 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); - - // calculate rotation of the camera - this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical; - this.cameraRotation.y = 0; - this.cameraRotation.z = -this.armRotation.horizontal; + TimeStep.prototype.hasNext = function () { + return this.current.valueOf() <= this._end.valueOf(); }; - module.exports = Camera; - -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var DataView = __webpack_require__(9); - /** - * @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 + * Do the next step */ - 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); - } + TimeStep.prototype.next = function () { + var prev = this.current.valueOf(); - // create an array with the filtered datapoints. this will be loaded afterwards - this.dataPoints = []; + // Two cases, needed to prevent issues with switching daylight savings + // (end of March and end of October) + if (this.current.getMonth() < 6) { + switch (this.scale) { + case "millisecond": - this.loaded = false; - this.onLoadCallback = undefined; - if (graph.animationPreload) { - this.loaded = false; - this.loadInBackground(); + this.current = new Date(this.current.valueOf() + this.step);break; + case "second": + this.current = new Date(this.current.valueOf() + this.step * 1000);break; + case "minute": + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60);break; + case "hour": + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); + // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) + var h = this.current.getHours(); + this.current.setHours(h - h % this.step); + break; + case "weekday": + // intentional fall through + case "day": + this.current.setDate(this.current.getDate() + this.step);break; + case "month": + this.current.setMonth(this.current.getMonth() + this.step);break; + case "year": + this.current.setFullYear(this.current.getFullYear() + this.step);break; + default: + break; + } } else { - this.loaded = true; + switch (this.scale) { + case "millisecond": + this.current = new Date(this.current.valueOf() + this.step);break; + case "second": + this.current.setSeconds(this.current.getSeconds() + this.step);break; + case "minute": + this.current.setMinutes(this.current.getMinutes() + this.step);break; + case "hour": + this.current.setHours(this.current.getHours() + this.step);break; + case "weekday": + // intentional fall through + case "day": + this.current.setDate(this.current.getDate() + this.step);break; + case "month": + this.current.setMonth(this.current.getMonth() + this.step);break; + case "year": + this.current.setFullYear(this.current.getFullYear() + this.step);break; + default: + break; + } } - }; - - - /** - * Return the label - * @return {string} label - */ - Filter.prototype.isLoaded = function () { - return this.loaded; - }; + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case "millisecond": + if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; + case "second": + if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; + case "minute": + if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; + case "hour": + if (this.current.getHours() < this.step) this.current.setHours(0);break; + case "weekday": + // intentional fall through + case "day": + if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; + case "month": + if (this.current.getMonth() < this.step) this.current.setMonth(0);break; + case "year": + break; // nothing to do for year + default: + break; + } + } - /** - * Return the loaded progress - * @return {Number} percentage between 0 and 100 - */ - Filter.prototype.getLoadedProgress = function () { - var len = this.values.length; - - var i = 0; - while (this.dataPoints[i]) { - i++; + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); } - return Math.round(i / len * 100); + DateUtil.stepOverHiddenDates(this, prev); }; /** - * Return the label - * @return {string} label + * Get the current datetime + * @return {Date} current The current date */ - Filter.prototype.getLabel = function () { - return this.graph.filterLabel; + TimeStep.prototype.getCurrent = function () { + return this.current; }; - /** - * Return the columnIndex of the filter - * @return {Number} columnIndex + * Set a custom scale. Autoscaling will be disabled. + * For example setScale('minute', 5) will result + * in minor steps of 5 minutes, and major steps of an hour. + * + * @param {{scale: string, step: number}} params + * An object containing two properties: + * - A string 'scale'. Choose from 'millisecond', 'second', + * 'minute', 'hour', 'weekday, 'day, 'month, 'year'. + * - A number 'step'. A step size, by default 1. + * Choose for example 1, 2, 5, or 10. */ - Filter.prototype.getColumn = function () { - return this.column; + TimeStep.prototype.setScale = function (params) { + if (params && typeof params.scale == "string") { + this.scale = params.scale; + this.step = params.step > 0 ? params.step : 1; + this.autoScale = false; + } }; /** - * Return the currently selected value. Returns undefined if there is no selection - * @return {*} value + * Enable or disable autoscaling + * @param {boolean} enable If true, autoascaling is set true */ - Filter.prototype.getSelectedValue = function () { - if (this.index === undefined) return undefined; - - return this.values[this.index]; + TimeStep.prototype.setAutoScale = function (enable) { + this.autoScale = enable; }; - /** - * Retrieve all values of the filter - * @return {Array} values - */ - Filter.prototype.getValues = function () { - return this.values; - }; /** - * Retrieve one value of the filter - * @param {Number} index - * @return {*} value + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds */ - Filter.prototype.getValue = function (index) { - if (index >= this.values.length) throw "Error: index out of range"; + TimeStep.prototype.setMinimumStep = function (minimumStep) { + if (minimumStep == undefined) { + return; + } - return this.values[index]; - }; + //var b = asc + ds; + var stepYear = 1000 * 60 * 60 * 24 * 30 * 12; + var stepMonth = 1000 * 60 * 60 * 24 * 30; + var stepDay = 1000 * 60 * 60 * 24; + var stepHour = 1000 * 60 * 60; + var stepMinute = 1000 * 60; + var stepSecond = 1000; + var stepMillisecond = 1; - /** - * 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; - - if (index === undefined) return []; - - var dataPoints; - if (this.dataPoints[index]) { - dataPoints = this.dataPoints[index]; - } else { - var f = {}; - f.column = this.column; - f.value = this.values[index]; - - var dataView = new DataView(this.data, { filter: function (item) { - return item[f.column] == f.value; - } }).get(); - dataPoints = this.graph._getDataPoints(dataView); - - this.dataPoints[index] = dataPoints; + // find the smallest step that is larger than the provided minimumStep + if (stepYear * 1000 > minimumStep) { + this.scale = "year";this.step = 1000; } - - return dataPoints; - }; - - - - /** - * Set a callback function when the filter is fully loaded. - */ - Filter.prototype.setOnLoadCallback = function (callback) { - this.onLoadCallback = callback; - }; - - - /** - * 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"; - - this.index = index; - this.value = this.values[index]; - }; - - /** - * 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; - - var frame = this.graph.frame; - - if (index < this.values.length) { - var dataPointsTemp = this._getDataPoints(index); - //this.graph.redrawInfo(); // TODO: not neat - - // create a progress box - if (frame.progress === undefined) { - frame.progress = document.createElement("DIV"); - frame.progress.style.position = "absolute"; - frame.progress.style.color = "gray"; - frame.appendChild(frame.progress); - } - var progress = this.getLoadedProgress(); - frame.progress.innerHTML = "Loading animation... " + progress + "%"; - // TODO: this is no nice solution... - frame.progress.style.bottom = 60 + "px"; // TODO: use height of slider - frame.progress.style.left = 10 + "px"; - - var me = this; - setTimeout(function () { - me.loadInBackground(index + 1); - }, 10); - this.loaded = false; - } else { - this.loaded = true; - - // remove the progress box - if (frame.progress !== undefined) { - frame.removeChild(frame.progress); - frame.progress = undefined; - } - - if (this.onLoadCallback) this.onLoadCallback(); + if (stepYear * 500 > minimumStep) { + this.scale = "year";this.step = 500; } - }; - - module.exports = Filter; - -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var util = __webpack_require__(1); - - /** - * @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"; + if (stepYear * 100 > minimumStep) { + this.scale = "year";this.step = 100; } - this.container = container; - this.visible = options && options.visible != undefined ? options.visible : true; - - if (this.visible) { - this.frame = document.createElement("DIV"); - //this.frame.style.backgroundColor = '#E5E5E5'; - this.frame.style.width = "100%"; - this.frame.style.position = "relative"; - this.container.appendChild(this.frame); - - this.frame.prev = document.createElement("INPUT"); - this.frame.prev.type = "BUTTON"; - this.frame.prev.value = "Prev"; - this.frame.appendChild(this.frame.prev); - - this.frame.play = document.createElement("INPUT"); - this.frame.play.type = "BUTTON"; - this.frame.play.value = "Play"; - this.frame.appendChild(this.frame.play); - - this.frame.next = document.createElement("INPUT"); - this.frame.next.type = "BUTTON"; - this.frame.next.value = "Next"; - this.frame.appendChild(this.frame.next); - - 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); - }; + if (stepYear * 50 > minimumStep) { + this.scale = "year";this.step = 50; } - - this.onChangeCallback = undefined; - - this.values = []; - this.index = undefined; - - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; - } - - /** - * Select the previous index - */ - Slider.prototype.prev = function () { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); + if (stepYear * 10 > minimumStep) { + this.scale = "year";this.step = 10; } - }; - - /** - * Select the next index - */ - Slider.prototype.next = function () { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); + if (stepYear * 5 > minimumStep) { + this.scale = "year";this.step = 5; } - }; - - /** - * Select the next index - */ - Slider.prototype.playNext = function () { - var start = new Date(); - - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } else if (this.playLoop) { - // jump to the start - index = 0; - this.setIndex(index); + if (stepYear > minimumStep) { + this.scale = "year";this.step = 1; } - - var end = new Date(); - var diff = end - start; - - // calculate how much time it to to set the index and to execute the callback - // function. - var interval = Math.max(this.playInterval - diff, 0); - // document.title = diff // TODO: cleanup - - var me = this; - this.playTimeout = setTimeout(function () { - me.playNext(); - }, interval); - }; - - /** - * Toggle start or stop playing - */ - Slider.prototype.togglePlay = function () { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); + if (stepMonth * 3 > minimumStep) { + this.scale = "month";this.step = 3; } - }; - - /** - * Start playing - */ - Slider.prototype.play = function () { - // Test whether already playing - if (this.playTimeout) return; - - this.playNext(); - - if (this.frame) { - this.frame.play.value = "Stop"; + if (stepMonth > minimumStep) { + this.scale = "month";this.step = 1; } - }; - - /** - * Stop playing - */ - Slider.prototype.stop = function () { - clearInterval(this.playTimeout); - this.playTimeout = undefined; - - if (this.frame) { - this.frame.play.value = "Play"; + if (stepDay * 5 > minimumStep) { + this.scale = "day";this.step = 5; } - }; - - /** - * 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; - }; - - /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds - */ - Slider.prototype.setPlayInterval = function (interval) { - this.playInterval = interval; - }; - - /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds - */ - Slider.prototype.getPlayInterval = function (interval) { - return this.playInterval; - }; - - /** - * 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; - }; - - - /** - * Execute the onchange callback function - */ - Slider.prototype.onChange = function () { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); + if (stepDay * 2 > minimumStep) { + this.scale = "day";this.step = 2; } - }; - - /** - * 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"; - - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = left + "px"; + if (stepDay > minimumStep) { + this.scale = "day";this.step = 1; } - }; - - - /** - * 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; - - if (this.values.length > 0) this.setIndex(0);else this.index = undefined; - }; - - /** - * Select a value by its index - * @param {Number} index - */ - Slider.prototype.setIndex = function (index) { - if (index < this.values.length) { - this.index = index; - - this.redraw(); - this.onChange(); - } else { - throw "Error: index out of range"; + if (stepDay / 2 > minimumStep) { + this.scale = "weekday";this.step = 1; + } + if (stepHour * 4 > minimumStep) { + this.scale = "hour";this.step = 4; + } + if (stepHour > minimumStep) { + this.scale = "hour";this.step = 1; + } + if (stepMinute * 15 > minimumStep) { + this.scale = "minute";this.step = 15; + } + if (stepMinute * 10 > minimumStep) { + this.scale = "minute";this.step = 10; + } + if (stepMinute * 5 > minimumStep) { + this.scale = "minute";this.step = 5; + } + if (stepMinute > minimumStep) { + this.scale = "minute";this.step = 1; + } + if (stepSecond * 15 > minimumStep) { + this.scale = "second";this.step = 15; + } + if (stepSecond * 10 > minimumStep) { + this.scale = "second";this.step = 10; + } + if (stepSecond * 5 > minimumStep) { + this.scale = "second";this.step = 5; + } + if (stepSecond > minimumStep) { + this.scale = "second";this.step = 1; + } + if (stepMillisecond * 200 > minimumStep) { + this.scale = "millisecond";this.step = 200; + } + if (stepMillisecond * 100 > minimumStep) { + this.scale = "millisecond";this.step = 100; + } + if (stepMillisecond * 50 > minimumStep) { + this.scale = "millisecond";this.step = 50; + } + if (stepMillisecond * 10 > minimumStep) { + this.scale = "millisecond";this.step = 10; + } + if (stepMillisecond * 5 > minimumStep) { + this.scale = "millisecond";this.step = 5; + } + if (stepMillisecond > minimumStep) { + this.scale = "millisecond";this.step = 1; } }; /** - * retrieve the index of the currently selected vaue - * @return {Number} index - */ - Slider.prototype.getIndex = function () { - return this.index; - }; - - - /** - * retrieve the currently selected value - * @return {*} value + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * Static function + * @param {Date} date the date to be snapped. + * @param {string} scale Current scale, can be 'millisecond', 'second', + * 'minute', 'hour', 'weekday, 'day, 'month, 'year'. + * @param {number} step Current step (1, 2, 4, 5, ... + * @return {Date} snappedDate */ - Slider.prototype.get = function () { - return this.values[this.index]; - }; - - - Slider.prototype._onMouseDown = function (event) { - // only react on left mouse button down - var leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!leftButtonDown) return; - - 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); - }; - - - 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; - }; + TimeStep.snap = function (date, scale, step) { + var clone = new Date(date.valueOf()); - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + if (scale == "year") { + var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); + clone.setFullYear(Math.round(year / step) * step); + clone.setMonth(0); + clone.setDate(0); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == "month") { + if (clone.getDate() > 15) { + clone.setDate(1); + clone.setMonth(clone.getMonth() + 1); + // important: first set Date to 1, after that change the month. + } else { + clone.setDate(1); + } - 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(); - }; - - - Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = "auto"; - - // remove event listeners - util.removeEventListener(document, "mousemove", this.onmousemove); - util.removeEventListener(document, "mouseup", this.onmouseup); - - util.preventDefault(); - }; - - module.exports = Slider; - -/***/ }, -/* 17 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - /** - * @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; - - this._current = 0; - this.setRange(start, end, step, prettyStep); - }; - - /** - * 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; - - this.setStep(step, prettyStep); - }; - - /** - * 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; - - if (prettyStep !== undefined) this.prettyStep = prettyStep; - - if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; - }; - - /** - * 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; - }; - - // try three steps (multiple of 1, 2, or 5 - var step1 = Math.pow(10, Math.round(log10(step))), - step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), - step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); - - // choose the best step (closest to minimum step) - var prettyStep = step1; - if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; - if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; - - // for safety - if (prettyStep <= 0) { - prettyStep = 1; - } - - return prettyStep; - }; - - /** - * returns the current value of the step - * @return {Number} current value - */ - StepNumber.prototype.getCurrent = function () { - return parseFloat(this._current.toPrecision(this.precision)); - }; - - /** - * returns the current step size - * @return {Number} current step size - */ - StepNumber.prototype.getStep = function () { - return this._step; - }; - - /** - * 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; - }; - - /** - * Do a step, add the step size to the current value - */ - StepNumber.prototype.next = function () { - this._current += this._step; - }; - - /** - * 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; - }; - - module.exports = StepNumber; - -/***/ }, -/* 18 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var Emitter = __webpack_require__(11); - var Hammer = __webpack_require__(19); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); - var Range = __webpack_require__(23); - var Core = __webpack_require__(27); - var TimeAxis = __webpack_require__(42); - var CurrentTime = __webpack_require__(43); - var CustomTime = __webpack_require__(40); - var ItemSet = __webpack_require__(28); - - /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [items] - * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [groups] - * @param {Object} [options] See Timeline.setOptions for the available options. - * @constructor - * @extends Core - */ - function Timeline(container, items, groups, options) { - if (!(this instanceof Timeline)) { - throw new SyntaxError("Constructor must be called with the new operator"); - } - - // if the third element is options, the forth is groups (optionally); - if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { - var forthArgument = options; - options = groups; - groups = forthArgument; - } - - 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) - }, - hiddenDates: [], - util: { - getScale: function () { - return me.timeAxis.step.scale; - }, - getStep: function () { - return me.timeAxis.step.step; - }, - - toScreen: me._toScreen.bind(me), - toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width - toTime: me._toTime.bind(me), - toGlobalTime: me._toGlobalTime.bind(me) - } - }; - - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; - - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(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.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); - } - - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } - - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); - } - } - - // Extend the functionality from Core - Timeline.prototype = new Core(); - - /** - * Force a redraw. The size of all items will be recalculated. - * Can be useful to manually redraw when option autoResize=false and the window - * has been resized, or when the items CSS has been changed. - */ - Timeline.prototype.redraw = function () { - this.itemSet && this.itemSet.markDirty({ refreshItems: true }); - this._redraw(); - }; - - /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items - */ - Timeline.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; - - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; - } else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: "Date", - end: "Date" - } - }); - } - - // set items - this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); - - if (initialLoad) { - if (this.options.start != undefined || this.options.end != undefined) { - if (this.options.start == undefined || this.options.end == undefined) { - var dataRange = this._getDataRange(); - } - - var start = this.options.start != undefined ? this.options.start : dataRange.start; - var end = this.options.end != undefined ? this.options.end : dataRange.end; - - this.setWindow(start, end, { animate: false }); - } else { - this.fit({ animate: false }); - } - } - }; - - /** - * Set groups - * @param {vis.DataSet | Array | google.visualization.DataTable} groups - */ - Timeline.prototype.setGroups = function (groups) { - // convert to type DataSet when needed - var newDataSet; - if (!groups) { - newDataSet = null; - } else if (groups instanceof DataSet || groups instanceof DataView) { - newDataSet = groups; - } else { - // turn an array into a dataset - newDataSet = new DataSet(groups); - } - - this.groupsData = newDataSet; - this.itemSet.setGroups(newDataSet); - }; - - /** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {string[] | string} [ids] An array with zero or more id's of the items to be - * selected. If ids is an empty array, all items will be - * unselected. - * @param {Object} [options] Available options: - * `focus: boolean` - * If true, focus will be set to the selected item(s) - * `animate: boolean | number` - * If true (default), the range is animated - * smoothly to the new window. - * If a number, the number is taken as duration - * for the animation. Default duration is 500 ms. - * Only applicable when option focus is true. - */ - Timeline.prototype.setSelection = function (ids, options) { - this.itemSet && this.itemSet.setSelection(ids); - - if (options && options.focus) { - this.focus(ids, options); - } - }; - - /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items - */ - Timeline.prototype.getSelection = function () { - return this.itemSet && this.itemSet.getSelection() || []; - }; - - /** - * Adjust the visible window such that the selected item (or multiple items) - * are centered on screen. - * @param {String | String[]} id An item id or array with item ids - * @param {Object} [options] Available options: - * `animate: boolean | number` - * If true (default), the range is animated - * smoothly to the new window. - * If a number, the number is taken as duration - * for the animation. Default duration is 500 ms. - * Only applicable when option focus is true - */ - Timeline.prototype.focus = function (id, options) { - if (!this.itemsData || id == undefined) return; - - var ids = Array.isArray(id) ? id : [id]; - - // get the specified item(s) - var itemsData = this.itemsData.getDataSet().get(ids, { - type: { - start: "Date", - end: "Date" - } - }); - - // calculate minimum start and maximum end of specified items - var start = null; - var end = null; - itemsData.forEach(function (itemData) { - var s = itemData.start.valueOf(); - var e = "end" in itemData ? itemData.end.valueOf() : itemData.start.valueOf(); - - if (start === null || s < start) { - start = s; - } - - if (end === null || e > end) { - end = e; - } - }); - - if (start !== null && end !== null) { - // calculate the new middle and interval for the window - var middle = (start + end) / 2; - var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1); - - var animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(middle - interval / 2, middle + interval / 2, animate); - } - }; - - /** - * Get the data range of the item set. - * @returns {{min: Date, max: Date}} range A range with a start and end Date. - * When no minimum is found, min==null - * When no maximum is found, max==null - */ - Timeline.prototype.getItemRange = function () { - // 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()); - } - } - } - - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; - }; - - - module.exports = Timeline; - -/***/ }, -/* 19 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - // Only load hammer.js when in a browser environment - // (loading hammer.js in a node.js environment gives errors) - if (typeof window !== "undefined") { - var propagating = __webpack_require__(20); - var Hammer = window.Hammer || __webpack_require__(21); - module.exports = propagating(Hammer); - } else { - module.exports = function () { - throw Error("hammer.js is only available in a browser, not in node.js."); - }; - } - -/***/ }, -/* 20 */ -/***/ function(module, exports, __webpack_require__) { - - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - - (function (factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - window.propagating = factory(); - } - }(function () { - // will contain the target element where the gesture started - var _firstTarget = null; // singleton - - /** - * Extend an Hammer.js instance with event propagation. - * - * Features: - * - Events emitted by hammer will propagate in order from child to parent - * elements. - * - Events are extended with a function `event.stopPropagation()` to stop - * propagation to parent elements. - * - * Usage: - * var hammer = propagatingHammer(new Hammer(element)); - * - * @param {Hammer.Manager} hammer An hammer instance. - * @return {Hammer.Manager} Returns the same hammer instance with extended - * functionality - */ - return function propagating(hammer) { - if (hammer.Manager) { - // This looks like the Hammer constructor. - // Overload the constructors with our own. - var Hammer = hammer; - - var PropagatingHammer = function(element, options) { - return propagating(new Hammer(element, options)); - }; - Hammer.extend(PropagatingHammer, Hammer); - PropagatingHammer.Manager = function (element, options) { - return propagating(new Hammer.Manager(element, options)); - }; - - return PropagatingHammer; - } - - // attach to DOM element - var element = hammer.element; - element.hammer = hammer; - - // move the original functions that we will wrap - hammer._on = hammer.on; - hammer._off = hammer.off; - hammer._emit = hammer.emit; - hammer._destroy = hammer.destroy; - - /** @type {Object.>} */ - hammer._handlers = {}; - - // register an event to catch the start of a gesture and store the - // target in a singleton - hammer._on('hammer.input', function (event) { - if (event.isFirst) { - _firstTarget = event.target; - } - }); - - /** - * Register a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} handler A callback function, called as handler(event) - * @returns {Hammer.Manager} Returns the hammer instance - */ - hammer.on = function (events, handler) { - // register the handler - split(events).forEach(function (event) { - var _handlers = hammer._handlers[event]; - if (!_handlers) { - hammer._handlers[event] = _handlers = []; - - // register the static, propagated handler - hammer._on(event, propagatedHandler); - } - _handlers.push(handler); - }); - - return hammer; - }; - - /** - * Unregister a handler for one or multiple events - * @param {String} events A space separated string with events - * @param {function} [handler] Optional. The registered handler. If not - * provided, all handlers for given events - * are removed. - * @returns {Hammer.Manager} Returns the hammer instance - */ - hammer.off = function (events, handler) { - // unregister the handler - split(events).forEach(function (event) { - var _handlers = hammer._handlers[event]; - if (_handlers) { - _handlers = handler ? _handlers.filter(function (h) { - return h !== handler; - }) : []; - - if (_handlers.length > 0) { - hammer._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer._off(event, propagatedHandler); - delete hammer._handlers[event]; - } - } - }); - - return hammer; - }; - - /** - * Emit to the event listeners - * @param {string} eventType - * @param {Event} event - */ - hammer.emit = function(eventType, event) { - _firstTarget = event.target; - hammer._emit(eventType, event); - }; - - hammer.destroy = function () { - // Detach from DOM element - var element = hammer.element; - delete element.hammer; - - // clear all handlers - hammer._handlers = {}; - - // call original hammer destroy - hammer._destroy(); - }; - - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); - } - - /** - * A static event handler, applying event propagation. - * @param {Object} event - */ - function propagatedHandler(event) { - // let only a single hammer instance handle this event - if (event.type !== 'hammer.input') { - if (event.srcEvent._handled && event.srcEvent._handled[event.type]) { - return; - } - else { - // it is possible that the same srcEvent is used with multiple hammer events - event.srcEvent._handled = {}; - event.srcEvent._handled[event.type] = true; - } - } - - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; - - // attach firstTarget property to the event - event.firstTarget = _firstTarget; - - // propagate over all elements (until stopped) - var elem = _firstTarget; - while (elem && !stopped) { - var _handlers = elem.hammer && elem.hammer._handlers[event.type]; - if (_handlers) { - for (var i = 0; i < _handlers.length && !stopped; i++) { - _handlers[i](event); - } - } - - elem = elem.parentNode; - } - } - - return hammer; - }; - })); - - -/***/ }, -/* 21 */ -/***/ function(module, exports, __webpack_require__) { - - var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ - * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ - (function(window, document, exportName, undefined) { - 'use strict'; - - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); - - var TYPE_FUNCTION = 'function'; - - var round = Math.round; - var abs = Math.abs; - var now = Date.now; - - /** - * set a timeout with a given scope - * @param {Function} fn - * @param {Number} timeout - * @param {Object} context - * @returns {number} - */ - function setTimeoutContext(fn, timeout, context) { - return setTimeout(bindFn(fn, context), timeout); - } - - /** - * if the argument is an array, we want to execute the fn on each entry - * if it aint an array we don't want to do a thing. - * this is used by all the methods that accept a single and array argument. - * @param {*|Array} arg - * @param {String} fn - * @param {Object} [context] - * @returns {Boolean} - */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; - } - return false; - } - - /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context - */ - function each(obj, iterator, context) { - var i; - - if (!obj) { - return; - } - - if (obj.forEach) { - obj.forEach(iterator, context); - } else if (obj.length !== undefined) { - i = 0; - while (i < obj.length) { - iterator.call(context, obj[i], i, obj); - i++; - } - } else { - for (i in obj) { - obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); - } - } - } - - /** - * extend object. - * means that properties in dest will be overwritten by the ones in src. - * @param {Object} dest - * @param {Object} src - * @param {Boolean} [merge] - * @returns {Object} dest - */ - function extend(dest, src, merge) { - var keys = Object.keys(src); - var i = 0; - while (i < keys.length) { - if (!merge || (merge && dest[keys[i]] === undefined)) { - dest[keys[i]] = src[keys[i]]; - } - i++; - } - return dest; - } - - /** - * merge the values from src in the dest. - * means that properties that exist in dest will not be overwritten by src - * @param {Object} dest - * @param {Object} src - * @returns {Object} dest - */ - function merge(dest, src) { - return extend(dest, src, true); - } - - /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] - */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; - - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; - - if (properties) { - extend(childP, properties); - } - } - - /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} - */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } - - /** - * let a boolean value also be a function that must return a boolean - * this first item in args will be used as the context - * @param {Boolean|Function} val - * @param {Array} [args] - * @returns {Boolean} - */ - function boolOrFn(val, args) { - if (typeof val == TYPE_FUNCTION) { - return val.apply(args ? args[0] || undefined : undefined, args); - } - return val; - } - - /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} - */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; - } - - /** - * addEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler - */ - function addEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.addEventListener(type, handler, false); - }); - } - - /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler - */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } - - /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found - */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; - } - - /** - * small indexOf wrapper - * @param {String} str - * @param {String} find - * @returns {Boolean} found - */ - function inStr(str, find) { - return str.indexOf(find) > -1; - } - - /** - * split string on whitespace - * @param {String} str - * @returns {Array} words - */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } - - /** - * find if a array contains the object using indexOf or a simple polyFill - * @param {Array} src - * @param {String} find - * @param {String} [findByKey] - * @return {Boolean|Number} false when not found, or the index - */ - function inArray(src, find, findByKey) { - if (src.indexOf && !findByKey) { - return src.indexOf(find); - } else { - var i = 0; - while (i < src.length) { - if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { - return i; - } - i++; - } - return -1; - } - } - - /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} - */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } - - /** - * unique array with objects based on a key (like 'id') or just by the array's value - * @param {Array} src [{id:1},{id:2},{id:1}] - * @param {String} [key] - * @param {Boolean} [sort=False] - * @returns {Array} [{id:1},{id:2}] - */ - function uniqueArray(src, key, sort) { - var results = []; - var values = []; - var i = 0; - - while (i < src.length) { - var val = key ? src[i][key] : src[i]; - if (inArray(values, val) < 0) { - results.push(src[i]); - } - values[i] = val; - i++; - } - - if (sort) { - if (!key) { - results = results.sort(); - } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); - } - } - - return results; - } - - /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed - */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); - - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; - - if (prop in obj) { - return prop; - } - i++; - } - return undefined; - } - - /** - * get a unique id - * @returns {number} uniqueId - */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } - - /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} - */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } - - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - - var SUPPORT_TOUCH = ('ontouchstart' in window); - var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; - var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); - - var INPUT_TYPE_TOUCH = 'touch'; - var INPUT_TYPE_PEN = 'pen'; - var INPUT_TYPE_MOUSE = 'mouse'; - var INPUT_TYPE_KINECT = 'kinect'; - - var COMPUTE_INTERVAL = 25; - - var INPUT_START = 1; - var INPUT_MOVE = 2; - var INPUT_END = 4; - var INPUT_CANCEL = 8; - - var DIRECTION_NONE = 1; - var DIRECTION_LEFT = 2; - var DIRECTION_RIGHT = 4; - var DIRECTION_UP = 8; - var DIRECTION_DOWN = 16; - - var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; - var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; - var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; - - var PROPS_XY = ['x', 'y']; - var PROPS_CLIENT_XY = ['clientX', 'clientY']; - - /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} - * @constructor - */ - function Input(manager, callback) { - var self = this; - this.manager = manager; - this.callback = callback; - this.element = manager.element; - this.target = manager.options.inputTarget; - - // smaller wrapper around the handler, for the scope and the enabled state of the manager, - // so when disabled the input events are completely bypassed. - this.domHandler = function(ev) { - if (boolOrFn(manager.options.enable, [manager])) { - self.handler(ev); - } - }; - - this.init(); - - } - - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, - - /** - * bind the events - */ - init: function() { - this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); - }, - - /** - * unbind the events - */ - destroy: function() { - this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); - } - }; - - /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} - */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; - - if (inputClass) { - Type = inputClass; - } else if (SUPPORT_POINTER_EVENTS) { - Type = PointerEventInput; - } else if (SUPPORT_ONLY_TOUCH) { - Type = TouchInput; - } else if (!SUPPORT_TOUCH) { - Type = MouseInput; - } else { - Type = TouchMouseInput; - } - return new (Type)(manager, inputHandler); - } - - /** - * handle input events - * @param {Manager} manager - * @param {String} eventType - * @param {Object} input - */ - function inputHandler(manager, eventType, input) { - var pointersLen = input.pointers.length; - var changedPointersLen = input.changedPointers.length; - var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); - var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); - - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; - - if (isFirst) { - manager.session = {}; - } - - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; - - // compute scale, rotation etc - computeInputData(manager, input); - - // emit secret event - manager.emit('hammer.input', input); - - manager.recognize(input); - manager.session.prevInput = input; - } - - /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input - */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; - - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } - - // to compute scale and rotation we need to store the multiple touches - if (pointersLength > 1 && !session.firstMultiple) { - session.firstMultiple = simpleCloneInputData(input); - } else if (pointersLength === 1) { - session.firstMultiple = false; - } - - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - - var center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; - - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); - - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); - - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - - computeIntervalInputData(session, input); - - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; - } - input.target = target; - } - - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; - - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; - - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } - - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } - - /** - * velocity is calculated every x ms - * @param {Object} session - * @param {Object} input - */ - function computeIntervalInputData(session, input) { - var last = session.lastInterval || input, - deltaTime = input.timeStamp - last.timeStamp, - velocity, velocityX, velocityY, direction; - - if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; - - var v = getVelocity(deltaTime, deltaX, deltaY); - velocityX = v.x; - velocityY = v.y; - velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; - direction = getDirection(deltaX, deltaY); - - session.lastInterval = input; - } else { - // use latest velocity info if it doesn't overtake a minimum period - velocity = last.velocity; - velocityX = last.velocityX; - velocityY = last.velocityY; - direction = last.direction; - } - - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } - - /** - * create a simple clone from the input used for storage of firstInput and firstMultiple - * @param {Object} input - * @returns {Object} clonedInputData - */ - function simpleCloneInputData(input) { - // make a simple copy of the pointers because we will get a reference if we don't - // we only need clientXY for the calculations - var pointers = []; - var i = 0; - while (i < input.pointers.length) { - pointers[i] = { - clientX: round(input.pointers[i].clientX), - clientY: round(input.pointers[i].clientY) - }; - i++; - } - - return { - timeStamp: now(), - pointers: pointers, - center: getCenter(pointers), - deltaX: input.deltaX, - deltaY: input.deltaY - }; - } - - /** - * get the center of all the pointers - * @param {Array} pointers - * @return {Object} center contains `x` and `y` properties - */ - function getCenter(pointers) { - var pointersLength = pointers.length; - - // no need to loop when only one touch - if (pointersLength === 1) { - return { - x: round(pointers[0].clientX), - y: round(pointers[0].clientY) - }; - } - - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; - } - - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } - - /** - * calculate the velocity between two points. unit is in px per ms. - * @param {Number} deltaTime - * @param {Number} x - * @param {Number} y - * @return {Object} velocity `x` and `y` - */ - function getVelocity(deltaTime, x, y) { - return { - x: x / deltaTime || 0, - y: y / deltaTime || 0 - }; - } - - /** - * get the direction between two points - * @param {Number} x - * @param {Number} y - * @return {Number} direction - */ - function getDirection(x, y) { - if (x === y) { - return DIRECTION_NONE; - } - - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } - - /** - * calculate the absolute distance between two points - * @param {Object} p1 {x, y} - * @param {Object} p2 {x, y} - * @param {Array} [props] containing x and y keys - * @return {Number} distance - */ - function getDistance(p1, p2, props) { - if (!props) { - props = PROPS_XY; - } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - - return Math.sqrt((x * x) + (y * y)); - } - - /** - * calculate the angle between two coordinates - * @param {Object} p1 - * @param {Object} p2 - * @param {Array} [props] containing x and y keys - * @return {Number} angle - */ - function getAngle(p1, p2, props) { - if (!props) { - props = PROPS_XY; - } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; - return Math.atan2(y, x) * 180 / Math.PI; - } - - /** - * calculate the rotation degrees between two pointersets - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} rotation - */ - function getRotation(start, end) { - return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); - } - - /** - * calculate the scale factor between two pointersets - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} scale - */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } - - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END - }; - - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - - /** - * Mouse events input - * @constructor - * @extends Input - */ - function MouseInput() { - this.evEl = MOUSE_ELEMENT_EVENTS; - this.evWin = MOUSE_WINDOW_EVENTS; - - this.allow = true; // used by Input.TouchMouse to disable mouse events - this.pressed = false; // mousedown state - - Input.apply(this, arguments); - } - - inherit(MouseInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; - - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } - - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } - - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; - } - - if (eventType & INPUT_END) { - this.pressed = false; - } - - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); - } - }); - - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL - }; - - // in IE10 the pointer types is defined as an enum - var IE10_POINTER_TYPE_ENUM = { - 2: INPUT_TYPE_TOUCH, - 3: INPUT_TYPE_PEN, - 4: INPUT_TYPE_MOUSE, - 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 - }; - - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } - - /** - * Pointer events input - * @constructor - * @extends Input - */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; - - Input.apply(this, arguments); - - this.store = (this.manager.session.pointerEvents = []); - } - - inherit(PointerEventInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function PEhandler(ev) { - var store = this.store; - var removePointer = false; - - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - - var isTouch = (pointerType == INPUT_TYPE_TOUCH); - - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - - // start and mouse must be down - if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { - if (storeIndex < 0) { - store.push(ev); - storeIndex = store.length - 1; - } - } else if (eventType & (INPUT_END | INPUT_CANCEL)) { - removePointer = true; - } - - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; - } - - // update the event in the store - store[storeIndex] = ev; - - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); - - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); - } - } - }); - - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; - - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - - /** - * Touch events input - * @constructor - * @extends Input - */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; - - Input.apply(this, arguments); - } - - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; - } - - if (!this.started) { - return; - } - - var touches = normalizeSingleTouches.call(this, ev, type); - - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; - } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); - - /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] - */ - function normalizeSingleTouches(ev, type) { - var all = toArray(ev.touches); - var changed = toArray(ev.changedTouches); - - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); - } - - return [all, changed]; - } - - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; - - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - - /** - * Multi-user touch events input - * @constructor - * @extends Input - */ - function TouchInput() { - this.evTarget = TOUCH_TARGET_EVENTS; - this.targetIds = {}; - - Input.apply(this, arguments); - } - - inherit(TouchInput, Input, { - handler: function MTEhandler(ev) { - var type = TOUCH_INPUT_MAP[ev.type]; - var touches = getTouches.call(this, ev, type); - if (!touches) { - return; - } - - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); - - /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] - */ - function getTouches(ev, type) { - var allTouches = toArray(ev.touches); - var targetIds = this.targetIds; - - // when there is only one touch, the process can be simplified - if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { - targetIds[allTouches[0].identifier] = true; - return [allTouches, allTouches]; - } - - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; - - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); - }); - - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; - } - } - - // filter changed touches to only contain touches that exist in the collected target ids - i = 0; - while (i < changedTouches.length) { - if (targetIds[changedTouches[i].identifier]) { - changedTargetTouches.push(changedTouches[i]); - } - - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; - } - i++; - } - - if (!changedTargetTouches.length) { - return; - } - - return [ - // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' - uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), - changedTargetTouches - ]; - } - - /** - * Combined touch and mouse input - * - * Touch has a higher priority then mouse, and while touching no mouse events are allowed. - * This because touch devices also emit mouse events while doing a touch. - * - * @constructor - * @extends Input - */ - function TouchMouseInput() { - Input.apply(this, arguments); - - var handler = bindFn(this.handler, this); - this.touch = new TouchInput(this.manager, handler); - this.mouse = new MouseInput(this.manager, handler); - } - - inherit(TouchMouseInput, Input, { - /** - * handle mouse and touch events - * @param {Hammer} manager - * @param {String} inputEvent - * @param {Object} inputData - */ - handler: function TMEhandler(manager, inputEvent, inputData) { - var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), - isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); - - // when we're in a touch event, so block all upcoming mouse events - // most mobile browser also emit mouseevents, right after touchstart - if (isTouch) { - this.mouse.allow = false; - } else if (isMouse && !this.mouse.allow) { - return; - } - - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } - - this.callback(manager, inputEvent, inputData); - }, - - /** - * remove the event listeners - */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); - } - }); - - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - - // magical touchAction value - var TOUCH_ACTION_COMPUTE = 'compute'; - var TOUCH_ACTION_AUTO = 'auto'; - var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented - var TOUCH_ACTION_NONE = 'none'; - var TOUCH_ACTION_PAN_X = 'pan-x'; - var TOUCH_ACTION_PAN_Y = 'pan-y'; - - /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor - */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); - } - - TouchAction.prototype = { - /** - * set the touchAction value on the element or enable the polyfill - * @param {String} value - */ - set: function(value) { - // find out the touch-action by the event handlers - if (value == TOUCH_ACTION_COMPUTE) { - value = this.compute(); - } - - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; - } - this.actions = value.toLowerCase().trim(); - }, - - /** - * just re-set the touchAction value - */ - update: function() { - this.set(this.manager.options.touchAction); - }, - - /** - * compute the value for the touchAction property based on the recognizer's settings - * @returns {String} value - */ - compute: function() { - var actions = []; - each(this.manager.recognizers, function(recognizer) { - if (boolOrFn(recognizer.options.enable, [recognizer])) { - actions = actions.concat(recognizer.getTouchAction()); - } - }); - return cleanTouchActions(actions.join(' ')); - }, - - /** - * this method is called on each input cycle and provides the preventing of the browser behavior - * @param {Object} input - */ - preventDefaults: function(input) { - // not needed with native support for the touchAction property - if (NATIVE_TOUCH_ACTION) { - return; - } - - var srcEvent = input.srcEvent; - var direction = input.offsetDirection; - - // if the touch action did prevented once this session - if (this.manager.session.prevented) { - srcEvent.preventDefault(); - return; - } - - var actions = this.actions; - var hasNone = inStr(actions, TOUCH_ACTION_NONE); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); - } - }, - - /** - * call preventDefault to prevent the browser's default behavior (scrolling in most cases) - * @param {Object} srcEvent - */ - preventSrc: function(srcEvent) { - this.manager.session.prevented = true; - srcEvent.preventDefault(); - } - }; - - /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} - */ - function cleanTouchActions(actions) { - // none - if (inStr(actions, TOUCH_ACTION_NONE)) { - return TOUCH_ACTION_NONE; + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == "day") { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 24) * 24);break; + default: + clone.setHours(Math.round(clone.getHours() / 12) * 12);break; } - - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == "weekday") { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 12) * 12);break; + default: + clone.setHours(Math.round(clone.getHours() / 6) * 6);break; } - - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == "hour") { + switch (step) { + case 4: + clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60);break; + default: + clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30);break; } - - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (scale == "minute") { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 15: + case 10: + clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); + clone.setSeconds(0); + break; + case 5: + clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60);break; + default: + clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30);break; } + clone.setMilliseconds(0); + } else if (scale == "second") { + //noinspection FallthroughInSwitchStatementJS + switch (step) { + case 15: + case 10: + clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); + clone.setMilliseconds(0); + break; + case 5: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000);break; + default: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500);break; + } + } else if (scale == "millisecond") { + var _step = step > 5 ? step / 2 : 1; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); + } - return TOUCH_ACTION_AUTO; - } - - /** - * Recognizer flow explained; * - * All recognizers have the initial state of POSSIBLE when a input session starts. - * The definition of a input session is from the first input until the last input, with all it's movement in it. * - * Example session for mouse-input: mousedown -> mousemove -> mouseup - * - * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed - * which determines with state it should be. - * - * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to - * POSSIBLE to give it another change on the next cycle. - * - * Possible - * | - * +-----+---------------+ - * | | - * +-----+-----+ | - * | | | - * Failed Cancelled | - * +-------+------+ - * | | - * Recognized Began - * | - * Changed - * | - * Ended/Recognized - */ - var STATE_POSSIBLE = 1; - var STATE_BEGAN = 2; - var STATE_CHANGED = 4; - var STATE_ENDED = 8; - var STATE_RECOGNIZED = STATE_ENDED; - var STATE_CANCELLED = 16; - var STATE_FAILED = 32; - - /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options - */ - function Recognizer(options) { - this.id = uniqueId(); - - this.manager = null; - this.options = merge(options || {}, this.defaults); - - // default is enable true - this.options.enable = ifUndefined(this.options.enable, true); - - this.state = STATE_POSSIBLE; - - this.simultaneous = {}; - this.requireFail = []; - } - - Recognizer.prototype = { - /** - * @virtual - * @type {Object} - */ - defaults: {}, - - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); - - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; - }, - - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; - } - - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); - } - return this; - }, - - /** - * drop the simultaneous link. it doesnt remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRecognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { - return this; - } - - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; - }, - - /** - * recognizer can only run when an other is failing - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - requireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { - return this; - } - - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; - }, - - /** - * drop the requireFailure link. it does not remove the link on the other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - dropRequireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { - return this; - } - - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); - } - return this; - }, - - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; - }, - - /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} - */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; - }, - - /** - * You should use `tryEmit` instead of `emit` directly to check - * that all the needed recognizers has failed before emitting. - * @param {Object} input - */ - emit: function(input) { - var self = this; - var state = this.state; - - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } - - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); - } - - emit(); // simple 'eventName' events - - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); - } - }, - - /** - * Check that all the require failure recognizers has failed, - * if true, it emits a gesture event, - * otherwise, setup the state to FAILED. - * @param {Object} input - */ - tryEmit: function(input) { - if (this.canEmit()) { - return this.emit(input); - } - // it's failing anyway - this.state = STATE_FAILED; - }, - - /** - * can we emit? - * @returns {boolean} - */ - canEmit: function() { - var i = 0; - while (i < this.requireFail.length) { - if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { - return false; - } - i++; - } - return true; - }, - - /** - * update the recognizer - * @param {Object} inputData - */ - recognize: function(inputData) { - // make a new copy of the inputData - // so we can change the inputData without messing up the other recognizers - var inputDataClone = extend({}, inputData); - - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; - } - - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; - } - - this.state = this.process(inputDataClone); - - // the recognizer has recognized a gesture - // so trigger an event - if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { - this.tryEmit(inputDataClone); - } - }, - - /** - * return the state of the recognizer - * the actual recognizing happens in this method - * @virtual - * @param {Object} inputData - * @returns {Const} STATE - */ - process: function(inputData) { }, // jshint ignore:line - - /** - * return the preferred touch-action - * @virtual - * @returns {Array} - */ - getTouchAction: function() { }, - - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } + return clone; }; /** - * get a usable string, used as event postfix - * @param {Const} state - * @returns {String} state + * 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. */ - function stateStr(state) { - if (state & STATE_CANCELLED) { - return 'cancel'; - } else if (state & STATE_ENDED) { - return 'end'; - } else if (state & STATE_CHANGED) { - return 'move'; - } else if (state & STATE_BEGAN) { - return 'start'; + TimeStep.prototype.isMajor = function () { + if (this.switchedYear == true) { + this.switchedYear = false; + switch (this.scale) { + case "year": + case "month": + case "weekday": + case "day": + case "hour": + case "minute": + case "second": + case "millisecond": + return true; + default: + return false; } - return ''; - } - - /** - * direction cons to string - * @param {Const} direction - * @returns {String} - */ - function directionStr(direction) { - if (direction == DIRECTION_DOWN) { - return 'down'; - } else if (direction == DIRECTION_UP) { - return 'up'; - } else if (direction == DIRECTION_LEFT) { - return 'left'; - } else if (direction == DIRECTION_RIGHT) { - return 'right'; + } else if (this.switchedMonth == true) { + this.switchedMonth = false; + switch (this.scale) { + case "weekday": + case "day": + case "hour": + case "minute": + case "second": + case "millisecond": + return true; + default: + return false; } - return ''; - } - - /** - * get a recognizer by name if it is bound to a manager - * @param {Recognizer|String} otherRecognizer - * @param {Recognizer} recognizer - * @returns {Recognizer} - */ - function getRecognizerByNameIfManager(otherRecognizer, recognizer) { - var manager = recognizer.manager; - if (manager) { - return manager.get(otherRecognizer); + } else if (this.switchedDay == true) { + this.switchedDay = false; + switch (this.scale) { + case "millisecond": + case "second": + case "minute": + case "hour": + return true; + default: + return false; } - return otherRecognizer; - } - - /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer - */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } - - inherit(AttrRecognizer, Recognizer, { - /** - * @namespace - * @memberof AttrRecognizer - */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, + } - /** - * Used to check if it the recognizer receives valid input, like input.distance > 10. - * @memberof AttrRecognizer - * @param {Object} input - * @returns {Boolean} recognized - */ - attrTest: function(input) { - var optionPointers = this.options.pointers; - return optionPointers === 0 || input.pointers.length === optionPointers; - }, + switch (this.scale) { + case "millisecond": + return this.current.getMilliseconds() == 0; + case "second": + return this.current.getSeconds() == 0; + case "minute": + return this.current.getHours() == 0 && this.current.getMinutes() == 0; + case "hour": + return this.current.getHours() == 0; + case "weekday": + // intentional fall through + case "day": + return this.current.getDate() == 1; + case "month": + return this.current.getMonth() == 0; + case "year": + return false; + default: + return false; + } + }; - /** - * Process the input and return the state for the recognizer - * @memberof AttrRecognizer - * @param {Object} input - * @returns {*} State - */ - process: function(input) { - var state = this.state; - var eventType = input.eventType; - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); + /** + * 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; + } - // on cancel input and we've recognized before, return STATE_CANCELLED - if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { - return state | STATE_CANCELLED; - } else if (isRecognized || isValid) { - if (eventType & INPUT_END) { - return state | STATE_ENDED; - } else if (!(state & STATE_BEGAN)) { - return STATE_BEGAN; - } - return state | STATE_CHANGED; - } - return STATE_FAILED; - } - }); + var format = this.format.minorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ""; + }; /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer + * 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 */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); + TimeStep.prototype.getLabelMajor = function (date) { + if (date == undefined) { + date = this.current; + } - this.pX = null; - this.pY = null; - } + var format = this.format.majorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ""; + }; - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, + TimeStep.prototype.getClassName = function () { + var m = moment(this.current); + var date = m.locale ? m.locale("en") : m.lang("en"); // old versions of moment have .lang() function + var step = this.step; - getTouchAction: function() { - var direction = this.options.direction; - var actions = []; - if (direction & DIRECTION_HORIZONTAL) { - actions.push(TOUCH_ACTION_PAN_Y); - } - if (direction & DIRECTION_VERTICAL) { - actions.push(TOUCH_ACTION_PAN_X); - } - return actions; - }, + function even(value) { + return value / step % 2 == 0 ? " even" : " odd"; + } - directionTest: function(input) { - var options = this.options; - var hasMoved = true; - var distance = input.distance; - var direction = input.direction; - var x = input.deltaX; - var y = input.deltaY; + function today(date) { + if (date.isSame(new Date(), "day")) { + return " today"; + } + if (date.isSame(moment().add(1, "day"), "day")) { + return " tomorrow"; + } + if (date.isSame(moment().add(-1, "day"), "day")) { + return " yesterday"; + } + return ""; + } - // lock to axis? - if (!(direction & options.direction)) { - if (options.direction & DIRECTION_HORIZONTAL) { - direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; - hasMoved = x != this.pX; - distance = Math.abs(input.deltaX); - } else { - direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; - hasMoved = y != this.pY; - distance = Math.abs(input.deltaY); - } - } - input.direction = direction; - return hasMoved && distance > options.threshold && direction & options.direction; - }, + function currentWeek(date) { + return date.isSame(new Date(), "week") ? " current-week" : ""; + } - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, + function currentMonth(date) { + return date.isSame(new Date(), "month") ? " current-month" : ""; + } - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + function currentYear(date) { + return date.isSame(new Date(), "year") ? " current-year" : ""; + } - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + switch (this.scale) { + case "millisecond": + return even(date.milliseconds()).trim(); - this._super.emit.call(this, input); - } - }); + case "second": + return even(date.seconds()).trim(); - /** - * Pinch - * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). - * @constructor - * @extends AttrRecognizer - */ - function PinchRecognizer() { - AttrRecognizer.apply(this, arguments); - } + case "minute": + return even(date.minutes()).trim(); - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, + case "hour": + var hours = date.hours(); + if (this.step == 4) { + hours = hours + "-h" + (hours + 4); + } + return "h" + hours + today(date) + even(date.hours()); - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + case "weekday": + return date.format("dddd").toLowerCase() + today(date) + currentWeek(date) + even(date.date()); - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); - }, + case "day": + var day = date.date(); + var month = date.format("MMMM").toLowerCase(); + return "day" + day + " " + month + currentMonth(date) + even(day - 1); - emit: function(input) { - this._super.emit.call(this, input); - if (input.scale !== 1) { - var inOut = input.scale < 1 ? 'in' : 'out'; - this.manager.emit(this.options.event + inOut, input); - } - } - }); + case "month": + return date.format("MMMM").toLowerCase() + currentMonth(date) + even(date.month()); - /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer - */ - function PressRecognizer() { - Recognizer.apply(this, arguments); + case "year": + var year = date.year(); + return "year" + year + currentYear(date) + even(year); - this._timer = null; - this._input = null; - } + default: + return ""; + } + }; - inherit(PressRecognizer, Recognizer, { - /** - * @namespace - * @memberof PressRecognizer - */ - defaults: { - event: 'press', - pointers: 1, - time: 500, // minimal time of the pointer to be pressed - threshold: 5 // a minimal movement is ok, but keep it low - }, + module.exports = TimeStep; - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; - }, +/***/ }, +/* 20 */ +/***/ function(module, exports, __webpack_require__) { - process: function(input) { - var options = this.options; - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTime = input.deltaTime > options.time; + "use strict"; - this._input = input; + var Hammer = __webpack_require__(41); + var util = __webpack_require__(1); - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { - this.reset(); - } else if (input.eventType & INPUT_START) { - this.reset(); - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.time, this); - } else if (input.eventType & INPUT_END) { - return STATE_RECOGNIZED; - } - return STATE_FAILED; - }, + /** + * @constructor Item + * @param {Object} data Object containing (optional) parameters type, + * start, end, content, group, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} options Configuration options + * // TODO: describe available options + */ + function Item(data, conversion, options) { + this.id = null; + this.parent = null; + this.data = data; + this.dom = null; + this.conversion = conversion || {}; + this.options = options || {}; - reset: function() { - clearTimeout(this._timer); - }, + this.selected = false; + this.displayed = false; + this.dirty = true; - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; - } + this.top = null; + this.left = null; + this.width = null; + this.height = null; + } - if (input && (input.eventType & INPUT_END)) { - this.manager.emit(this.options.event + 'up', input); - } else { - this._input.timeStamp = now(); - this.manager.emit(this.options.event, this._input); - } - } - }); + Item.prototype.stack = true; /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer + * Select current item */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); - } + Item.prototype.select = function () { + this.selected = true; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 - }, + /** + * Unselect current item + */ + Item.prototype.unselect = function () { + this.selected = false; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + /** + * Set data for the item. Existing data will be updated. The id should not + * be changed. When the item is displayed, it will be redrawn immediately. + * @param {Object} data + */ + Item.prototype.setData = function (data) { + this.data = data; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + /** + * Set a parent for the item + * @param {ItemSet | Group} parent + */ + Item.prototype.setParent = function (parent) { + if (this.displayed) { + this.hide(); + this.parent = parent; + if (this.parent) { + this.show(); } - }); + } else { + this.parent = parent; + } + }; /** - * Swipe - * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. - * @constructor - * @extends AttrRecognizer + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible */ - function SwipeRecognizer() { - AttrRecognizer.apply(this, arguments); - } - - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 - }, - - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, - - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + Item.prototype.isVisible = function (range) { + // Should be implemented by Item implementations + return false; + }; - if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { - velocity = input.velocity; - } else if (direction & DIRECTION_HORIZONTAL) { - velocity = input.velocityX; - } else if (direction & DIRECTION_VERTICAL) { - velocity = input.velocityY; - } + /** + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed + */ + Item.prototype.show = function () { + return false; + }; - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; - }, + /** + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed + */ + Item.prototype.hide = function () { + return false; + }; - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + /** + * Repaint the item + */ + Item.prototype.redraw = function () {}; - this.manager.emit(this.options.event, input); - } - }); + /** + * Reposition the Item horizontally + */ + Item.prototype.repositionX = function () {}; /** - * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur - * between the given interval and position. The delay option can be used to recognize multi-taps without firing - * a single tap. - * - * The eventData from the emitted event contains the property `tapCount`, which contains the amount of - * multi-taps being recognized. - * @constructor - * @extends Recognizer + * Reposition the Item vertically */ - function TapRecognizer() { - Recognizer.apply(this, arguments); + Item.prototype.repositionY = function () {}; - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + /** + * Repaint a delete button on the top right of the item when the item is selected + * @param {HTMLElement} anchor + * @protected + */ + Item.prototype._repaintDeleteButton = function (anchor) { + if (this.selected && this.options.editable.remove && !this.dom.deleteButton) { + // create and show button + var me = this; - this._timer = null; - this._input = null; - this.count = 0; - } + var deleteButton = document.createElement("div"); + deleteButton.className = "delete"; + deleteButton.title = "Delete this item"; - inherit(TapRecognizer, Recognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'tap', - pointers: 1, - taps: 1, - interval: 300, // max time between the multi-tap taps - time: 250, // max time of the pointer to be down (like finger on the screen) - threshold: 2, // a minimal movement is ok, but keep it low - posThreshold: 10 // a multi-tap can be a bit off the initial position - }, + // TODO: be able to destroy the delete button + new Hammer(deleteButton).on("tap", function (event) { + me.parent.removeFromDataSet(me); + event.stopPropagation(); + event.preventDefault(); + }); - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; - }, + 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; + } + }; - process: function(input) { - var options = this.options; + /** + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private + */ + Item.prototype._updateContents = function (element) { + var content; + if (this.options.template) { + var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset + content = this.options.template(itemData); + } else { + content = this.data.content; + } - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; + if (content !== this.content) { + // only replace the content when changed + if (content instanceof Element) { + element.innerHTML = ""; + element.appendChild(content); + } else if (content != undefined) { + element.innerHTML = content; + } else { + if (!(this.data.type == "background" && this.data.content === undefined)) { + throw new Error("Property \"content\" missing in item " + this.id); + } + } - this.reset(); + this.content = content; + } + }; - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); - } + /** + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents + * @private + */ + Item.prototype._updateTitle = function (element) { + if (this.data.title != null) { + element.title = this.data.title || ""; + } else { + element.removeAttribute("title"); + } + }; - // we only allow little movement - // and we've reached an end event, so a tap is possible - if (validMovement && validTouchTime && validPointers) { - if (input.eventType != INPUT_END) { - return this.failTimeout(); - } + /** + * Process dataAttributes timeline option and set as data- attributes on dom.content + * @param {Element} element HTML element to which the attributes will be attached + * @private + */ + Item.prototype._updateDataAttributes = function (element) { + if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { + var attributes = []; - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + if (Array.isArray(this.options.dataAttributes)) { + attributes = this.options.dataAttributes; + } else if (this.options.dataAttributes == "all") { + attributes = Object.keys(this.data); + } else { + return; + } - this.pTime = input.timeStamp; - this.pCenter = input.center; + for (var i = 0; i < attributes.length; i++) { + var name = attributes[i]; + var value = this.data[name]; - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } + if (value != null) { + element.setAttribute("data-" + name, value); + } else { + element.removeAttribute("data-" + name); + } + } + } + }; - this._input = input; + /** + * Update custom styles of the element + * @param element + * @private + */ + Item.prototype._updateStyle = function (element) { + // remove old styles + if (this.style) { + util.removeCssText(element, this.style); + this.style = null; + } - // if tap count matches we have recognized it, - // else it has began recognizing... - var tapCount = this.count % options.taps; - if (tapCount === 0) { - // no failing requirements, immediately trigger the tap event - // or wait as long as the multitap interval to trigger - if (!this.hasRequireFailures()) { - return STATE_RECOGNIZED; - } else { - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.interval, this); - return STATE_BEGAN; - } - } - } - return STATE_FAILED; - }, + // append new styles + if (this.data.style) { + util.addCssText(element, this.data.style); + this.style = this.data.style; + } + }; - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; - }, + module.exports = Item; + // should be implemented by the item + // should be implemented by the item + // should be implemented by the item - reset: function() { - clearTimeout(this._timer); - }, +/***/ }, +/* 21 */ +/***/ function(module, exports, __webpack_require__) { - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); - } - } - }); + "use strict"; - /** - * Simple way to create an manager with a default set of recognizers. - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Hammer(element, options) { - options = options || {}; - options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); - return new Manager(element, options); - } + var Hammer = __webpack_require__(41); + var Item = __webpack_require__(20); + var BackgroundGroup = __webpack_require__(31); + var RangeItem = __webpack_require__(24); /** - * @const {string} + * @constructor BackgroundItem + * @extends Item + * @param {Object} data Object containing parameters start, end + * content, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} [options] Configuration options + * // TODO: describe options */ - Hammer.VERSION = '2.0.4'; + // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation + function BackgroundItem(data, conversion, options) { + this.props = { + content: { + width: 0 + } + }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true - /** - * default settings - * @namespace - */ - Hammer.defaults = { - /** - * set if DOM events are being triggered. - * But this is slower and unused by simple implementations, so disabled by default. - * @type {Boolean} - * @default false - */ - domEvents: false, + // 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); + } + } - /** - * The value for the touchAction property/fallback. - * When set to `compute` it will magically set the correct value based on the added recognizers. - * @type {String} - * @default compute - */ - touchAction: TOUCH_ACTION_COMPUTE, + Item.call(this, data, conversion, options); - /** - * @type {Boolean} - * @default true - */ - enable: true, + this.emptyContent = false; + } - /** - * EXPERIMENTAL FEATURE -- can be removed/changed - * Change the parent input target element. - * If Null, then it is being set the to main element. - * @type {Null|EventTarget} - * @default null - */ - inputTarget: null, + BackgroundItem.prototype = new Item(null, null, null); - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, + BackgroundItem.prototype.baseClassName = "item background"; + BackgroundItem.prototype.stack = false; - /** - * Default recognizer setup when calling `Hammer()` - * When creating a new Manager these will be skipped. - * @type {Array} - */ - preset: [ - // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] - [RotateRecognizer, { enable: false }], - [PinchRecognizer, { enable: false }, ['rotate']], - [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], - [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], - [TapRecognizer], - [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], - [PressRecognizer] - ], + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + BackgroundItem.prototype.isVisible = function (range) { + // determine visibility + return this.data.start < range.end && this.data.end > range.start; + }; - /** - * Some CSS properties can be used to improve the working of Hammer. - * Add them to this method and they will be set when creating a new Manager. - * @namespace - */ - cssProps: { - /** - * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userSelect: 'none', + /** + * Repaint the item + */ + BackgroundItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + // background box + dom.box = document.createElement("div"); + // className is updated in redraw() - /** - * Disables the default callout shown when you touch and hold a touch target. - * On iOS, when you touch and hold a touch target such as a link, Safari displays - * a callout containing information about the link. This property allows you to disable that callout. - * @type {String} - * @default 'none' - */ - touchCallout: 'none', + // contents box + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + // Note: we do NOT attach this item as attribute to the DOM, + // such that background items cannot be selected + //dom.box['timeline-item'] = this; - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', + this.dirty = true; + } - /** - * Overrides the highlight color shown when the user taps a link or a JavaScript - * clickable element in iOS. This property obeys the alpha value, if specified. - * @type {String} - * @default 'rgba(0,0,0,0)' - */ - tapHighlightColor: 'rgba(0,0,0,0)' + // append DOM to parent DOM + if (!this.parent) { + throw new Error("Cannot redraw item: no parent attached"); + } + if (!dom.box.parentNode) { + var background = this.parent.dom.background; + if (!background) { + throw new Error("Cannot redraw item: parent has no background container element"); } - }; - - var STOP = 1; - var FORCED_STOP = 2; + background.appendChild(dom.box); + } + this.displayed = true; - /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Manager(element, options) { - options = options || {}; + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.content); + this._updateDataAttributes(this.dom.content); + this._updateStyle(this.dom.box); - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; + // update class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = this.baseClassName + className; - this.handlers = {}; - this.session = {}; - this.recognizers = []; + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + // recalculate size + this.props.content.width = this.dom.content.offsetWidth; + this.height = 0; // set height zero, so this item will be ignored when stacking items - toggleCssProps(this, true); + this.dirty = false; + } + }; - each(options.recognizers, function(item) { - var recognizer = this.add(new (item[0])(item[1])); - item[2] && recognizer.recognizeWith(item[2]); - item[3] && recognizer.requireFailure(item[3]); - }, this); - } + /** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + */ + BackgroundItem.prototype.show = RangeItem.prototype.show; - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + /** + * Hide the item from the DOM (when visible) + * @return {Boolean} changed + */ + BackgroundItem.prototype.hide = RangeItem.prototype.hide; - // Options that need a little more setup - if (options.touchAction) { - this.touchAction.update(); - } - if (options.inputTarget) { - // Clean up existing event listeners and reinitialize - this.input.destroy(); - this.input.target = options.inputTarget; - this.input.init(); - } - return this; - }, + /** + * Reposition the item horizontally + * @Override + */ + BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; - /** - * stop recognizing for this session. - * This session will be discarded, when a new [input]start event is fired. - * When forced, the recognizer cycle is stopped immediately. - * @param {Boolean} [force] - */ - stop: function(force) { - this.session.stopped = force ? FORCED_STOP : STOP; - }, + /** + * Reposition the item vertically + * @Override + */ + BackgroundItem.prototype.repositionY = function (margin) { + var onTop = this.options.orientation === "top"; + this.dom.content.style.top = onTop ? "" : "0"; + this.dom.content.style.bottom = onTop ? "0" : ""; + var height; - /** - * run the recognizers! - * called by the inputHandler function on every movement of the pointers (touches) - * it walks through all the recognizers and tries to detect the gesture that is being made - * @param {Object} inputData - */ - recognize: function(inputData) { - var session = this.session; - if (session.stopped) { - return; + // special positioning for subgroups + if (this.data.subgroup !== undefined) { + var itemSubgroup = this.data.subgroup; + var subgroups = this.parent.subgroups; + var subgroupIndex = subgroups[itemSubgroup].index; + // if the orientation is top, we need to take the difference in height into account. + if (onTop == true) { + // the first subgroup will have to account for the distance from the top to the first item. + height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; + height += subgroupIndex == 0 ? margin.axis - 0.5 * margin.item.vertical : 0; + var newTop = this.parent.top; + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroupIndex) { + newTop += subgroups[subgroup].height + margin.item.vertical; + } } + } - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); - - var recognizer; - var recognizers = this.recognizers; + // the others will have to be offset downwards with this same distance. + newTop += subgroupIndex != 0 ? margin.axis - 0.5 * margin.item.vertical : 0; + this.dom.box.style.top = newTop + "px"; + this.dom.box.style.bottom = ""; + } + // and when the orientation is bottom: + else { + var newTop = this.parent.top; + for (var subgroup in subgroups) { + if (subgroups.hasOwnProperty(subgroup)) { + if (subgroups[subgroup].visible == true && subgroups[subgroup].index > subgroupIndex) { + newTop += subgroups[subgroup].height + margin.item.vertical; + } + } + } + height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; + this.dom.box.style.top = newTop + "px"; + this.dom.box.style.bottom = ""; + } + } + // and in the case of no subgroups: + else { + // we want backgrounds with groups to only show in groups. + if (this.parent instanceof BackgroundGroup) { + // if the item is not in a group: + height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); + this.dom.box.style.top = onTop ? "0" : ""; + this.dom.box.style.bottom = onTop ? "" : "0"; + } else { + height = this.parent.height; + // same alignment for items when orientation is top or bottom + this.dom.box.style.top = this.parent.top + "px"; + this.dom.box.style.bottom = ""; + } + } + this.dom.box.style.height = height + "px"; + }; - // this holds the recognizer that is being recognized. - // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED - // if no recognizer is detecting a thing, it is set to `null` - var curRecognizer = session.curRecognizer; + module.exports = BackgroundItem; - // reset when the last recognizer is recognized - // or when we're in a new session - if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { - curRecognizer = session.curRecognizer = null; - } +/***/ }, +/* 22 */ +/***/ function(module, exports, __webpack_require__) { - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; + "use strict"; - // find out if we are allowed try to recognize the input for this one. - // 1. allow if the session is NOT forced stopped (see the .stop() method) - // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one - // that is being recognized. - // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. - // this can be setup with the `recognizeWith()` method on the recognizer. - if (session.stopped !== FORCED_STOP && ( // 1 - !curRecognizer || recognizer == curRecognizer || // 2 - recognizer.canRecognizeWith(curRecognizer))) { // 3 - recognizer.recognize(inputData); - } else { - recognizer.reset(); - } + var Item = __webpack_require__(20); + var util = __webpack_require__(1); - // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the - // current active recognizer. but only if we don't already have an active recognizer - if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { - curRecognizer = session.curRecognizer = recognizer; - } - i++; - } + /** + * @constructor BoxItem + * @extends Item + * @param {Object} data Object containing parameters start + * content, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} [options] Configuration options + * // TODO: describe available options + */ + function BoxItem(data, conversion, options) { + this.props = { + dot: { + width: 0, + height: 0 }, + line: { + width: 0, + height: 0 + } + }; - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; - } + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data); + } + } - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } - } - return null; - }, + Item.call(this, data, conversion, options); + } - /** - * add a recognizer to the manager - * existing recognizers with the same event name will be removed - * @param {Recognizer} recognizer - * @returns {Recognizer|Manager} - */ - add: function(recognizer) { - if (invokeArrayArg(recognizer, 'add', this)) { - return this; - } + BoxItem.prototype = new Item(null, null, null); - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); - } + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + BoxItem.prototype.isVisible = function (range) { + // determine visibility + // TODO: account for the real width of the item. Right now we just add 1/4 to the window + var interval = (range.end - range.start) / 4; + return this.data.start > range.start - interval && this.data.start < range.end + interval; + }; - this.recognizers.push(recognizer); - recognizer.manager = this; + /** + * Repaint the item + */ + BoxItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - this.touchAction.update(); - return recognizer; - }, + // create main box + dom.box = document.createElement("DIV"); - /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} - */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', this)) { - return this; - } + // contents box (inside the background box). used for making margins + dom.content = document.createElement("DIV"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); + // line to axis + dom.line = document.createElement("DIV"); + dom.line.className = "line"; - this.touchAction.update(); - return this; - }, + // dot on axis + dom.dot = document.createElement("DIV"); + dom.dot.className = "dot"; - /** - * bind event - * @param {String} events - * @param {Function} handler - * @returns {EventEmitter} this - */ - on: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - handlers[event] = handlers[event] || []; - handlers[event].push(handler); - }); - return this; - }, + // attach this item as attribute + dom.box["timeline-item"] = this; - /** - * unbind event, leave emit blank to remove all handlers - * @param {String} events - * @param {Function} [handler] - * @returns {EventEmitter} this - */ - off: function(events, handler) { - var handlers = this.handlers; - each(splitStr(events), function(event) { - if (!handler) { - delete handlers[event]; - } else { - handlers[event].splice(inArray(handlers[event], handler), 1); - } - }); - return this; - }, + this.dirty = true; + } - /** - * emit event to the listeners - * @param {String} event - * @param {Object} data - */ - emit: function(event, data) { - // we also want to trigger dom events - if (this.options.domEvents) { - triggerDomEvent(event, data); - } + // append DOM to parent DOM + if (!this.parent) { + throw new Error("Cannot redraw item: no parent attached"); + } + if (!dom.box.parentNode) { + var foreground = this.parent.dom.foreground; + if (!foreground) throw new Error("Cannot redraw item: parent has no foreground container element"); + foreground.appendChild(dom.box); + } + if (!dom.line.parentNode) { + var background = this.parent.dom.background; + if (!background) throw new Error("Cannot redraw item: parent has no background container element"); + background.appendChild(dom.line); + } + if (!dom.dot.parentNode) { + var axis = this.parent.dom.axis; + if (!background) throw new Error("Cannot redraw item: parent has no axis container element"); + axis.appendChild(dom.dot); + } + this.displayed = true; - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { - return; - } + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.box); + this._updateDataAttributes(this.dom.box); + this._updateStyle(this.dom.box); - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; + // update class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = "item box" + className; + dom.line.className = "item line" + className; + dom.dot.className = "item dot" + className; - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; - } - }, + // recalculate size + this.props.dot.height = dom.dot.offsetHeight; + this.props.dot.width = dom.dot.offsetWidth; + this.props.line.width = dom.line.offsetWidth; + this.width = dom.box.offsetWidth; + this.height = dom.box.offsetHeight; - /** - * destroy the manager and unbinds all events - * it doesn't unbind dom events, that is the user own responsibility - */ - destroy: function() { - this.element && toggleCssProps(this, false); + this.dirty = false; + } - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; - } + this._repaintDeleteButton(dom.box); }; /** - * add/remove the css properties as defined in manager.options.cssProps - * @param {Manager} manager - * @param {Boolean} add + * Show the item in the DOM (when not already displayed). The items DOM will + * be created when needed. */ - function toggleCssProps(manager, add) { - var element = manager.element; - each(manager.options.cssProps, function(value, name) { - element.style[prefixed(element.style, name)] = add ? value : ''; - }); - } + BoxItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); + } + }; /** - * trigger dom event - * @param {String} event - * @param {Object} data + * Hide the item from the DOM (when visible) */ - function triggerDomEvent(event, data) { - var gestureEvent = document.createEvent('Event'); - gestureEvent.initEvent(event, true, true); - gestureEvent.gesture = data; - data.target.dispatchEvent(gestureEvent); - } + BoxItem.prototype.hide = function () { + if (this.displayed) { + var dom = this.dom; - extend(Hammer, { - INPUT_START: INPUT_START, - INPUT_MOVE: INPUT_MOVE, - INPUT_END: INPUT_END, - INPUT_CANCEL: INPUT_CANCEL, + 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); - STATE_POSSIBLE: STATE_POSSIBLE, - STATE_BEGAN: STATE_BEGAN, - STATE_CHANGED: STATE_CHANGED, - STATE_ENDED: STATE_ENDED, - STATE_RECOGNIZED: STATE_RECOGNIZED, - STATE_CANCELLED: STATE_CANCELLED, - STATE_FAILED: STATE_FAILED, + this.top = null; + this.left = null; - DIRECTION_NONE: DIRECTION_NONE, - DIRECTION_LEFT: DIRECTION_LEFT, - DIRECTION_RIGHT: DIRECTION_RIGHT, - DIRECTION_UP: DIRECTION_UP, - DIRECTION_DOWN: DIRECTION_DOWN, - DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, - DIRECTION_VERTICAL: DIRECTION_VERTICAL, - DIRECTION_ALL: DIRECTION_ALL, + this.displayed = false; + } + }; - Manager: Manager, - Input: Input, - TouchAction: TouchAction, + /** + * Reposition the item horizontally + * @Override + */ + BoxItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); + var align = this.options.align; + var left; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; - TouchInput: TouchInput, - MouseInput: MouseInput, - PointerEventInput: PointerEventInput, - TouchMouseInput: TouchMouseInput, - SingleTouchInput: SingleTouchInput, + // calculate left position of the box + if (align == "right") { + this.left = start - this.width; + } else if (align == "left") { + this.left = start; + } else { + // default or 'center' + this.left = start - this.width / 2; + } - Recognizer: Recognizer, - AttrRecognizer: AttrRecognizer, - Tap: TapRecognizer, - Pan: PanRecognizer, - Swipe: SwipeRecognizer, - Pinch: PinchRecognizer, - Rotate: RotateRecognizer, - Press: PressRecognizer, + // reposition box + box.style.left = this.left + "px"; - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed - }); + // reposition line + line.style.left = start - this.props.line.width / 2 + "px"; + + // reposition dot + dot.style.left = start - this.props.dot.width / 2 + "px"; + }; - if ("function" == TYPE_FUNCTION && __webpack_require__(22)) { - !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { - return Hammer; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof module != 'undefined' && module.exports) { - module.exports = Hammer; - } else { - window[exportName] = Hammer; - } + /** + * Reposition the item vertically + * @Override + */ + BoxItem.prototype.repositionY = function () { + var orientation = this.options.orientation; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; - })(window, document, 'Hammer'); + if (orientation == "top") { + box.style.top = (this.top || 0) + "px"; + line.style.top = "0"; + line.style.height = this.parent.top + this.top + 1 + "px"; + line.style.bottom = ""; + } else { + // orientation 'bottom' + var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty + var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top; -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { + box.style.top = (this.parent.height - this.top - this.height || 0) + "px"; + line.style.top = itemSetHeight - lineHeight + "px"; + line.style.bottom = "0"; + } - /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; + dot.style.top = -this.props.dot.height / 2 + "px"; + }; - /* WEBPACK VAR INJECTION */}.call(exports, {})) + module.exports = BoxItem; /***/ }, /* 23 */ @@ -12655,2225 +9885,2382 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - var util = __webpack_require__(1); - var hammerUtil = __webpack_require__(24); - var moment = __webpack_require__(2); - var Component = __webpack_require__(25); - var DateUtil = __webpack_require__(26); + var Item = __webpack_require__(20); /** - * @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 + * @constructor PointItem + * @extends Item + * @param {Object} data Object containing parameters start + * content, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} [options] Configuration options + * // TODO: describe available options */ - function Range(body, options) { - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add(-3, "days").valueOf(); // Number - this.end = now.clone().add(4, "days").valueOf(); // Number + function PointItem(data, conversion, options) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; - this.body = body; - this.deltaDifference = 0; - this.scaleOffset = 0; - this.startToFront = false; - this.endToFront = true; + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data); + } + } - // 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); + Item.call(this, data, conversion, options); + } - this.props = { - touch: {} - }; - this.animateTimer = null; + PointItem.prototype = new Item(null, null, null); - // drag listeners for dragging - this.body.emitter.on("panstart", this._onDragStart.bind(this)); - this.body.emitter.on("panmove", this._onDrag.bind(this)); - this.body.emitter.on("panend", this._onDragEnd.bind(this)); + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + PointItem.prototype.isVisible = function (range) { + // determine visibility + // TODO: account for the real width of the item. Right now we just add 1/4 to the window + var interval = (range.end - range.start) / 4; + return this.data.start > range.start - interval && this.data.start < range.end + interval; + }; - // ignore dragging when holding - this.body.emitter.on("press", this._onHold.bind(this)); + /** + * Repaint the item + */ + PointItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - // mouse wheel for zooming - this.body.emitter.on("mousewheel", this._onMouseWheel.bind(this)); + // background box + dom.point = document.createElement("div"); + // className is updated in redraw() - // pinch to zoom - this.body.emitter.on("touch", this._onTouch.bind(this)); - this.body.emitter.on("pinch", this._onPinch.bind(this)); + // contents box, right from the dot + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.point.appendChild(dom.content); - this.setOptions(options); - } + // dot at start + dom.dot = document.createElement("div"); + dom.point.appendChild(dom.dot); - Range.prototype = new Component(); + // attach this item as attribute + dom.point["timeline-item"] = this; - /** - * Set options for the range controller - * @param {Object} options Available options: - * {Number | Date | String} start Start date for the range - * {Number | Date | String} end End date for the range - * {Number} min Minimum value for start - * {Number} max Maximum value for end - * {Number} zoomMin Set a minimum value for - * (end - start). - * {Number} zoomMax Set a maximum value for - * (end - start). - * {Boolean} moveable Enable moving of the range - * by dragging. True by default - * {Boolean} zoomable Enable zooming of the range - * by pinching/scrolling. True by default - */ - Range.prototype.setOptions = function (options) { - if (options) { - // copy the options that we know - var fields = ["direction", "min", "max", "zoomMin", "zoomMax", "moveable", "zoomable", "activate", "hiddenDates"]; - util.selectiveExtend(fields, this.options, options); + this.dirty = true; + } - if ("start" in options || "end" in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); + // append DOM to parent DOM + if (!this.parent) { + throw new Error("Cannot redraw item: no parent attached"); + } + if (!dom.point.parentNode) { + var foreground = this.parent.dom.foreground; + if (!foreground) { + throw new Error("Cannot redraw item: parent has no foreground container element"); } + foreground.appendChild(dom.point); + } + this.displayed = true; + + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.point); + this._updateDataAttributes(this.dom.point); + this._updateStyle(this.dom.point); + + // update class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.point.className = "item point" + className; + dom.dot.className = "item dot" + className; + + // recalculate size + this.width = dom.point.offsetWidth; + this.height = dom.point.offsetHeight; + this.props.dot.width = dom.dot.offsetWidth; + this.props.dot.height = dom.dot.offsetHeight; + this.props.content.height = dom.content.offsetHeight; + + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + "px"; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + + dom.dot.style.top = (this.height - this.props.dot.height) / 2 + "px"; + dom.dot.style.left = this.props.dot.width / 2 + "px"; + + this.dirty = false; } + + this._repaintDeleteButton(dom.point); }; /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - function validateDirection(direction) { - if (direction != "horizontal" && direction != "vertical") { - throw new TypeError("Unknown direction \"" + direction + "\". " + "Choose \"horizontal\" or \"vertical\"."); + PointItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); } - } + }; /** - * Set a new start and end range - * @param {Date | Number | String} [start] - * @param {Date | Number | String} [end] - * @param {boolean | number} [animate=false] If true, the range is animated - * smoothly to the new window. - * If animate is a number, the - * number is taken as duration - * Default duration is 500 ms. - * @param {Boolean} [byUser=false] - * + * Hide the item from the DOM (when visible) */ - Range.prototype.setRange = function (start, end, animate, byUser) { - if (byUser !== true) { - byUser = false; + PointItem.prototype.hide = function () { + if (this.displayed) { + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); + } + + this.top = null; + this.left = null; + + this.displayed = false; } - var _start = start != undefined ? util.convert(start, "Date").valueOf() : null; - var _end = end != undefined ? util.convert(end, "Date").valueOf() : null; - this._cancelAnimation(); + }; - if (animate) { - var me = this; - var initStart = this.start; - var initEnd = this.end; - var duration = typeof animate === "number" ? animate : 500; - var initTime = new Date().valueOf(); - var anyChanged = false; + /** + * Reposition the item horizontally + * @Override + */ + PointItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); - var next = function () { - if (!me.props.touch.dragging) { - var now = new Date().valueOf(); - var time = now - initTime; - var done = time > duration; - var s = done || _start === null ? _start : util.easeInOutQuad(time, initStart, _start, duration); - var e = done || _end === null ? _end : util.easeInOutQuad(time, initEnd, _end, duration); + this.left = start - this.props.dot.width; - changed = me._applyRange(s, e); - DateUtil.updateHiddenDates(me.body, me.options.hiddenDates); - anyChanged = anyChanged || changed; - if (changed) { - me.body.emitter.emit("rangechange", { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); - } + // reposition point + this.dom.point.style.left = this.left + "px"; + }; - if (done) { - if (anyChanged) { - me.body.emitter.emit("rangechanged", { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); - } - } else { - // animate with as high as possible frame rate, leave 20 ms in between - // each to prevent the browser from blocking - me.animateTimer = setTimeout(next, 20); - } - } - }; + /** + * Reposition the item vertically + * @Override + */ + PointItem.prototype.repositionY = function () { + var orientation = this.options.orientation, + point = this.dom.point; - return next(); + if (orientation == "top") { + point.style.top = this.top + "px"; } else { - var changed = this._applyRange(_start, _end); - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - if (changed) { - var params = { start: new Date(this.start), end: new Date(this.end), byUser: byUser }; - this.body.emitter.emit("rangechange", params); - this.body.emitter.emit("rangechanged", params); - } + point.style.top = this.parent.height - this.top - this.height + "px"; } }; + module.exports = PointItem; + +/***/ }, +/* 24 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var Hammer = __webpack_require__(41); + var Item = __webpack_require__(20); + /** - * Stop an animation - * @private + * @constructor RangeItem + * @extends Item + * @param {Object} data Object containing parameters start, end + * content, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} [options] Configuration options + * // TODO: describe options */ - Range.prototype._cancelAnimation = function () { - if (this.animateTimer) { - clearTimeout(this.animateTimer); - this.animateTimer = null; + function RangeItem(data, conversion, options) { + this.props = { + content: { + width: 0 + } + }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true + + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data.id); + } + if (data.end == undefined) { + throw new Error("Property \"end\" missing in item " + data.id); + } } + + Item.call(this, data, conversion, options); + } + + RangeItem.prototype = new Item(null, null, null); + + RangeItem.prototype.baseClassName = "item range"; + + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + RangeItem.prototype.isVisible = function (range) { + // determine visibility + return this.data.start < range.end && this.data.end > range.start; }; /** - * 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 + * Repaint the item */ - 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; + RangeItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - // 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 + "\""); - } + // background box + dom.box = document.createElement("div"); + // className is updated in redraw() - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } + // contents box + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = min - newStart; - newStart += diff; - newEnd += diff; + // attach this item as attribute + dom.box["timeline-item"] = this; - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } + this.dirty = true; } - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = newEnd - max; - newStart -= diff; - newEnd -= diff; - - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } - } + // append DOM to parent DOM + if (!this.parent) { + throw new Error("Cannot redraw item: no parent attached"); } - - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if (newEnd - newStart < zoomMin) { - if (this.end - this.start === zoomMin && newStart > this.start && newEnd < this.end) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } else { - // zoom to the minimum - diff = zoomMin - (newEnd - newStart); - newStart -= diff / 2; - newEnd += diff / 2; - } + if (!dom.box.parentNode) { + var foreground = this.parent.dom.foreground; + if (!foreground) { + throw new Error("Cannot redraw item: parent has no foreground container element"); } + foreground.appendChild(dom.box); } + this.displayed = true; - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.box); + this._updateDataAttributes(this.dom.box); + this._updateStyle(this.dom.box); - if (newEnd - newStart > zoomMax) { - if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } else { - // zoom to the maximum - diff = newEnd - newStart - zoomMax; - newStart += diff / 2; - newEnd -= diff / 2; - } - } - } + // update class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = this.baseClassName + className; - var changed = this.start != newStart || this.end != newEnd; + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; - // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) - if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { - this.body.emitter.emit("checkRangedItems"); + // recalculate size + // turn off max-width to be able to calculate the real width + // this causes an extra browser repaint/reflow, but so be it + this.dom.content.style.maxWidth = "none"; + this.props.content.width = this.dom.content.offsetWidth; + this.height = this.dom.box.offsetHeight; + this.dom.content.style.maxWidth = ""; + + this.dirty = false; } - this.start = newStart; - this.end = newEnd; - return changed; + this._repaintDeleteButton(dom.box); + this._repaintDragLeft(); + this._repaintDragRight(); }; /** - * Retrieve the current range. - * @return {Object} An object with start and end properties + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - Range.prototype.getRange = function () { - return { - start: this.start, - end: this.end - }; + RangeItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); + } }; /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion + * Hide the item from the DOM (when visible) + * @return {Boolean} changed */ - Range.prototype.conversion = function (width, totalHidden) { - return Range.conversion(this.start, this.end, width, totalHidden); - }; + RangeItem.prototype.hide = function () { + if (this.displayed) { + var box = this.dom.box; - /** - * Static method to calculate the conversion offset and scale for a range, - * based on the provided start, end, and width - * @param {Number} start - * @param {Number} end - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.conversion = function (start, end, width, totalHidden) { - if (totalHidden === undefined) { - totalHidden = 0; - } - if (width != 0 && end - start != 0) { - return { - offset: start, - scale: width / (end - start - totalHidden) - }; - } else { - return { - offset: 0, - scale: 1 - }; + if (box.parentNode) { + box.parentNode.removeChild(box); + } + + this.top = null; + this.left = null; + + this.displayed = false; } }; /** - * Start dragging horizontally or vertically - * @param {Event} event - * @private + * Reposition the item horizontally + * @Override */ - Range.prototype._onDragStart = function (event) { - this.deltaDifference = 0; - this.previousDelta = 0; - // 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; - this.props.touch.dragging = true; + RangeItem.prototype.repositionX = function () { + var parentWidth = this.parent.width; + var start = this.conversion.toScreen(this.data.start); + var end = this.conversion.toScreen(this.data.end); + var contentLeft; + var contentWidth; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = "move"; + // 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); - event.preventDefault(); - }; + if (this.overflow) { + this.left = start; + this.width = boxWidth + this.props.content.width; + contentWidth = this.props.content.width; - /** - * Perform dragging operation - * @param {Event} event - * @private - */ - Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + // Note: The calculation of width is an optimistic calculation, giving + // a width which will not change when moving the Timeline + // So no re-stacking needed, which is nicer for the eye; + } else { + this.left = start; + this.width = boxWidth; + contentWidth = Math.min(end - start - 2 * this.options.padding, this.props.content.width); + } - // TODO: this may be redundant in hammerjs2 - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + this.dom.box.style.left = this.left + "px"; + this.dom.box.style.width = boxWidth + "px"; - var direction = this.options.direction; - validateDirection(direction); - var delta = direction == "horizontal" ? event.deltaX : event.deltaY; - delta -= this.deltaDifference; - var interval = this.props.touch.end - this.props.touch.start; + switch (this.options.align) { + case "left": + this.dom.content.style.left = "0"; + break; - // normalize dragging speed if cutout is in between. - var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - interval -= duration; + case "right": + this.dom.content.style.left = Math.max(boxWidth - contentWidth - 2 * this.options.padding, 0) + "px"; + break; - var width = direction == "horizontal" ? this.body.domProps.center.width : this.body.domProps.center.height; - var diffRange = -delta / width * interval; - var newStart = this.props.touch.start + diffRange; - var newEnd = this.props.touch.end + diffRange; + case "center": + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + "px"; + break; - // snapping times away from hidden zones - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, this.previousDelta - delta, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, this.previousDelta - delta, true); - if (safeStart != newStart || safeEnd != newEnd) { - this.deltaDifference += delta; - this.props.touch.start = safeStart; - this.props.touch.end = safeEnd; - this._onDrag(event); - return; + default: + // 'auto' + // when range exceeds left of the window, position the contents at the left of the visible area + if (this.overflow) { + if (end > 0) { + contentLeft = Math.max(-start, 0); + } else { + contentLeft = -contentWidth; // ensure it's not visible anymore + } + } else { + if (start < 0) { + contentLeft = Math.min(-start, end - start - contentWidth - 2 * this.options.padding); + // TODO: remove the need for options.padding. it's terrible. + } else { + contentLeft = 0; + } + } + this.dom.content.style.left = contentLeft + "px"; } + }; - this.previousDelta = delta; - this._applyRange(newStart, newEnd); - - // fire a rangechange event - this.body.emitter.emit("rangechange", { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + /** + * Reposition the item vertically + * @Override + */ + RangeItem.prototype.repositionY = function () { + var orientation = this.options.orientation, + box = this.dom.box; - event.preventDefault(); + if (orientation == "top") { + box.style.top = this.top + "px"; + } else { + box.style.top = this.parent.height - this.top - this.height + "px"; + } }; /** - * Stop dragging operation - * @param {event} event - * @private + * Repaint a drag area on the left side of the range when the range is selected + * @protected */ - Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + RangeItem.prototype._repaintDragLeft = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { + // create and show drag area + var dragLeft = document.createElement("div"); + dragLeft.className = "drag-left"; + dragLeft.dragLeftItem = this; - // TODO: this may be redundant in hammerjs2 - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + //// TODO: this should be redundant? + //Hammer(dragLeft, { + // preventDefault: true + //}).on('drag', function () { + // //console.log('drag left') + // }); - this.props.touch.dragging = false; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = "auto"; + 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; } - - // fire a rangechanged event - this.body.emitter.emit("rangechanged", { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event - * @private + * Repaint a drag area on the right side of the range when the range is selected + * @protected */ - Range.prototype._onMouseWheel = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + RangeItem.prototype._repaintDragRight = function () { + if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { + // create and show drag area + var dragRight = document.createElement("div"); + dragRight.className = "drag-right"; + dragRight.dragRightItem = this; - // 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; + //// TODO: this should be redundant? + //Hammer(dragRight, { + // preventDefault: true + //}).on('drag', function () { + // //console.log('drag right') + //}); + + this.dom.box.appendChild(dragRight); + this.dom.dragRight = dragRight; + } else if (!this.selected && this.dom.dragRight) { + // delete drag area + if (this.dom.dragRight.parentNode) { + this.dom.dragRight.parentNode.removeChild(this.dom.dragRight); + } + this.dom.dragRight = null; } + }; - // 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 + module.exports = RangeItem; - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - delta / 5; - } else { - scale = 1 / (1 + delta / 5); - } +/***/ }, +/* 25 */ +/***/ function(module, exports, __webpack_require__) { - // calculate center, the date to zoom around - var pointer = getPointer({ x: event.pageX, y: event.pageY }, this.body.dom.center); - var pointerDate = this._pointerToDate(pointer); + "use strict"; - this.zoom(scale, pointerDate, delta); - } + /** + * 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; + } - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); + /** + * 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); + } }; /** - * Start of a touch gesture - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Range.prototype._onTouch = function (event) { - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.allowDragging = true; - this.props.touch.center = null; - this.scaleOffset = 0; - this.deltaDifference = 0; + Component.prototype.redraw = function () { + // should be implemented by the component + return false; }; /** - * On start of a hold gesture - * @private + * Destroy the component. Cleanup DOM and event listeners */ - Range.prototype._onHold = function () { - this.props.touch.allowDragging = false; - }; + Component.prototype.destroy = function () {}; /** - * Handle pinch event - * @param {Event} event - * @private + * Test whether the component is resized since the last time _isResized() was + * called. + * @return {Boolean} Returns true if the component is resized + * @protected */ - Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + Component.prototype._isResized = function () { + var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - this.props.touch.allowDragging = false; + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.center, this.body.dom.center); - } + return resized; + }; - var scale = 1 / (event.scale + this.scaleOffset); - var centerDate = this._pointerToDate(this.props.touch.center); + module.exports = Component; + // should be implemented by the component - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; +/***/ }, +/* 26 */ +/***/ function(module, exports, __webpack_require__) { - // calculate new start and end - var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; - var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; + "use strict"; - // snapping times away from hidden zones - this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times - this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times + var util = __webpack_require__(1); + var Component = __webpack_require__(25); + var moment = __webpack_require__(40); + var locales = __webpack_require__(44); - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); - if (safeStart != newStart || safeEnd != newEnd) { - this.props.touch.start = safeStart; - this.props.touch.end = safeEnd; - this.scaleOffset = 1 - event.scale; - newStart = safeStart; - newEnd = safeEnd; - } + /** + * 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; - this.setRange(newStart, newEnd, false, true); + // default options + this.defaultOptions = { + showCurrentTime: true, - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + locales: locales, + locale: "en" + }; + this.options = util.extend({}, this.defaultOptions); + this.offset = 0; - event.preventDefault(); - }; + this._create(); + + this.setOptions(options); + } + + CurrentTime.prototype = new Component(); /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date + * Create the HTML DOM for the current time bar * @private */ - Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; - - validateDirection(direction); + CurrentTime.prototype._create = function () { + var bar = document.createElement("div"); + bar.className = "currenttime"; + bar.style.position = "absolute"; + bar.style.top = "0px"; + bar.style.height = "100%"; - if (direction == "horizontal") { - return this.body.util.toTime(pointer.x).valueOf(); - } else { - var height = this.body.domProps.center.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; - } + this.bar = bar; }; /** - * Get the pointer location relative to the location of the dom element - * @param {{x: Number, y: Number}} touch - * @param {Element} element HTML DOM element - * @return {{x: Number, y: Number}} pointer - * @private + * Destroy the CurrentTime bar */ - function getPointer(touch, element) { - return { - x: touch.x - util.getAbsoluteLeft(element), - y: touch.y - util.getAbsoluteTop(element) - }; - } + CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing + + this.body = null; + }; /** - * 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 options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] */ - Range.prototype.zoom = function (scale, center, delta) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } - - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - - // calculate new start and end - var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; - var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; - - // snapping times away from hidden zones - this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times - this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times - var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); - var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); - if (safeStart != newStart || safeEnd != newEnd) { - newStart = safeStart; - newEnd = safeEnd; + CurrentTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(["showCurrentTime", "locale", "locales"], this.options, options); } - - this.setRange(newStart, newEnd, false, true); - - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default }; + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + 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(); + } - /** - * 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; + var now = new Date(new Date().valueOf() + this.offset); + var x = this.body.util.toScreen(now); - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; + 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); - // TODO: reckon with min and max range + this.bar.style.left = x + "px"; + this.bar.title = title; + } else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + this.stop(); + } - this.start = newStart; - this.end = newEnd; + return false; }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * Start auto refreshing the current time bar */ - Range.prototype.moveTo = function (moveTo) { - var center = (this.start + this.end) / 2; - - var diff = center - moveTo; - - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + CurrentTime.prototype.start = function () { + var me = this; - this.setRange(newStart, newEnd); - }; + function update() { + me.stop(); - module.exports = Range; + // 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; -/***/ }, -/* 24 */ -/***/ function(module, exports, __webpack_require__) { + me.redraw(); - "use strict"; + // start a renderTimer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); + } - var Hammer = __webpack_require__(19); + update(); + }; /** - * Register a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Stop auto refreshing the current time bar */ - exports.onTouch = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFirst) { - callback(event); - } - }; - - hammer.on("hammer.input", callback.inputHandler); + CurrentTime.prototype.stop = function () { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } }; /** - * Register a release event, taking place after a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Set a current time. This can be used for example to ensure that a client's + * time is synchronized with a shared server time. + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. */ - exports.onRelease = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFinal) { - callback(event); - } - }; - - return hammer.on("hammer.input", callback.inputHandler); + CurrentTime.prototype.setCurrentTime = function (time) { + var t = util.convert(time, "Date").valueOf(); + var now = new Date().valueOf(); + this.offset = t - now; + this.redraw(); }; /** - * Unregister a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Get the current time. + * @return {Date} Returns the current time. */ - exports.offTouch = function (hammer, callback) { - hammer.off("hammer.input", callback.inputHandler); + CurrentTime.prototype.getCurrentTime = function () { + return new Date(new Date().valueOf() + this.offset); }; - /** - * Unregister a release event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) - */ - exports.offRelease = exports.offTouch; + module.exports = CurrentTime; /***/ }, -/* 25 */ +/* 27 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var Hammer = __webpack_require__(41); + var util = __webpack_require__(1); + var Component = __webpack_require__(25); + var moment = __webpack_require__(40); + var locales = __webpack_require__(44); + /** - * Prototype for visual components - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] - * @param {Object} [options] + * A custom time bar + * @param {{range: Range, dom: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCustomTime] + * @constructor CustomTime + * @extends Component */ - function Component(body, options) { - this.options = null; - this.props = null; + + function CustomTime(body, options) { + this.body = body; + + // default options + this.defaultOptions = { + showCustomTime: false, + locales: locales, + locale: "en", + id: 0 + }; + this.options = util.extend({}, this.defaultOptions); + + if (options && options.time) { + this.customTime = options.time; + } else { + this.customTime = new Date(); + } + + this.eventParams = {}; // stores state parameters while dragging the bar + + // create the DOM + this._create(); + + this.setOptions(options); } + CustomTime.prototype = new Component(); + /** - * Set options for the component. The new options will be merged into the - * current options. - * @param {Object} options + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCustomTime] */ - Component.prototype.setOptions = function (options) { + CustomTime.prototype.setOptions = function (options) { if (options) { - util.extend(this.options, options); + // copy all options that we know + util.selectiveExtend(["showCustomTime", "locale", "locales", "id"], this.options, options); + + // Triggered by addCustomTimeBar, redraw to add new bar + if (this.options.id) { + this.redraw(); + } } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Create the DOM for the custom time + * @private */ - Component.prototype.redraw = function () { - // should be implemented by the component - return false; + 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 = new Hammer(drag); + this.hammer.on("panstart", this._onDragStart.bind(this)); + this.hammer.on("panmove", this._onDrag.bind(this)); + this.hammer.on("panend", this._onDragEnd.bind(this)); + this.hammer.on("pan", function (event) { + event.preventDefault(); + }); }; /** - * Destroy the component. Cleanup DOM and event listeners + * Destroy the CustomTime bar */ - Component.prototype.destroy = function () {}; + CustomTime.prototype.destroy = function () { + this.options.showCustomTime = false; + this.redraw(); // will remove the bar from the DOM + + this.hammer.enable(false); + this.hammer = null; + + this.body = null; + }; /** - * Test whether the component is resized since the last time _isResized() was - * called. - * @return {Boolean} Returns true if the component is resized - * @protected + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Component.prototype._isResized = function () { - var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; + 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); + } - return resized; - }; + var x = this.body.util.toScreen(this.customTime); - module.exports = Component; - // should be implemented by the component + 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); -/***/ }, -/* 26 */ -/***/ function(module, exports, __webpack_require__) { + this.bar.style.left = x + "px"; + this.bar.title = title; + } else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + } - "use strict"; + return false; + }; /** - * Created by Alex on 10/3/2014. + * Set custom time. + * @param {Date | number | string} time */ - var moment = __webpack_require__(2); - + CustomTime.prototype.setCustomTime = function (time) { + this.customTime = util.convert(time, "Date"); + this.redraw(); + }; /** - * used in Core to convert the options into a volatile variable - * - * @param Core + * Retrieve the current custom time. + * @return {Date} customTime */ - exports.convertHiddenOptions = function (body, hiddenDates) { - body.hiddenDates = []; - if (hiddenDates) { - if (Array.isArray(hiddenDates) == true) { - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat === undefined) { - var dateItem = {}; - dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); - dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); - body.hiddenDates.push(dateItem); - } - } - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time - } - } + CustomTime.prototype.getCustomTime = function () { + return new Date(this.customTime.valueOf()); }; - /** - * create new entrees for the repeating hidden dates - * @param body - * @param hiddenDates + * Start moving horizontally + * @param {Event} event + * @private */ - exports.updateHiddenDates = function (body, hiddenDates) { - if (hiddenDates && body.domProps.centerContainer.width !== undefined) { - exports.convertHiddenOptions(body, hiddenDates); + CustomTime.prototype._onDragStart = function (event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; - var start = moment(body.range.start); - var end = moment(body.range.end); + event.stopPropagation(); + event.preventDefault(); + }; - var totalRange = body.range.end - body.range.start; - var pixelTime = totalRange / body.domProps.centerContainer.width; + /** + * Perform moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat !== undefined) { - var startDate = moment(hiddenDates[i].start); - var endDate = moment(hiddenDates[i].end); + var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; + var time = this.body.util.toTime(x); - if (startDate._d == "Invalid Date") { - throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); - } - if (endDate._d == "Invalid Date") { - throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); - } + this.setCustomTime(time); - var duration = endDate - startDate; - if (duration >= 4 * pixelTime) { - var offset = 0; - var runUntil = end.clone(); - switch (hiddenDates[i].repeat) { - case "daily": - // case of time - if (startDate.day() != endDate.day()) { - offset = 1; - } - startDate.dayOfYear(start.dayOfYear()); - startDate.year(start.year()); - startDate.subtract(7, "days"); + // fire a timechange event + this.body.emitter.emit("timechange", { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - endDate.dayOfYear(start.dayOfYear()); - endDate.year(start.year()); - endDate.subtract(7 - offset, "days"); + event.stopPropagation(); + event.preventDefault(); + }; - runUntil.add(1, "weeks"); - break; - case "weekly": - var dayOffset = endDate.diff(startDate, "days"); - var day = startDate.day(); + /** + * Stop moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; - // set the start date to the range.start - startDate.date(start.date()); - startDate.month(start.month()); - startDate.year(start.year()); - endDate = startDate.clone(); + // fire a timechanged event + this.body.emitter.emit("timechanged", { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - // force - startDate.day(day); - endDate.day(day); - endDate.add(dayOffset, "days"); + event.stopPropagation(); + event.preventDefault(); + }; - startDate.subtract(1, "weeks"); - endDate.subtract(1, "weeks"); + module.exports = CustomTime; - runUntil.add(1, "weeks"); - break; - case "monthly": - if (startDate.month() != endDate.month()) { - offset = 1; - } - startDate.month(start.month()); - startDate.year(start.year()); - startDate.subtract(1, "months"); +/***/ }, +/* 28 */ +/***/ function(module, exports, __webpack_require__) { - endDate.month(start.month()); - endDate.year(start.year()); - endDate.subtract(1, "months"); - endDate.add(offset, "months"); + "use strict"; - runUntil.add(1, "months"); - break; - case "yearly": - if (startDate.year() != endDate.year()) { - offset = 1; - } - startDate.year(start.year()); - startDate.subtract(1, "years"); - endDate.year(start.year()); - endDate.subtract(1, "years"); - endDate.add(offset, "years"); + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(2); + var Component = __webpack_require__(25); + var DataStep = __webpack_require__(16); - runUntil.add(1, "years"); - break; - default: - console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); - return; - } - while (startDate < runUntil) { - body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); - switch (hiddenDates[i].repeat) { - case "daily": - startDate.add(1, "days"); - endDate.add(1, "days"); - break; - case "weekly": - startDate.add(1, "weeks"); - endDate.add(1, "weeks"); - break; - case "monthly": - startDate.add(1, "months"); - endDate.add(1, "months"); - break; - case "yearly": - startDate.add(1, "y"); - endDate.add(1, "y"); - break; - default: - console.log("Wrong repeat format, allowed are: daily, weekly, monthly, yearly. Given:", hiddenDates[i].repeat); - return; - } - } - body.hiddenDates.push({ start: startDate.valueOf(), end: endDate.valueOf() }); - } - } - } - // remove duplicates, merge where possible - exports.removeDuplicates(body); - // ensure the new positions are not on hidden dates - var startHidden = exports.isHidden(body.range.start, body.hiddenDates); - var endHidden = exports.isHidden(body.range.end, body.hiddenDates); - var rangeStart = body.range.start; - var rangeEnd = body.range.end; - if (startHidden.hidden == true) { - rangeStart = body.range.startToFront == true ? startHidden.startDate - 1 : startHidden.endDate + 1; - } - if (endHidden.hidden == true) { - rangeEnd = body.range.endToFront == true ? endHidden.startDate - 1 : endHidden.endDate + 1; - } - if (startHidden.hidden == true || endHidden.hidden == true) { - body.range._applyRange(rangeStart, rangeEnd); + /** + * A horizontal time axis + * @param {Object} [options] See DataAxis.setOptions for the available + * options. + * @constructor DataAxis + * @extends Component + * @param body + */ + function DataAxis(body, options, svg, linegraphOptions) { + this.id = util.randomUUID(); + this.body = body; + + this.defaultOptions = { + orientation: "left", // supported: 'left', 'right' + showMinorLabels: true, + showMajorLabels: true, + icons: true, + majorLinesOffset: 7, + minorLinesOffset: 4, + labelOffsetX: 10, + labelOffsetY: 2, + iconWidth: 20, + width: "40px", + visible: true, + alignZeros: true, + customRange: { + left: { min: undefined, max: undefined }, + right: { min: undefined, max: undefined } + }, + title: { + left: { text: undefined }, + right: { text: undefined } + }, + format: { + left: { decimals: undefined }, + right: { decimals: undefined } } - } - }; + }; + this.linegraphOptions = linegraphOptions; + this.linegraphSVG = svg; + this.props = {}; + this.DOMelements = { // dynamic elements + lines: {}, + labels: {}, + title: {} + }; - /** - * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. - * Scales with N^2 - * @param body - */ - exports.removeDuplicates = function (body) { - var hiddenDates = body.hiddenDates; - var safeDates = []; - for (var i = 0; i < hiddenDates.length; i++) { - for (var j = 0; j < hiddenDates.length; j++) { - if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { - // j inside i - if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[j].remove = true; - } - // j start inside i - else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { - hiddenDates[i].end = hiddenDates[j].end; - hiddenDates[j].remove = true; - } - // j end inside i - else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[i].start = hiddenDates[j].start; - hiddenDates[j].remove = true; - } - } - } - } + this.dom = {}; - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].remove !== true) { - safeDates.push(hiddenDates[i]); - } - } + this.range = { start: 0, end: 0 }; - body.hiddenDates = safeDates; - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time - }; + this.options = util.extend({}, this.defaultOptions); + this.conversionFactor = 1; - exports.printDates = function (dates) { - for (var i = 0; i < dates.length; i++) { - console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); - } - }; + this.setOptions(options); + this.width = Number(("" + this.options.width).replace("px", "")); + this.minWidth = this.width; + this.height = this.linegraphSVG.offsetHeight; + this.hidden = false; - /** - * Used in TimeStep to avoid the hidden times. - * @param timeStep - * @param previousTime - */ - exports.stepOverHiddenDates = function (timeStep, previousTime) { - var stepInHidden = false; - var currentValue = timeStep.current.valueOf(); - for (var i = 0; i < timeStep.hiddenDates.length; i++) { - var startDate = timeStep.hiddenDates[i].start; - var endDate = timeStep.hiddenDates[i].end; - if (currentValue >= startDate && currentValue < endDate) { - stepInHidden = true; - break; - } - } + this.stepPixels = 25; + this.stepPixelsForced = 25; + this.zeroCrossing = -1; - if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { - var prevValue = moment(previousTime); - var newValue = moment(endDate); - //check if the next step should be major - if (prevValue.year() != newValue.year()) { - timeStep.switchedYear = true; - } else if (prevValue.month() != newValue.month()) { - timeStep.switchedMonth = true; - } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { - timeStep.switchedDay = true; - } + this.lineOffset = 0; + this.master = true; + this.svgElements = {}; + this.iconsRemoved = false; - timeStep.current = newValue.toDate(); - } - }; + this.groups = {}; + this.amountOfGroups = 0; - ///** - // * Used in TimeStep to avoid the hidden times. - // * @param timeStep - // * @param previousTime - // */ - //exports.checkFirstStep = function(timeStep) { - // var stepInHidden = false; - // var currentValue = timeStep.current.valueOf(); - // for (var i = 0; i < timeStep.hiddenDates.length; i++) { - // var startDate = timeStep.hiddenDates[i].start; - // var endDate = timeStep.hiddenDates[i].end; - // if (currentValue >= startDate && currentValue < endDate) { - // stepInHidden = true; - // break; - // } - // } - // - // if (stepInHidden == true && currentValue <= timeStep._end.valueOf()) { - // var newValue = moment(endDate); - // timeStep.current = newValue.toDate(); - // } - //}; + // create the HTML DOM + this._create(); - /** - * replaces the Core toScreen methods - * @param Core - * @param time - * @param width - * @returns {number} - */ - exports.toScreen = function (Core, time, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return (time.valueOf() - conversion.offset) * conversion.scale; - } else { - var hidden = exports.isHidden(time, Core.body.hiddenDates); - if (hidden.hidden == true) { - time = hidden.startDate; - } + var me = this; + this.body.emitter.on("verticalDrag", function () { + me.dom.lineContainer.style.top = me.body.domProps.scrollTop + "px"; + }); + } - var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); + DataAxis.prototype = new Component(); - var conversion = Core.range.conversion(width, duration); - return (time.valueOf() - conversion.offset) * conversion.scale; + + DataAxis.prototype.addGroup = function (label, graphOptions) { + if (!this.groups.hasOwnProperty(label)) { + this.groups[label] = graphOptions; } + this.amountOfGroups += 1; }; + DataAxis.prototype.updateGroup = function (label, graphOptions) { + this.groups[label] = graphOptions; + }; - /** - * Replaces the core toTime methods - * @param body - * @param range - * @param x - * @param width - * @returns {Date} - */ - exports.toTime = function (Core, x, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return new Date(x / conversion.scale + conversion.offset); - } else { - var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - var totalDuration = Core.range.end - Core.range.start - hiddenDuration; - var partialDuration = totalDuration * x / width; - var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); - - var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); - return newTime; + DataAxis.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; } }; - /** - * Support function - * - * @param hiddenDates - * @param range - * @returns {number} - */ - exports.getHiddenDurationBetween = function (hiddenDates, start, end) { - var duration = 0; - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - // if time after the cutout, and the - if (startDate >= start && endDate < end) { - duration += endDate - startDate; + DataAxis.prototype.setOptions = function (options) { + if (options) { + var redraw = false; + if (this.options.orientation != options.orientation && options.orientation !== undefined) { + redraw = true; + } + var fields = ["orientation", "showMinorLabels", "showMajorLabels", "icons", "majorLinesOffset", "minorLinesOffset", "labelOffsetX", "labelOffsetY", "iconWidth", "width", "visible", "customRange", "title", "format", "alignZeros"]; + util.selectiveExtend(fields, this.options, options); + + this.minWidth = Number(("" + this.options.width).replace("px", "")); + + if (redraw == true && this.dom.frame) { + this.hide(); + this.show(); } } - return duration; }; /** - * Support function - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} + * Create the HTML DOM for the DataAxis */ - exports.correctTimeForHidden = function (hiddenDates, range, time) { - time = moment(time).toDate().valueOf(); - time -= exports.getHiddenDurationBefore(hiddenDates, range, time); - return time; + 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; + + this.dom.lineContainer = document.createElement("div"); + this.dom.lineContainer.style.width = "100%"; + this.dom.lineContainer.style.height = this.height; + this.dom.lineContainer.style.position = "relative"; + + // 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); }; - exports.getHiddenDurationBefore = function (hiddenDates, range, time) { - var timeOffset = 0; - time = moment(time).toDate().valueOf(); + DataAxis.prototype._redrawGroupIcons = function () { + DOMutil.prepareElements(this.svgElements); - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - // if time after the cutout, and the - if (startDate >= range.start && endDate < range.end) { - if (time >= endDate) { - timeOffset += endDate - startDate; - } - } + var x; + var iconWidth = this.options.iconWidth; + var iconHeight = 15; + var iconOffset = 4; + var y = iconOffset + 0.5 * iconHeight; + + if (this.options.orientation == "left") { + x = iconOffset; + } else { + x = this.width - iconWidth - iconOffset; } - return timeOffset; - }; - /** - * sum the duration from start to finish, including the hidden duration, - * until the required amount has been reached, return the accumulated hidden duration - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} - */ - exports.getAccumulatedHiddenDuration = function (hiddenDates, range, requiredDuration) { - var hiddenDuration = 0; - var duration = 0; - var previousPoint = range.start; - //exports.printDates(hiddenDates) - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - // if time after the cutout, and the - if (startDate >= range.start && endDate < range.end) { - duration += startDate - previousPoint; - previousPoint = endDate; - if (duration >= requiredDuration) { - break; - } else { - hiddenDuration += endDate - startDate; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); + y += iconHeight + iconOffset; } } } - return hiddenDuration; + DOMutil.cleanupElements(this.svgElements); + this.iconsRemoved = false; }; - + DataAxis.prototype._cleanupIcons = function () { + if (this.iconsRemoved == false) { + DOMutil.prepareElements(this.svgElements); + DOMutil.cleanupElements(this.svgElements); + this.iconsRemoved = true; + } + }; /** - * used to step over to either side of a hidden block. Correction is disabled on tablets, might be set to true - * @param hiddenDates - * @param time - * @param direction - * @param correctionEnabled - * @returns {*} + * Create the HTML DOM for the DataAxis */ - exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { - var isHidden = exports.isHidden(time, hiddenDates); - if (isHidden.hidden == true) { - if (direction < 0) { - if (correctionEnabled == true) { - return isHidden.startDate - (isHidden.endDate - time) - 1; - } else { - return isHidden.startDate - 1; - } + DataAxis.prototype.show = function () { + this.hidden = false; + if (!this.dom.frame.parentNode) { + if (this.options.orientation == "left") { + this.body.dom.left.appendChild(this.dom.frame); } else { - if (correctionEnabled == true) { - return isHidden.endDate + (time - isHidden.startDate) + 1; - } else { - return isHidden.endDate + 1; - } + this.body.dom.right.appendChild(this.dom.frame); } - } else { - return time; } - }; + if (!this.dom.lineContainer.parentNode) { + this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); + } + }; /** - * Check if a time is hidden - * - * @param time - * @param hiddenDates - * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} + * Create the HTML DOM for the DataAxis */ - exports.isHidden = function (time, hiddenDates) { - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; + DataAxis.prototype.hide = function () { + this.hidden = true; + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } - if (time >= startDate && time < endDate) { - // if the start is entering a hidden zone - return { hidden: true, startDate: startDate, endDate: endDate }; - break; + if (this.dom.lineContainer.parentNode) { + this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); + } + }; + + /** + * Set a range (start and end) + * @param end + * @param start + * @param end + */ + DataAxis.prototype.setRange = function (start, end) { + if (this.master == false && this.options.alignZeros == true && this.zeroCrossing != -1) { + if (start > 0) { + start = 0; } } - return { hidden: false, startDate: startDate, endDate: endDate }; + this.range.start = start; + this.range.end = end; }; -/***/ }, -/* 27 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + DataAxis.prototype.redraw = function () { + var resized = false; + var activeGroups = 0; - "use strict"; + // Make sure the line container adheres to the vertical scrolling. + this.dom.lineContainer.style.top = this.body.domProps.scrollTop + "px"; - var Emitter = __webpack_require__(11); - var Hammer = __webpack_require__(19); - var hammerUtil = __webpack_require__(24); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); - var Range = __webpack_require__(23); - var ItemSet = __webpack_require__(28); - var Activator = __webpack_require__(38); - var DateUtil = __webpack_require__(26); - var CustomTime = __webpack_require__(40); + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + activeGroups++; + } + } + } + if (this.amountOfGroups == 0 || activeGroups == 0) { + this.hide(); + } else { + this.show(); + this.height = Number(this.linegraphSVG.style.height.replace("px", "")); - /** - * 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 - */ - function Core() {} + // 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; - // turn Core into an event emitter - Emitter(Core.prototype); + var props = this.props; + var frame = this.dom.frame; - /** - * 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 - */ - Core.prototype._create = function (container) { - this.dom = {}; + // update classname + frame.className = "dataaxis"; - 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"); + // calculate character width and height + this._calculateCharSize(); - this.dom.root.className = "vis timeline root"; - 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"; + var orientation = this.options.orientation; + var showMinorLabels = this.options.showMinorLabels; + var showMajorLabels = this.options.showMajorLabels; - 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); + // determine the width and height of the elements for the axis + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); + props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset; + props.minorLineHeight = 1; + props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset; + props.majorLineHeight = 1; - this.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); + // take frame offline while updating (is almost twice as fast) + if (orientation == "left") { + frame.style.top = "0"; + frame.style.left = "0"; + frame.style.bottom = ""; + frame.style.width = this.width + "px"; + frame.style.height = this.height + "px"; + this.props.width = this.body.domProps.left.width; + this.props.height = this.body.domProps.left.height; + } else { + // right + frame.style.top = ""; + frame.style.bottom = "0"; + frame.style.left = "0"; + frame.style.width = this.width + "px"; + frame.style.height = this.height + "px"; + this.props.width = this.body.domProps.right.width; + this.props.height = this.body.domProps.right.height; + } - this.on("rangechange", this.redraw.bind(this)); + resized = this._redrawLabels(); + resized = this._isResized() || resized; - var me = this; - this.on("change", function (properties) { - if (properties && properties.queue == true) { - // redraw once on next tick - if (!me._redrawTimer) { - me._redrawTimer = setTimeout(function () { - me._redrawTimer = null; - me._redraw(); - }, 0); - } + if (this.options.icons == true) { + this._redrawGroupIcons(); } else { - // redraw immediately - me._redraw(); + this._cleanupIcons(); } - }); - // create event listeners for all interesting events, these events will be - // emitted via emitter - this.hammer = new Hammer(this.dom.root, { touchAction: "pan-y" }); - this.hammer.get("pinch").set({ enable: true }); - this.listeners = {}; + this._redrawTitle(orientation); + } + return resized; + }; - var events = ["tap", "doubletap", "press", "pinch", "pan", "panstart", "panmove", "panend" - // TODO: cleanup - //'touch', 'pinch', - //'tap', 'doubletap', 'hold', - //'dragstart', 'drag', 'dragend', - //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (type) { - var listener = function (event) { - if (me.isActive()) { - me.emit(type, event); - } - }; - me.hammer.on(type, listener); - me.listeners[type] = listener; - }); + /** + * Repaint major and minor text labels and vertical grid lines + * @private + */ + DataAxis.prototype._redrawLabels = function () { + var resized = false; + DOMutil.prepareElements(this.DOMelements.lines); + DOMutil.prepareElements(this.DOMelements.labels); - // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) - hammerUtil.onTouch(this.hammer, (function (event) { - me.emit("touch", event); - }).bind(this)); + var orientation = this.options.orientation; - function onMouseWheel(event) { - if (me.isActive()) { - me.emit("mousewheel", event); - } - } - this.dom.root.addEventListener("mousewheel", onMouseWheel); - this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel); + // 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; - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; + var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation], this.master == false && this.options.alignZeros // doess the step have to align zeros? only if not master and the options is on + ); - this.redrawCount = 0; + 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); - // attach the root panel to the provided container - if (!container) throw new Error("No container provided"); - container.appendChild(this.dom.root); - }; + this.stepPixels = stepPixels; - /** - * 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 - */ - Core.prototype.setOptions = function (options) { - if (options) { - // copy the known options - var fields = ["width", "height", "minHeight", "maxHeight", "autoResize", "start", "end", "orientation", "clickToUse", "dataAttributes", "hiddenDates"]; - util.selectiveExtend(fields, this.options, options); + var amountOfSteps = this.height / stepPixels; + var stepDifference = 0; - if ("hiddenDates" in this.options) { - DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); + // the slave axis needs to use the same horizontal lines as the master axis. + 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; - if ("clickToUse" in options) { - if (options.clickToUse) { - if (!this.activator) { - this.activator = new Activator(this.dom.root); + if (this.zeroCrossing != -1 && this.options.alignZeros == true) { + var zeroStepDifference = step.marginEnd / step.step - this.zeroCrossing; + if (zeroStepDifference > 0) { + for (var i = 0; i < zeroStepDifference; i++) { + step.next(); } - } else { - if (this.activator) { - this.activator.destroy(); - delete this.activator; + } else if (zeroStepDifference < 0) { + for (var i = 0; i < -zeroStepDifference; i++) { + step.previous(); } } } - - // enable/disable autoResize - this._initAutoResize(); + } else { + amountOfSteps += 0.25; } - // propagate options to all components - this.components.forEach(function (component) { - return component.setOptions(options); - }); - // 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.valueAtZero = step.marginEnd; + var marginStartPos = 0; - // redraw everything - this._redraw(); - }; + // do not draw the first label + var max = 1; - /** - * Returns true when the Timeline is active. - * @returns {boolean} - */ - Core.prototype.isActive = function () { - return !this.activator || this.activator.active; - }; + // Get the number of decimal places + var decimals; + if (this.options.format[orientation] !== undefined) { + decimals = this.options.format[orientation].decimals; + } - /** - * Destroy the Core, clean up all DOM elements and event listeners. - */ - Core.prototype.destroy = function () { - // unbind datasets - this.clear(); + 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(); - // remove all event listeners - this.off(); + if (this.options.showMinorLabels && isMajor == false || this.master == false && this.options.showMinorLabels == true) { + this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, "yAxis minor", this.props.minorCharHeight); + } - // stop checking for changed size - this._stopAutoResize(); + 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(decimals), orientation, "yAxis major", this.props.majorCharHeight); + } + 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); + } - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); + if (this.master == true && step.current == 0) { + this.zeroCrossing = max; + } + + max++; } - this.dom = null; - // remove Activator - if (this.activator) { - this.activator.destroy(); - delete this.activator; + if (this.master == false) { + this.conversionFactor = y / (this.valueAtZero - step.current); + } else { + this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; } - // cleanup hammer touch events - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - delete this.listeners[event]; - } + // Note that title is rotated, so we're using the height, not width! + var titleWidth = 0; + if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) { + titleWidth = this.props.titleCharHeight; } - this.listeners = null; - this.hammer = null; + var offset = this.options.icons == true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15; - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - return component.destroy(); - }); + // this will resize the yAxis to accommodate the labels. + if (this.maxLabelSize > this.width - offset && this.options.visible == true) { + this.width = this.maxLabelSize + offset; + this.options.width = this.width + "px"; + DOMutil.cleanupElements(this.DOMelements.lines); + DOMutil.cleanupElements(this.DOMelements.labels); + this.redraw(); + resized = true; + } + // this will resize the yAxis if it is too big for the labels. + else if (this.maxLabelSize < this.width - offset && this.options.visible == true && this.width > this.minWidth) { + this.width = Math.max(this.minWidth, this.maxLabelSize + offset); + this.options.width = this.width + "px"; + DOMutil.cleanupElements(this.DOMelements.lines); + DOMutil.cleanupElements(this.DOMelements.labels); + this.redraw(); + resized = true; + } else { + DOMutil.cleanupElements(this.DOMelements.lines); + DOMutil.cleanupElements(this.DOMelements.labels); + resized = false; + } - this.body = null; + return resized; }; + DataAxis.prototype.convertValue = function (value) { + var invertedValue = this.valueAtZero - value; + var convertedValue = invertedValue * this.conversionFactor; + return convertedValue; + }; /** - * Set a custom time bar - * @param {Date} time - * @param {int} id + * Create a label for the axis at position x + * @private + * @param y + * @param text + * @param orientation + * @param className + * @param characterHeight */ - Core.prototype.setCustomTime = function (time, id) { - if (!this.customTime) { - throw new Error("Cannot get custom time: Custom time bar is not enabled"); + 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"; } - var barId = id || 0; + label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + "px"; - this.components.forEach(function (element, index, components) { - if (element instanceof CustomTime && element.options.id === barId) { - element.setCustomTime(time); - } - }); + text += ""; + + var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); + if (this.maxLabelSize < text.length * largestWidth) { + this.maxLabelSize = text.length * largestWidth; + } }; /** - * Retrieve the current custom time. - * @return {Date} customTime - * @param {int} id + * Create a minor line for the axis at position y + * @param y + * @param orientation + * @param className + * @param offset + * @param width */ - Core.prototype.getCustomTime = function (id) { - if (!this.customTime) { - throw new Error("Cannot get custom time: Custom time bar is not enabled"); + DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) { + if (this.master == true) { + var line = DOMutil.getDOMElement("div", this.DOMelements.lines, this.dom.lineContainer); //this.dom.redundant.lines.shift(); + line.className = className; + line.innerHTML = ""; + + if (orientation == "left") { + line.style.left = this.width - offset + "px"; + } else { + line.style.right = this.width - offset + "px"; + } + + line.style.width = width + "px"; + line.style.top = y + "px"; } + }; - var barId = id || 0, - customTime = this.customTime.getCustomTime(); + /** + * Create a title for the axis + * @private + * @param orientation + */ + DataAxis.prototype._redrawTitle = function (orientation) { + DOMutil.prepareElements(this.DOMelements.title); - this.components.forEach(function (element, index, components) { - if (element instanceof CustomTime && element.options.id === barId) { - customTime = element.getCustomTime(); + // Check if the title is defined for this axes + if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) { + var title = DOMutil.getDOMElement("div", this.DOMelements.title, this.dom.frame); + title.className = "yAxis title " + orientation; + title.innerHTML = this.options.title[orientation].text; + + // Add style - if provided + if (this.options.title[orientation].style !== undefined) { + util.addCssText(title, this.options.title[orientation].style); } - }); - return customTime; + if (orientation == "left") { + title.style.left = this.props.titleCharHeight + "px"; + } else { + title.style.right = this.props.titleCharHeight + "px"; + } + + title.style.width = this.height + "px"; + } + + // we need to clean up in case we did not use all elements. + DOMutil.cleanupElements(this.DOMelements.title); }; + + + /** - * Add custom vertical bar - * @param {Date | String | Number} time A Date, unix timestamp, or - * ISO date string. Time point where the new bar should be placed - * @param {Number | String} ID of the new bar - * @return {Number | String} ID of the new bar + * 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 */ - Core.prototype.addCustomTime = function (time, id) { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); - } + 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); - if (time === undefined) { - throw new Error("Time parameter for the custom bar must be provided"); + this.props.minorCharHeight = measureCharMinor.clientHeight; + this.props.minorCharWidth = measureCharMinor.clientWidth; + + this.dom.frame.removeChild(measureCharMinor); } - var ts = util.convert(time, "Date").valueOf(), - numIds, - customTime, - customBarId; + 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); - // All bar IDs are kept in 1 array, mixed types - // Bar with ID 0 is the default bar. - if (!this.customBarIds || this.customBarIds.constructor !== Array) { - this.customBarIds = [0]; + this.props.majorCharHeight = measureCharMajor.clientHeight; + this.props.majorCharWidth = measureCharMajor.clientWidth; + + this.dom.frame.removeChild(measureCharMajor); } - // If the ID is not provided, generate one, otherwise just use it - if (id === undefined) { - numIds = this.customBarIds.filter(function (element) { - return util.isNumber(element); - }); + if (!("titleCharHeight" in this.props)) { + var textTitle = document.createTextNode("0"); + var measureCharTitle = document.createElement("div"); + measureCharTitle.className = "yAxis title measure"; + measureCharTitle.appendChild(textTitle); + this.dom.frame.appendChild(measureCharTitle); - customBarId = numIds.length > 0 ? Math.max.apply(null, numIds) + 1 : 1; - } else { - // Check for duplicates - this.customBarIds.forEach(function (element) { - if (element === id) { - throw new Error("Custom time ID already exists"); - } - }); + this.props.titleCharHeight = measureCharTitle.clientHeight; + this.props.titleCharWidth = measureCharTitle.clientWidth; - customBarId = id; + this.dom.frame.removeChild(measureCharTitle); } + }; - this.customBarIds.push(customBarId); + module.exports = DataAxis; - customTime = new CustomTime(this.body, { - showCustomTime: true, - time: ts, - id: customBarId - }); +/***/ }, +/* 29 */ +/***/ function(module, exports, __webpack_require__) { - this.components.push(customTime); - this.redraw(); + "use strict"; - return customBarId; - }; + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(2); + var Line = __webpack_require__(45); + var Bar = __webpack_require__(46); + var Points = __webpack_require__(47); /** - * Remove previously added custom bar - * @param {int} id ID of the custom bar to be removed - * @return {boolean} True if the bar exists and is removed, false otherwise + * /** + * @param {object} group | the object of the group from the dataset + * @param {string} groupId | ID of the group + * @param {object} options | the default options + * @param {array} groupsUsingDefaultStyles | this array has one entree. + * It is passed as an array so it is passed by reference. + * It enumerates through the default styles + * @constructor */ - Core.prototype.removeCustomTime = function (id) { - var me = this; + function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { + this.id = groupId; + var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart", "drawPoints", "shaded", "catmullRom"]; + this.options = util.selectiveBridgeObject(fields, options); + this.usingDefaultStyle = group.className === undefined; + this.groupsUsingDefaultStyles = groupsUsingDefaultStyles; + this.zeroPosition = 0; + this.update(group); + if (this.usingDefaultStyle == true) { + this.groupsUsingDefaultStyles[0] += 1; + } + this.itemsData = []; + this.visible = group.visible === undefined ? true : group.visible; + } - this.components.forEach(function (bar, index, components) { - if (bar instanceof CustomTime && bar.options.id === id) { - // Only the lines added by the user will be removed - if (bar.options.id !== 0) { - me.customBarIds.splice(me.customBarIds.indexOf(id), 1); - components.splice(index, 1); - bar.destroy(); - } + + /** + * this loads a reference to all items in this group into this group. + * @param {array} items + */ + GraphGroup.prototype.setItems = function (items) { + if (items != null) { + this.itemsData = items; + if (this.options.sort == true) { + this.itemsData.sort(function (a, b) { + return a.x - b.x; + }); } - }); + } else { + this.itemsData = []; + } }; /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * this is used for plotting barcharts, this way, we only have to calculate it once. + * @param pos */ - Core.prototype.getVisibleItems = function () { - return this.itemSet && this.itemSet.getVisibleItems() || []; + GraphGroup.prototype.setZeroPosition = function (pos) { + this.zeroPosition = pos; }; - /** - * 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} + * set the options of the graph group over the default options. + * @param options */ - Core.prototype.clear = function (what) { - // clear items - if (!what || what.items) { - this.setItems(null); - } + GraphGroup.prototype.setOptions = function (options) { + if (options !== undefined) { + var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart"]; + util.selectiveDeepExtend(fields, this.options, options); - // clear groups - if (!what || what.groups) { - this.setGroups(null); - } + util.mergeOptions(this.options, options, "catmullRom"); + util.mergeOptions(this.options, options, "drawPoints"); + util.mergeOptions(this.options, options, "shaded"); - // clear options of timeline and of each of the components - if (!what || what.options) { - this.components.forEach(function (component) { - return component.setOptions(component.defaultOptions); - }); + 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; + } else { + this.options.catmullRom.parametrization = "centripetal"; + this.options.catmullRom.alpha = 0.5; + } + } + } + } + } - this.setOptions(this.defaultOptions); // this will also do a redraw + if (this.options.style == "line") { + this.type = new Line(this.id, this.options); + } else if (this.options.style == "bar") { + this.type = new Bar(this.id, this.options); + } else if (this.options.style == "points") { + this.type = new Points(this.id, this.options); } }; + /** - * Set Core window such that it fits all items - * @param {Object} [options] Available options: - * `animate: boolean | number` - * If true (default), the range is animated - * smoothly to the new window. - * If a number, the number is taken as duration - * for the animation. Default duration is 500 ms. + * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph + * @param group */ - Core.prototype.fit = function (options) { - var range = this._getDataRange(); - - // skip range set if there is no start and end date - if (range.start === null && range.end === null) { - return; - } - - var animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(range.start, range.end, animate); + 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.style = group.style; + this.setOptions(group.options); }; + /** - * Calculate the data range of the items and applies a 5% window around it. - * @returns {{start: Date | null, end: Date | null}} - * @protected + * draw the icon for the legend. + * + * @param x + * @param y + * @param JSONcontainer + * @param SVGcontainer + * @param iconWidth + * @param iconHeight */ - Core.prototype._getDataRange = function () { - // apply the data range as range - var dataRange = this.getItemRange(); + GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { + var fillHeight = iconHeight * 0.5; + var path, fillPath; - // 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 + var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer); + outline.setAttributeNS(null, "x", x); + outline.setAttributeNS(null, "y", y - fillHeight); + outline.setAttributeNS(null, "width", iconWidth); + outline.setAttributeNS(null, "height", 2 * fillHeight); + outline.setAttributeNS(null, "class", "outline"); + + if (this.options.style == "line") { + path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer); + path.setAttributeNS(null, "class", this.className); + if (this.style !== undefined) { + path.setAttributeNS(null, "style", this.style); } - start = new Date(start.valueOf() - interval * 0.05); - end = new Date(end.valueOf() + interval * 0.05); - } - return { - start: start, - end: end - }; - }; + 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"); + } - /** - * Set the visible window. Both parameters are optional, you can change only - * start or only end. Syntax: - * - * TimeLine.setWindow(start, end) - * TimeLine.setWindow(start, end, options) - * TimeLine.setWindow(range) - * - * Where start and end can be a Date, number, or string, and range is an - * object with properties start and end. - * - * @param {Date | Number | String | Object} [start] Start date of visible window - * @param {Date | Number | String} [end] End date of visible window - * @param {Object} [options] Available options: - * `animate: boolean | number` - * If true (default), the range is animated - * smoothly to the new window. - * If a number, the number is taken as duration - * for the animation. Default duration is 500 ms. - */ - Core.prototype.setWindow = function (start, end, options) { - var animate; - if (arguments.length == 1) { - var range = arguments[0]; - animate = range.animate !== undefined ? range.animate : true; - this.range.setRange(range.start, range.end, animate); + if (this.options.drawPoints.enabled == true) { + DOMutil.drawPoint(x + 0.5 * iconWidth, y, this, JSONcontainer, SVGcontainer); + } } else { - animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(start, end, animate); + var barWidth = Math.round(0.3 * iconWidth); + var bar1Height = Math.round(0.4 * iconHeight); + var bar2Height = Math.round(0.75 * iconHeight); + + var offset = Math.round((iconWidth - 2 * barWidth) / 3); + + DOMutil.drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + " bar", JSONcontainer, SVGcontainer); + DOMutil.drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + " bar", JSONcontainer, SVGcontainer); } }; + /** - * Move the window such that given time is centered on screen. - * @param {Date | Number | String} time - * @param {Object} [options] Available options: - * `animate: boolean | number` - * If true (default), the range is animated - * smoothly to the new window. - * If a number, the number is taken as duration - * for the animation. Default duration is 500 ms. + * return the legend entree for this group. + * + * @param iconWidth + * @param iconHeight + * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} */ - Core.prototype.moveTo = function (time, options) { - var interval = this.range.end - this.range.start; - var t = util.convert(time, "Date").valueOf(); + 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 }; + }; - var start = t - interval / 2; - var end = t + interval / 2; - var animate = options && options.animate !== undefined ? options.animate : true; + GraphGroup.prototype.getYRange = function (groupData) { + return this.type.getYRange(groupData); + }; - this.range.setRange(start, end, animate); + GraphGroup.prototype.draw = function (dataset, group, framework) { + this.type.draw(dataset, group, framework); }; + + module.exports = GraphGroup; + +/***/ }, +/* 30 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var stack = __webpack_require__(18); + var RangeItem = __webpack_require__(24); + /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range + * @constructor Group + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ - Core.prototype.getWindow = function () { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) + function Group(groupId, data, itemSet) { + this.groupId = groupId; + this.subgroups = {}; + this.subgroupIndex = 0; + this.subgroupOrderer = data && data.subgroupOrder; + 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 = { + byStart: [], + byEnd: [] + }; + this.checkRangedItems = false; // needed to refresh the ranged items if the window is programatically changed with NO overlap. + var me = this; + this.itemSet.body.emitter.on("checkRangedItems", function () { + me.checkRangedItems = true; + }); + + this._create(); + + this.setData(data); + } /** - * Force a redraw. Can be overridden by implementations of Core + * Create DOM elements for the group + * @private */ - Core.prototype.redraw = function () { - this._redraw(); + Group.prototype._create = function () { + var label = document.createElement("div"); + label.className = "vlabel"; + this.dom.label = label; + + var inner = document.createElement("div"); + inner.className = "inner"; + label.appendChild(inner); + this.dom.inner = inner; + + var foreground = document.createElement("div"); + foreground.className = "group"; + foreground["timeline-group"] = this; + this.dom.foreground = foreground; + + this.dom.background = document.createElement("div"); + this.dom.background.className = "group"; + + this.dom.axis = document.createElement("div"); + this.dom.axis.className = "group"; + + // create a hidden marker to detect when the Timelines container is attached + // to the DOM, or the style of a parent of the Timeline is changed from + // display:none is changed to visible. + this.dom.marker = document.createElement("div"); + this.dom.marker.style.visibility = "hidden"; // TODO: ask jos why this is not none? + this.dom.marker.innerHTML = "?"; + this.dom.background.appendChild(this.dom.marker); }; /** - * Redraw for internal use. Redraws all components. See also the public - * method redraw. - * @protected + * Set the group data for this group + * @param {Object} data Group data, can contain properties content and className */ - Core.prototype._redraw = function () { - var resized = false; - var options = this.options; - var props = this.props; - var dom = this.dom; - - if (!dom) return; // when destroyed + 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 + } - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + // update title + this.dom.label.title = data && data.title || ""; - // update class names - if (options.orientation == "top") { - util.addClassName(dom.root, "top"); - util.removeClassName(dom.root, "bottom"); + if (!this.dom.inner.firstChild) { + util.addClassName(this.dom.inner, "hidden"); } else { - util.removeClassName(dom.root, "top"); - util.addClassName(dom.root, "bottom"); + util.removeClassName(this.dom.inner, "hidden"); } - // 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; + // update className + var className = data && data.className || null; + if (className != this.className) { + if (this.className) { + util.removeClassName(this.dom.label, this.className); + util.removeClassName(this.dom.foreground, this.className); + util.removeClassName(this.dom.background, this.className); + util.removeClassName(this.dom.axis, this.className); + } + util.addClassName(this.dom.label, className); + util.addClassName(this.dom.foreground, className); + util.addClassName(this.dom.background, className); + util.addClassName(this.dom.axis, className); + this.className = className; + } - // workaround for a bug in IE: the clientWidth of an element with - // a height:0px and overflow:hidden is not calculated and always has value 0 - if (dom.centerContainer.clientHeight === 0) { - props.border.left = props.border.top; - props.border.right = props.border.left; + // update style + if (this.style) { + util.removeCssText(this.dom.label, this.style); + this.style = null; } - if (dom.root.clientHeight === 0) { - borderRootWidth = borderRootHeight; + if (data && data.style) { + util.addCssText(this.dom.label, data.style); + this.style = data.style; } + }; - // 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; + /** + * Get the width of the group label + * @return {number} width + */ + Group.prototype.getLabelWidth = function () { + return this.props.label.width; + }; - // 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"; + /** + * Repaint this group + * @param {{start: number, end: number}} range + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @param {boolean} [restack=false] Force restacking of all items + * @return {boolean} Returns true if the group is resized + */ + Group.prototype.redraw = function (range, margin, restack) { + var resized = false; - dom.background.style.width = props.background.width + "px"; - dom.backgroundVertical.style.width = props.centerContainer.width + "px"; - dom.backgroundHorizontal.style.width = props.background.width + "px"; - dom.centerContainer.style.width = props.center.width + "px"; - dom.top.style.width = props.top.width + "px"; - dom.bottom.style.width = props.bottom.width + "px"; + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - // reposition the panels - dom.background.style.left = "0"; - dom.background.style.top = "0"; - dom.backgroundVertical.style.left = props.left.width + props.border.left + "px"; - dom.backgroundVertical.style.top = "0"; - dom.backgroundHorizontal.style.left = "0"; - dom.backgroundHorizontal.style.top = props.top.height + "px"; - dom.centerContainer.style.left = props.left.width + "px"; - dom.centerContainer.style.top = props.top.height + "px"; - dom.leftContainer.style.left = "0"; - dom.leftContainer.style.top = props.top.height + "px"; - dom.rightContainer.style.left = props.left.width + props.center.width + "px"; - dom.rightContainer.style.top = props.top.height + "px"; - dom.top.style.left = props.left.width + "px"; - dom.top.style.top = "0"; - dom.bottom.style.left = props.left.width + "px"; - dom.bottom.style.top = props.top.height + props.centerContainer.height + "px"; + // 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; - // 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(); + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); - // 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); + restack = true; } - 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"; - - // show shadows when vertical scrolling is available - var visibilityTop = this.props.scrollTop == 0 ? "hidden" : ""; - var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? "hidden" : ""; - dom.shadowTop.style.visibility = visibilityTop; - dom.shadowBottom.style.visibility = visibilityBottom; - dom.shadowTopLeft.style.visibility = visibilityTop; - dom.shadowBottomLeft.style.visibility = visibilityBottom; - dom.shadowTopRight.style.visibility = visibilityTop; - dom.shadowBottomRight.style.visibility = visibilityBottom; - // redraw all components - this.components.forEach(function (component) { - resized = component.redraw() || resized; - }); - if (resized) { - // keep repainting until all sizes are settled - var MAX_REDRAWS = 3; // maximum number of consecutive redraws - if (this.redrawCount < MAX_REDRAWS) { - this.redrawCount++; - this._redraw(); - } else { - console.log("WARNING: infinite loop in redraw?"); - } - this.redrawCount = 0; + // reposition visible items vertically + if (this.itemSet.options.stack) { + // TODO: ugly way to access options... + stack.stack(this.visibleItems, margin, restack); + } else { + // no stacking + stack.nostack(this.visibleItems, margin, this.subgroups); } - this.emit("finishedRedraw"); - }; + // recalculate the height of the group + var height = this._calculateHeight(margin); - // TODO: deprecated since version 1.1.0, remove some day - Core.prototype.repaint = function () { - throw new Error("Function repaint is deprecated. Use redraw instead."); + // calculate actual size and position + var foreground = this.dom.foreground; + this.top = foreground.offsetTop; + this.left = foreground.offsetLeft; + this.width = foreground.offsetWidth; + resized = util.updateProperty(this, "height", height) || resized; + + // recalculate size of label + resized = util.updateProperty(this.props.label, "width", this.dom.inner.clientWidth) || resized; + resized = util.updateProperty(this.props.label, "height", this.dom.inner.clientHeight) || resized; + + // apply new height + this.dom.background.style.height = height + "px"; + this.dom.foreground.style.height = height + "px"; + this.dom.label.style.height = height + "px"; + + // update vertical position of items after they are re-stacked and the height of the group is calculated + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + var item = this.visibleItems[i]; + item.repositionY(margin); + } + + return resized; }; /** - * Set a current time. This can be used for example to ensure that a client's - * time is synchronized with a shared server time. - * Only applicable when option `showCurrentTime` is true. - * @param {Date | String | Number} time A Date, unix timestamp, or - * ISO date string. + * recalculate the height of the group + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @returns {number} Returns the height + * @private */ - Core.prototype.setCurrentTime = function (time) { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); + Group.prototype._calculateHeight = function (margin) { + // recalculate the height of the group + var height; + var visibleItems = this.visibleItems; + //var visibleSubgroups = []; + //this.visibleSubgroups = 0; + this.resetSubgroups(); + var me = this; + if (visibleItems.length > 0) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, item.top + item.height); + if (item.data.subgroup !== undefined) { + me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); + me.subgroups[item.data.subgroup].visible = true; + } + }); + if (min > margin.axis) { + // there is an empty gap between the lowest item and the axis + var offset = min - margin.axis; + max -= offset; + util.forEach(visibleItems, function (item) { + item.top -= offset; + }); + } + height = max + margin.item.vertical / 2; + } else { + height = margin.axis + margin.item.vertical; } + height = Math.max(height, this.props.label.height); - this.currentTime.setCurrentTime(time); + return height; }; /** - * Get the current time. - * Only applicable when option `showCurrentTime` is true. - * @return {Date} Returns the current time. + * Show this group: attach to the DOM */ - Core.prototype.getCurrentTime = function () { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); + Group.prototype.show = function () { + if (!this.dom.label.parentNode) { + this.itemSet.dom.labelSet.appendChild(this.dom.label); } - return this.currentTime.getCurrentTime(); + 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); + } }; /** - * 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 + * Hide this group: remove from the DOM */ - // TODO: move this function to Range - Core.prototype._toTime = function (x) { - return DateUtil.toTime(this, x, this.props.center.width); + Group.prototype.hide = function () { + var label = this.dom.label; + if (label.parentNode) { + label.parentNode.removeChild(label); + } + + var foreground = this.dom.foreground; + if (foreground.parentNode) { + foreground.parentNode.removeChild(foreground); + } + + var background = this.dom.background; + if (background.parentNode) { + background.parentNode.removeChild(background); + } + + var axis = this.dom.axis; + if (axis.parentNode) { + axis.parentNode.removeChild(axis); + } }; /** - * 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 + * Add an item to the group + * @param {Item} item */ - // TODO: move this function to Range - Core.prototype._toGlobalTime = function (x) { - return DateUtil.toTime(this, x, this.props.root.width); - //var conversion = this.range.conversion(this.props.root.width); - //return new Date(x / conversion.scale + conversion.offset); + Group.prototype.add = function (item) { + this.items[item.id] = item; + item.setParent(this); + + // add to + if (item.data.subgroup !== undefined) { + if (this.subgroups[item.data.subgroup] === undefined) { + this.subgroups[item.data.subgroup] = { height: 0, visible: false, index: this.subgroupIndex, items: [] }; + this.subgroupIndex++; + } + this.subgroups[item.data.subgroup].items.push(item); + } + this.orderSubgroups(); + + if (this.visibleItems.indexOf(item) == -1) { + var range = this.itemSet.body.range; // TODO: not nice accessing the range like this + this._checkIfVisible(item, this.visibleItems, range); + } + }; + + Group.prototype.orderSubgroups = function () { + if (this.subgroupOrderer !== undefined) { + var sortArray = []; + if (typeof this.subgroupOrderer == "string") { + for (var subgroup in this.subgroups) { + sortArray.push({ subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer] }); + } + sortArray.sort(function (a, b) { + return a.sortField - b.sortField; + }); + } else if (typeof this.subgroupOrderer == "function") { + for (var subgroup in this.subgroups) { + sortArray.push(this.subgroups[subgroup].items[0].data); + } + sortArray.sort(this.subgroupOrderer); + } + + if (sortArray.length > 0) { + for (var i = 0; i < sortArray.length; i++) { + this.subgroups[sortArray[i].subgroup].index = i; + } + } + } + }; + + Group.prototype.resetSubgroups = function () { + for (var subgroup in this.subgroups) { + if (this.subgroups.hasOwnProperty(subgroup)) { + this.subgroups[subgroup].visible = false; + } + } }; /** - * 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 + * Remove an item from the group + * @param {Item} item */ - // TODO: move this function to Range - Core.prototype._toScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.center.width); - }; + Group.prototype.remove = function (item) { + delete this.items[item.id]; + item.setParent(null); + + // remove from visible items + var index = this.visibleItems.indexOf(item); + if (index != -1) this.visibleItems.splice(index, 1); + // TODO: also remove from ordered items? + }; /** - * 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 + * Remove an item from the corresponding DataSet + * @param {Item} item */ - // TODO: move this function to Range - Core.prototype._toGlobalScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.root.width); - //var conversion = this.range.conversion(this.props.root.width); - //return (time.valueOf() - conversion.offset) * conversion.scale; + Group.prototype.removeFromDataSet = function (item) { + this.itemSet.removeItem(item.id); }; /** - * Initialize watching when option autoResize is true - * @private + * Reorder the items */ - Core.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); - } else { - this._stopAutoResize(); + Group.prototype.order = function () { + var array = util.toArray(this.items); + var startArray = []; + var endArray = []; + + for (var i = 0; i < array.length; i++) { + if (array[i].data.end !== undefined) { + endArray.push(array[i]); + } + startArray.push(array[i]); } + this.orderedItems = { + byStart: startArray, + byEnd: endArray + }; + + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); }; + /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. + * 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 */ - Core.prototype._startAutoResize = function () { - var me = this; + Group.prototype._updateVisibleItems = function (orderedItems, oldVisibleItems, range) { + var visibleItems = []; + var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems + var interval = (range.end - range.start) / 4; + var lowerBound = range.start - interval; + var upperBound = range.end + interval; + var item, i; - this._stopAutoResize(); + // this function is used to do the binary search. + var searchFunction = function (value) { + if (value < lowerBound) { + return -1; + } else if (value <= upperBound) { + return 0; + } else { + return 1; + } + }; - this._onResize = function () { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); - return; + // first check if the items that were in view previously are still in view. + // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! + // also cleans up invisible items. + if (oldVisibleItems.length > 0) { + for (i = 0; i < oldVisibleItems.length; i++) { + this._checkIfVisibleWithReference(oldVisibleItems[i], visibleItems, visibleItemsLookup, range); } + } - if (me.dom.root) { - // check whether the frame is resized - // Note: we compare offsetWidth here, not clientWidth. For some reason, - // IE does not restore the clientWidth from 0 to the actual width after - // changing the timeline's container display style from none to visible - if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { - me.props.lastWidth = me.dom.root.offsetWidth; - me.props.lastHeight = me.dom.root.offsetHeight; + // we do a binary search for the items that have only start values. + var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, "data", "start"); - me.emit("change"); - } + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. + this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { + return item.data.start < lowerBound || item.data.start > upperBound; + }); + + // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. + // We therefore have to brute force check all items in the byEnd list + if (this.checkRangedItems == true) { + this.checkRangedItems = false; + for (i = 0; i < orderedItems.byEnd.length; i++) { + this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); } - }; + } else { + // we do a binary search for the items that have defined end times. + var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, "data", "end"); - // add event listener to window resize - util.addEventListener(window, "resize", this._onResize); + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. + this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { + return item.data.end < lowerBound || item.data.end > upperBound; + }); + } - 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; + // finally, we reposition all the visible items. + for (i = 0; i < visibleItems.length; i++) { + item = visibleItems[i]; + if (!item.displayed) item.show(); + // reposition item horizontally + item.repositionX(); } - // remove event listener on window.resize - util.removeEventListener(window, "resize", this._onResize); - this._onResize = null; + // debug + //console.log("new line") + //if (this.groupId == null) { + // for (i = 0; i < orderedItems.byStart.length; i++) { + // item = orderedItems.byStart[i].data; + // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") + // } + // for (i = 0; i < orderedItems.byEnd.length; i++) { + // item = orderedItems.byEnd[i].data; + // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") + // } + //} + + return visibleItems; + }; + + Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { + var item; + var i; + + if (initialPos != -1) { + for (i = initialPos; i >= 0; i--) { + item = items[i]; + if (breakCondition(item)) { + break; + } else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } + } + } + + for (i = initialPos + 1; i < items.length; i++) { + item = items[i]; + if (breakCondition(item)) { + break; + } else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } + } + } + } }; + /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop + * 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 */ - Core.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; + 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(); + } }; + /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop + * 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 */ - 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; + Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { + if (item.isVisible(range)) { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); } - this.props.scrollTopMin = scrollTopMin; + } else { + if (item.displayed) item.hide(); } + }; - // 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; + + module.exports = Group; + +/***/ }, +/* 31 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var Group = __webpack_require__(30); + + /** + * @constructor BackgroundGroup + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet + */ + function BackgroundGroup(groupId, data, itemSet) { + Group.call(this, groupId, data, itemSet); + + this.width = 0; + this.height = 0; + this.top = 0; + this.left = 0; + } + + BackgroundGroup.prototype = Object.create(Group.prototype); + + /** + * Repaint this group + * @param {{start: number, end: number}} range + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @param {boolean} [restack=false] Force restacking of all items + * @return {boolean} Returns true if the group is resized + */ + BackgroundGroup.prototype.redraw = function (range, margin, restack) { + var resized = false; + + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + + // calculate actual size + this.width = this.dom.background.offsetWidth; + + // apply new height (just always zero for BackgroundGroup + this.dom.background.style.height = "0"; + + // update vertical position of items after they are re-stacked and the height of the group is calculated + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + var item = this.visibleItems[i]; + item.repositionY(margin); + } + + return resized; }; /** - * Get the current scrollTop - * @returns {number} scrollTop - * @private + * Show this group: attach to the DOM */ - Core.prototype._getScrollTop = function () { - return this.props.scrollTop; + BackgroundGroup.prototype.show = function () { + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); + } }; - module.exports = Core; + module.exports = BackgroundGroup; /***/ }, -/* 28 */ +/* 32 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Hammer = __webpack_require__(19); + var Hammer = __webpack_require__(41); var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); - var TimeStep = __webpack_require__(29); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var TimeStep = __webpack_require__(19); var Component = __webpack_require__(25); var Group = __webpack_require__(30); - var BackgroundGroup = __webpack_require__(34); - var BoxItem = __webpack_require__(35); - var PointItem = __webpack_require__(36); - var RangeItem = __webpack_require__(32); - var BackgroundItem = __webpack_require__(37); + var BackgroundGroup = __webpack_require__(31); + var BoxItem = __webpack_require__(22); + var PointItem = __webpack_require__(23); + var RangeItem = __webpack_require__(24); + var BackgroundItem = __webpack_require__(21); var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items @@ -16246,1402 +13633,2174 @@ return /******/ (function(modules) { // webpackBootstrap newItem.end = snap ? snap(end, scale, step) : end; } - newItem[this.itemsData._fieldId] = util.randomUUID(); + newItem[this.itemsData._fieldId] = util.randomUUID(); + + var group = this.groupFromTarget(event); + if (group) { + newItem.group = group.groupId; + } + + // execute async handler to customize (or cancel) adding an item + this.options.onAdd(newItem, function (item) { + if (item) { + me.itemsData.getDataSet().add(item); + // TODO: need to trigger a redraw? + } + }); + } + }; + + /** + * 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 shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; + if (shiftKey) { + // select all items between the old selection and the tapped item + + // determine the selection range + selection.push(item.id); + var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + + // select all items within the selection range + selection = []; + for (var id in this.items) { + if (this.items.hasOwnProperty(id)) { + var _item = this.items[id]; + var start = _item.data.start; + var end = _item.data.end !== undefined ? _item.data.end : start; + + if (start >= range.min && end <= range.max) { + selection.push(_item.id); // do not use id but item.id, id itself is stringified + } + } + } + } else { + // add/remove this item from the current selection + var index = selection.indexOf(item.id); + if (index == -1) { + // item is not yet selected -> select it + selection.push(item.id); + } else { + // item is already selected -> deselect it + selection.splice(index, 1); + } + } + + this.setSelection(selection); + + this.body.emitter.emit("select", { + items: this.getSelection() + }); + } + }; + + /** + * Calculate the time range of a list of items + * @param {Array.} itemsData + * @return {{min: Date, max: Date}} Returns the range of the provided items + * @private + */ + ItemSet._getItemRange = function (itemsData) { + var max = null; + var min = null; + + itemsData.forEach(function (data) { + if (min == null || data.start < min) { + min = data.start; + } + + if (data.end != undefined) { + if (max == null || data.end > max) { + max = data.end; + } + } else { + if (max == null || data.start > max) { + max = data.start; + } + } + }); + + return { + min: min, + max: max + }; + }; + + /** + * Find an item from an event target: + * searches for the attribute 'timeline-item' in the event target's element tree + * @param {Event} event + * @return {Item | null} item + */ + ItemSet.itemFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty("timeline-item")) { + return target["timeline-item"]; + } + target = target.parentNode; + } + + return null; + }; + + /** + * Find the Group from an event target: + * searches for the attribute 'timeline-group' in the event target's element tree + * @param {Event} event + * @return {Group | null} group + */ + ItemSet.prototype.groupFromTarget = function (event) { + // TODO: cleanup when the new solution is stable (also on mobile) + //var target = event.target; + //while (target) { + // if (target.hasOwnProperty('timeline-group')) { + // return target['timeline-group']; + // } + // target = target.parentNode; + //} + // + + var clientY = event.gesture.center.clientY; + for (var i = 0; i < this.groupIds.length; i++) { + var groupId = this.groupIds[i]; + var group = this.groups[groupId]; + var foreground = group.dom.foreground; + var top = util.getAbsoluteTop(foreground); + if (clientY > top && clientY < top + foreground.offsetHeight) { + return group; + } + + if (this.options.orientation === "top") { + if (i === this.groupIds.length - 1 && clientY > top) { + return group; + } + } else { + if (i === 0 && clientY < top + foreground.offset) { + return group; + } + } + } + + return null; + }; + + /** + * Find the ItemSet from an event target: + * searches for the attribute 'timeline-itemset' in the event target's element tree + * @param {Event} event + * @return {ItemSet | null} item + */ + ItemSet.itemSetFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty("timeline-itemset")) { + return target["timeline-itemset"]; + } + target = target.parentNode; + } + + return null; + }; + + module.exports = ItemSet; + +/***/ }, +/* 33 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(2); + var Component = __webpack_require__(25); + + /** + * Legend for Graph2d + */ + function Legend(body, options, side, linegraphOptions) { + this.body = body; + this.defaultOptions = { + enabled: true, + icons: true, + iconSize: 20, + iconSpacing: 6, + left: { + visible: true, + position: "top-left" // top/bottom - left,center,right + }, + right: { + visible: true, + position: "top-left" // top/bottom - left,center,right + } + }; + this.side = side; + this.options = util.extend({}, this.defaultOptions); + this.linegraphOptions = linegraphOptions; + + this.svgElements = {}; + this.dom = {}; + this.groups = {}; + this.amountOfGroups = 0; + this._create(); + + this.setOptions(options); + } + + Legend.prototype = new Component(); + + Legend.prototype.clear = function () { + this.groups = {}; + this.amountOfGroups = 0; + }; + + Legend.prototype.addGroup = function (label, graphOptions) { + if (!this.groups.hasOwnProperty(label)) { + this.groups[label] = graphOptions; + } + this.amountOfGroups += 1; + }; + + Legend.prototype.updateGroup = function (label, graphOptions) { + this.groups[label] = graphOptions; + }; + + Legend.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; + } + }; + + 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.svg.style.height = "100%"; + + this.dom.frame.appendChild(this.svg); + this.dom.frame.appendChild(this.dom.textArea); + }; + + /** + * Hide the component from the DOM + */ + Legend.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } + }; + + /** + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed + */ + Legend.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } + }; + + Legend.prototype.setOptions = function (options) { + var fields = ["enabled", "orientation", "icons", "left", "right"]; + util.selectiveDeepExtend(fields, this.options, options); + }; + + Legend.prototype.redraw = function () { + var activeGroups = 0; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + activeGroups++; + } + } + } + + if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false || activeGroups == 0) { + this.hide(); + } else { + this.show(); + if (this.options[this.side].position == "top-left" || this.options[this.side].position == "bottom-left") { + this.dom.frame.style.left = "4px"; + this.dom.frame.style.textAlign = "left"; + this.dom.textArea.style.textAlign = "left"; + this.dom.textArea.style.left = this.options.iconSize + 15 + "px"; + this.dom.textArea.style.right = ""; + this.svg.style.left = 0 + "px"; + this.svg.style.right = ""; + } else { + this.dom.frame.style.right = "4px"; + this.dom.frame.style.textAlign = "right"; + this.dom.textArea.style.textAlign = "right"; + this.dom.textArea.style.right = this.options.iconSize + 15 + "px"; + this.dom.textArea.style.left = ""; + this.svg.style.right = 0 + "px"; + this.svg.style.left = ""; + } + + if (this.options[this.side].position == "top-left" || this.options[this.side].position == "top-right") { + this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px", "")) + "px"; + this.dom.frame.style.bottom = ""; + } else { + var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height; + this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace("px", "")) + "px"; + this.dom.frame.style.top = ""; + } - var group = this.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; + 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(); } - // execute async handler to customize (or cancel) adding an item - this.options.onAdd(newItem, function (item) { - if (item) { - me.itemsData.getDataSet().add(item); - // TODO: need to trigger a redraw? + var content = ""; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + content += this.groups[groupId].content + "
"; + } } - }); + } + this.dom.textArea.innerHTML = content; + this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + "px"; } }; - /** - * 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 shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; - if (shiftKey) { - // select all items between the old selection and the tapped item - - // determine the selection range - selection.push(item.id); - var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + 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; - // select all items within the selection range - selection = []; - for (var id in this.items) { - if (this.items.hasOwnProperty(id)) { - var _item = this.items[id]; - var start = _item.data.start; - var end = _item.data.end !== undefined ? _item.data.end : start; + this.svg.style.width = iconWidth + 5 + iconOffset + "px"; - if (start >= range.min && end <= range.max) { - selection.push(_item.id); // do not use id but item.id, id itself is stringified - } + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); + y += iconHeight + this.options.iconSpacing; } } - } else { - // add/remove this item from the current selection - var index = selection.indexOf(item.id); - if (index == -1) { - // item is not yet selected -> select it - selection.push(item.id); - } else { - // item is already selected -> deselect it - selection.splice(index, 1); - } } - this.setSelection(selection); - - this.body.emitter.emit("select", { - items: this.getSelection() - }); + DOMutil.cleanupElements(this.svgElements); } }; - /** - * Calculate the time range of a list of items - * @param {Array.} itemsData - * @return {{min: Date, max: Date}} Returns the range of the provided items - * @private - */ - ItemSet._getItemRange = function (itemsData) { - var max = null; - var min = null; + module.exports = Legend; - itemsData.forEach(function (data) { - if (min == null || data.start < min) { - min = data.start; - } +/***/ }, +/* 34 */ +/***/ function(module, exports, __webpack_require__) { - if (data.end != undefined) { - if (max == null || data.end > max) { - max = data.end; - } - } else { - if (max == null || data.start > max) { - max = data.start; - } - } - }); + "use strict"; - return { - min: min, - max: max - }; - }; + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(2); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var Component = __webpack_require__(25); + var DataAxis = __webpack_require__(28); + var GraphGroup = __webpack_require__(29); + var Legend = __webpack_require__(33); + var BarGraphFunctions = __webpack_require__(46); + + var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items /** - * 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 + * This is the constructor of the LineGraph. It requires a Timeline body and options. + * + * @param body + * @param options + * @constructor */ - ItemSet.itemFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty("timeline-item")) { - return target["timeline-item"]; + function LineGraph(body, options) { + this.id = util.randomUUID(); + this.body = body; + + this.defaultOptions = { + yAxisOrientation: "left", + defaultGroup: "default", + sort: true, + sampling: true, + graphHeight: "400px", + shaded: { + enabled: false, + orientation: "bottom" // top, bottom + }, + style: "line", // line, bar + barChart: { + width: 50, + handleOverlap: "overlap", + align: "center" // left, center, right + }, + catmullRom: { + enabled: true, + parametrization: "centripetal", // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) + alpha: 0.5 + }, + drawPoints: { + enabled: true, + size: 6, + style: "square" // square, circle + }, + dataAxis: { + showMinorLabels: true, + showMajorLabels: true, + icons: false, + width: "40px", + visible: true, + alignZeros: true, + customRange: { + left: { min: undefined, max: undefined }, + right: { min: undefined, max: undefined } + } + //, these options are not set by default, but this shows the format they will be in + //format: { + // left: {decimals: 2}, + // right: {decimals: 2} + //}, + //title: { + // left: { + // text: 'left', + // style: 'color:black;' + // }, + // right: { + // text: 'right', + // style: 'color:black;' + // } + //} + }, + legend: { + enabled: false, + icons: true, + left: { + visible: true, + position: "top-left" // top/bottom - left,right + }, + right: { + visible: true, + position: "top-right" // top/bottom - left,right + } + }, + groups: { + visibility: {} } - target = target.parentNode; - } + }; - return null; - }; + // options is shared by this ItemSet and all its items + this.options = util.extend({}, this.defaultOptions); + this.dom = {}; + this.props = {}; + this.hammer = null; + this.groups = {}; + this.abortedGraphUpdate = false; + this.updateSVGheight = false; + this.updateSVGheightOnResize = false; - /** - * Find the Group from an event target: - * searches for the attribute 'timeline-group' in the event target's element tree - * @param {Event} event - * @return {Group | null} group - */ - ItemSet.prototype.groupFromTarget = function (event) { - // TODO: cleanup when the new solution is stable (also on mobile) - //var target = event.target; - //while (target) { - // if (target.hasOwnProperty('timeline-group')) { - // return target['timeline-group']; - // } - // target = target.parentNode; - //} - // + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - var clientY = event.gesture.center.clientY; - for (var i = 0; i < this.groupIds.length; i++) { - var groupId = this.groupIds[i]; - var group = this.groups[groupId]; - var foreground = group.dom.foreground; - var top = util.getAbsoluteTop(foreground); - if (clientY > top && clientY < top + foreground.offsetHeight) { - return group; + // 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.options.orientation === "top") { - if (i === this.groupIds.length - 1 && clientY > top) { - return group; - } - } else { - if (i === 0 && clientY < top + foreground.offset) { - return group; - } + // 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 null; - }; + this.items = {}; // object with an Item for every data item + this.selection = []; // list with the ids of all selected nodes + this.lastStart = this.body.range.start; + this.touchParams = {}; // stores properties while dragging + + this.svgElements = {}; + this.setOptions(options); + this.groupsUsingDefaultStyles = [0]; + this.COUNTER = 0; + this.body.emitter.on("rangechanged", function () { + me.lastStart = me.body.range.start; + me.svg.style.left = util.option.asSize(-me.props.width); + me.redraw.call(me, true); + }); + + // create the HTML DOM + this._create(); + this.framework = { svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups }; + this.body.emitter.emit("change"); + } + + LineGraph.prototype = new Component(); /** - * 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 + * Create the HTML DOM for the ItemSet */ - ItemSet.itemSetFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty("timeline-itemset")) { - return target["timeline-itemset"]; - } - target = target.parentNode; - } + LineGraph.prototype._create = function () { + var frame = document.createElement("div"); + frame.className = "LineGraph"; + this.dom.frame = frame; - return null; - }; + // 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); - module.exports = ItemSet; + // data axis + this.options.dataAxis.orientation = "left"; + this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); -/***/ }, -/* 29 */ -/***/ function(module, exports, __webpack_require__) { + this.options.dataAxis.orientation = "right"; + this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); + delete this.options.dataAxis.orientation; - "use strict"; + // legends + this.legendLeft = new Legend(this.body, this.options.legend, "left", this.options.groups); + this.legendRight = new Legend(this.body, this.options.legend, "right", this.options.groups); - var moment = __webpack_require__(2); - var DateUtil = __webpack_require__(26); - var util = __webpack_require__(1); + this.show(); + }; /** - * @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 + * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. + * @param {object} options */ - function TimeStep(start, end, minimumStep, hiddenDates) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); + LineGraph.prototype.setOptions = function (options) { + if (options) { + var fields = ["sampling", "defaultGroup", "height", "graphHeight", "yAxisOrientation", "style", "barChart", "dataAxis", "sort", "groups"]; + if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) { + this.updateSVGheight = true; + this.updateSVGheightOnResize = true; + } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { + if (parseInt((options.graphHeight + "").replace("px", "")) < this.body.domProps.centerContainer.height) { + this.updateSVGheight = true; + } + } + util.selectiveDeepExtend(fields, this.options, options); + util.mergeOptions(this.options, options, "catmullRom"); + util.mergeOptions(this.options, options, "drawPoints"); + util.mergeOptions(this.options, options, "shaded"); + util.mergeOptions(this.options, options, "legend"); - this.autoScale = true; - this.scale = "day"; - this.step = 1; + 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; + } else { + this.options.catmullRom.parametrization = "centripetal"; + this.options.catmullRom.alpha = 0.5; + } + } + } + } - // initialize the range - this.setRange(start, end, minimumStep); + if (this.yAxisLeft) { + if (options.dataAxis !== undefined) { + this.yAxisLeft.setOptions(this.options.dataAxis); + this.yAxisRight.setOptions(this.options.dataAxis); + } + } - // hidden Dates options - this.switchedDay = false; - this.switchedMonth = false; - this.switchedYear = false; - this.hiddenDates = hiddenDates; - if (hiddenDates === undefined) { - this.hiddenDates = []; - } + if (this.legendLeft) { + if (options.legend !== undefined) { + this.legendLeft.setOptions(this.options.legend); + this.legendRight.setOptions(this.options.legend); + } + } - this.format = TimeStep.FORMAT; // default formatting - } + if (this.groups.hasOwnProperty(UNGROUPED)) { + this.groups[UNGROUPED].setOptions(options); + } + } - // Time formatting - TimeStep.FORMAT = { - minorLabels: { - millisecond: "SSS", - second: "s", - minute: "HH:mm", - hour: "HH:mm", - weekday: "ddd D", - day: "D", - month: "MMM", - year: "YYYY" - }, - majorLabels: { - millisecond: "HH:mm:ss", - second: "D MMMM HH:mm", - minute: "ddd D MMMM", - hour: "ddd D MMMM", - weekday: "MMMM YYYY", - day: "MMMM YYYY", - month: "YYYY", - year: "" + // this is used to redraw the graph if the visibility of the groups is changed. + if (this.dom.frame) { + this.redraw(true); } }; /** - * Set custom formatting for the minor an major labels of the TimeStep. - * Both `minorLabels` and `majorLabels` are an Object with properties: - * 'millisecond, 'second, 'minute', 'hour', 'weekday, 'day, 'month, 'year'. - * @param {{minorLabels: Object, majorLabels: Object}} format + * Hide the component from the DOM */ - TimeStep.prototype.setFormat = function (format) { - var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); - this.format = util.deepExtend(defaultFormat, format); + LineGraph.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } }; + /** - * 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 + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed */ - 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); + LineGraph.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); } }; - /** - * Set the range iterator to the start date. - */ - TimeStep.prototype.first = function () { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); - }; /** - * Round the current date to the first minor date value - * This must be executed once when the current date is set to start Date + * Set items + * @param {vis.DataSet | null} items */ - TimeStep.prototype.roundToMinor = function () { - // round to floor - // IMPORTANT: we have no breaks in this switch! (this is no bug) - // noinspection FallThroughInSwitchStatementJS - switch (this.scale) { - case "year": - this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); - this.current.setMonth(0); - case "month": - this.current.setDate(1); - case "day": - // intentional fall through - case "weekday": - this.current.setHours(0); - case "hour": - this.current.setMinutes(0); - case "minute": - this.current.setSeconds(0); - case "second": - this.current.setMilliseconds(0); - //case 'millisecond': // nothing to do for milliseconds + LineGraph.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; + + // replace the dataset + if (!items) { + this.itemsData = null; + } else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } else { + throw new TypeError("Data must be an instance of DataSet or DataView"); } - if (this.step != 1) { - // round down to the first minor value that is a multiple of the current step size - switch (this.scale) { - case "millisecond": - this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step);break; - case "second": - this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step);break; - case "minute": - this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step);break; - case "hour": - this.current.setHours(this.current.getHours() - this.current.getHours() % this.step);break; - case "weekday": - // intentional fall through - case "day": - this.current.setDate(this.current.getDate() - 1 - (this.current.getDate() - 1) % this.step + 1);break; - case "month": - this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step);break; - case "year": - this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step);break; - default: - break; - } + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); + + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); } - }; - /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date - */ - TimeStep.prototype.hasNext = function () { - return this.current.valueOf() <= this._end.valueOf(); + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); + + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + } + this._updateUngrouped(); + //this._updateGraph(); + this.redraw(true); }; + /** - * Do the next step + * Set groups + * @param {vis.DataSet} groups */ - TimeStep.prototype.next = function () { - var prev = this.current.valueOf(); + LineGraph.prototype.setGroups = function (groups) { + var me = this; + var ids; - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case "millisecond": + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - this.current = new Date(this.current.valueOf() + this.step);break; - case "second": - this.current = new Date(this.current.valueOf() + this.step * 1000);break; - case "minute": - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60);break; - case "hour": - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - h % this.step); - break; - case "weekday": - // intentional fall through - case "day": - this.current.setDate(this.current.getDate() + this.step);break; - case "month": - this.current.setMonth(this.current.getMonth() + this.step);break; - case "year": - this.current.setFullYear(this.current.getFullYear() + this.step);break; - default: - break; - } + // replace the dataset + if (!groups) { + this.groupsData = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + this.groupsData = groups; } else { - switch (this.scale) { - case "millisecond": - this.current = new Date(this.current.valueOf() + this.step);break; - case "second": - this.current.setSeconds(this.current.getSeconds() + this.step);break; - case "minute": - this.current.setMinutes(this.current.getMinutes() + this.step);break; - case "hour": - this.current.setHours(this.current.getHours() + this.step);break; - case "weekday": - // intentional fall through - case "day": - this.current.setDate(this.current.getDate() + this.step);break; - case "month": - this.current.setMonth(this.current.getMonth() + this.step);break; - case "year": - this.current.setFullYear(this.current.getFullYear() + this.step);break; - default: - break; - } + throw new TypeError("Data must be an instance of DataSet or DataView"); } - if (this.step != 1) { - // round down to the correct major value - switch (this.scale) { - case "millisecond": - if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; - case "second": - if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; - case "minute": - if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; - case "hour": - if (this.current.getHours() < this.step) this.current.setHours(0);break; - case "weekday": - // intentional fall through - case "day": - if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; - case "month": - if (this.current.getMonth() < this.step) this.current.setMonth(0);break; - case "year": - break; // nothing to do for year - default: - break; - } + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); + + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); } + this._onUpdate(); + }; - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); + + /** + * Update the data + * @param [ids] + * @private + */ + LineGraph.prototype._onUpdate = function (ids) { + this._updateUngrouped(); + this._updateAllGroupData(); + //this._updateGraph(); + this.redraw(true); + }; + LineGraph.prototype._onAdd = function (ids) { + this._onUpdate(ids); + }; + LineGraph.prototype._onRemove = function (ids) { + this._onUpdate(ids); + }; + LineGraph.prototype._onUpdateGroups = function (groupIds) { + for (var i = 0; i < groupIds.length; i++) { + var group = this.groupsData.get(groupIds[i]); + this._updateGroup(group, groupIds[i]); } - DateUtil.stepOverHiddenDates(this, prev); + //this._updateGraph(); + this.redraw(true); + }; + LineGraph.prototype._onAddGroups = function (groupIds) { + this._onUpdateGroups(groupIds); }; /** - * Get the current datetime - * @return {Date} current The current date + * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph + * @param {Array} groupIds + * @private */ - TimeStep.prototype.getCurrent = function () { - return this.current; + LineGraph.prototype._onRemoveGroups = function (groupIds) { + for (var i = 0; i < groupIds.length; i++) { + if (this.groups.hasOwnProperty(groupIds[i])) { + if (this.groups[groupIds[i]].options.yAxisOrientation == "right") { + this.yAxisRight.removeGroup(groupIds[i]); + this.legendRight.removeGroup(groupIds[i]); + this.legendRight.redraw(); + } else { + this.yAxisLeft.removeGroup(groupIds[i]); + this.legendLeft.removeGroup(groupIds[i]); + this.legendLeft.redraw(); + } + delete this.groups[groupIds[i]]; + } + } + this._updateUngrouped(); + //this._updateGraph(); + this.redraw(true); }; + /** - * Set a custom scale. Autoscaling will be disabled. - * For example setScale('minute', 5) will result - * in minor steps of 5 minutes, and major steps of an hour. + * update a group object with the group dataset entree * - * @param {{scale: string, step: number}} params - * An object containing two properties: - * - A string 'scale'. Choose from 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day, 'month, 'year'. - * - A number 'step'. A step size, by default 1. - * Choose for example 1, 2, 5, or 10. + * @param group + * @param groupId + * @private */ - TimeStep.prototype.setScale = function (params) { - if (params && typeof params.scale == "string") { - this.scale = params.scale; - this.step = params.step > 0 ? params.step : 1; - this.autoScale = false; + 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(); }; + /** - * Enable or disable autoscaling - * @param {boolean} enable If true, autoascaling is set true + * this updates all groups, it is used when there is an update the the itemset. + * + * @private */ - TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; + LineGraph.prototype._updateAllGroupData = function () { + if (this.itemsData != null) { + var groupsContent = {}; + var groupId; + for (groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + groupsContent[groupId] = []; + } + } + for (var itemId in this.itemsData._data) { + if (this.itemsData._data.hasOwnProperty(itemId)) { + var item = this.itemsData._data[itemId]; + if (groupsContent[item.group] === undefined) { + throw new Error("Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups."); + } + item.x = util.convert(item.x, "Date"); + groupsContent[item.group].push(item); + } + } + for (groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + this.groups[groupId].setItems(groupsContent[groupId]); + } + } + } }; /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds + * 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 */ - TimeStep.prototype.setMinimumStep = function (minimumStep) { - if (minimumStep == undefined) { - return; + LineGraph.prototype._updateUngrouped = function () { + if (this.itemsData && this.itemsData != null) { + var ungroupedCounter = 0; + for (var itemId in this.itemsData._data) { + if (this.itemsData._data.hasOwnProperty(itemId)) { + var item = this.itemsData._data[itemId]; + if (item != undefined) { + if (item.hasOwnProperty("group")) { + if (item.group === undefined) { + item.group = UNGROUPED; + } + } else { + item.group = UNGROUPED; + } + ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter; + } + } + } + + if (ungroupedCounter == 0) { + delete this.groups[UNGROUPED]; + this.legendLeft.removeGroup(UNGROUPED); + this.legendRight.removeGroup(UNGROUPED); + this.yAxisLeft.removeGroup(UNGROUPED); + this.yAxisRight.removeGroup(UNGROUPED); + } else { + var group = { id: UNGROUPED, content: this.options.defaultGroup }; + this._updateGroup(group, UNGROUPED); + } + } else { + delete this.groups[UNGROUPED]; + this.legendLeft.removeGroup(UNGROUPED); + this.legendRight.removeGroup(UNGROUPED); + this.yAxisLeft.removeGroup(UNGROUPED); + this.yAxisRight.removeGroup(UNGROUPED); } - //var b = asc + ds; + this.legendLeft.redraw(); + this.legendRight.redraw(); + }; - 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 = "year";this.step = 1000; - } - if (stepYear * 500 > minimumStep) { - this.scale = "year";this.step = 500; - } - if (stepYear * 100 > minimumStep) { - this.scale = "year";this.step = 100; - } - if (stepYear * 50 > minimumStep) { - this.scale = "year";this.step = 50; - } - if (stepYear * 10 > minimumStep) { - this.scale = "year";this.step = 10; - } - if (stepYear * 5 > minimumStep) { - this.scale = "year";this.step = 5; - } - if (stepYear > minimumStep) { - this.scale = "year";this.step = 1; - } - if (stepMonth * 3 > minimumStep) { - this.scale = "month";this.step = 3; - } - if (stepMonth > minimumStep) { - this.scale = "month";this.step = 1; - } - if (stepDay * 5 > minimumStep) { - this.scale = "day";this.step = 5; - } - if (stepDay * 2 > minimumStep) { - this.scale = "day";this.step = 2; - } - if (stepDay > minimumStep) { - this.scale = "day";this.step = 1; - } - if (stepDay / 2 > minimumStep) { - this.scale = "weekday";this.step = 1; - } - if (stepHour * 4 > minimumStep) { - this.scale = "hour";this.step = 4; - } - if (stepHour > minimumStep) { - this.scale = "hour";this.step = 1; - } - if (stepMinute * 15 > minimumStep) { - this.scale = "minute";this.step = 15; - } - if (stepMinute * 10 > minimumStep) { - this.scale = "minute";this.step = 10; - } - if (stepMinute * 5 > minimumStep) { - this.scale = "minute";this.step = 5; - } - if (stepMinute > minimumStep) { - this.scale = "minute";this.step = 1; - } - if (stepSecond * 15 > minimumStep) { - this.scale = "second";this.step = 15; - } - if (stepSecond * 10 > minimumStep) { - this.scale = "second";this.step = 10; - } - if (stepSecond * 5 > minimumStep) { - this.scale = "second";this.step = 5; - } - if (stepSecond > minimumStep) { - this.scale = "second";this.step = 1; - } - if (stepMillisecond * 200 > minimumStep) { - this.scale = "millisecond";this.step = 200; - } - if (stepMillisecond * 100 > minimumStep) { - this.scale = "millisecond";this.step = 100; - } - if (stepMillisecond * 50 > minimumStep) { - this.scale = "millisecond";this.step = 50; + /** + * Redraw the component, mandatory function + * @return {boolean} Returns true if the component is resized + */ + LineGraph.prototype.redraw = function (forceGraphUpdate) { + var resized = false; + + // calculate actual size and position + this.props.width = this.dom.frame.offsetWidth; + this.props.height = this.body.domProps.centerContainer.height; + + // update the graph if there is no lastWidth or with, used for the initial draw + if (this.lastWidth === undefined && this.props.width) { + forceGraphUpdate = true; } - if (stepMillisecond * 10 > minimumStep) { - this.scale = "millisecond";this.step = 10; + + // 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.lastVisibleInterval = visibleInterval; + + + // the svg element is three times as big as the width, this allows for fully dragging left and right + // without reloading the graph. the controls for this are bound to events in the constructor + if (resized == true) { + this.svg.style.width = util.option.asSize(3 * this.props.width); + this.svg.style.left = util.option.asSize(-this.props.width); + + // if the height of the graph is set as proportional, change the height of the svg + if ((this.options.height + "").indexOf("%") != -1 || this.updateSVGheightOnResize == true) { + this.updateSVGheight = true; + } } - if (stepMillisecond * 5 > minimumStep) { - this.scale = "millisecond";this.step = 5; + + // update the height of the graph on each redraw of the graph. + if (this.updateSVGheight == true) { + if (this.options.graphHeight != this.body.domProps.centerContainer.height + "px") { + this.options.graphHeight = this.body.domProps.centerContainer.height + "px"; + this.svg.style.height = this.body.domProps.centerContainer.height + "px"; + } + this.updateSVGheight = false; + } else { + this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; } - if (stepMillisecond > minimumStep) { - this.scale = "millisecond";this.step = 1; + + // zoomed is here to ensure that animations are shown correctly. + if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) { + resized = this._updateGraph() || resized; + } else { + // move the whole svg while dragging + if (this.lastStart != 0) { + var offset = this.body.range.start - this.lastStart; + var range = this.body.range.end - this.body.range.start; + if (this.props.width != 0) { + var rangePerPixelInv = this.props.width / range; + var xOffset = offset * rangePerPixelInv; + this.svg.style.left = -this.props.width - xOffset + "px"; + } + } } + + this.legendLeft.redraw(); + this.legendRight.redraw(); + return resized; }; + /** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * Static function - * @param {Date} date the date to be snapped. - * @param {string} scale Current scale, can be 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day, 'month, 'year'. - * @param {number} step Current step (1, 2, 4, 5, ... - * @return {Date} snappedDate + * Update and redraw the graph. + * */ - TimeStep.snap = function (date, scale, step) { - var clone = new Date(date.valueOf()); + LineGraph.prototype._updateGraph = function () { + // reset the svg elements + DOMutil.prepareElements(this.svgElements); + if (this.props.width != 0 && this.itemsData != null) { + var group, i; + var preprocessedGroupData = {}; + var processedGroupData = {}; + var groupRanges = {}; + var changeCalled = false; - if (scale == "year") { - var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); - clone.setFullYear(Math.round(year / step) * step); - clone.setMonth(0); - clone.setDate(0); - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == "month") { - if (clone.getDate() > 15) { - clone.setDate(1); - clone.setMonth(clone.getMonth() + 1); - // important: first set Date to 1, after that change the month. - } else { - clone.setDate(1); + // getting group Ids + var groupIds = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + group = this.groups[groupId]; + if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) { + groupIds.push(groupId); + } + } } + if (groupIds.length > 0) { + // this is the range of the SVG canvas + var minDate = this.body.util.toGlobalTime(-this.body.domProps.root.width); + var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width); + var groupsData = {}; + // fill groups data, this only loads the data we require based on the timewindow + this._getRelevantData(groupIds, groupsData, minDate, maxDate); - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == "day") { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 24) * 24);break; - default: - clone.setHours(Math.round(clone.getHours() / 12) * 12);break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == "weekday") { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 12) * 12);break; - default: - clone.setHours(Math.round(clone.getHours() / 6) * 6);break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == "hour") { - switch (step) { - case 4: - clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60);break; - default: - clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30);break; - } - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (scale == "minute") { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 15: - case 10: - clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); - clone.setSeconds(0); - break; - case 5: - clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60);break; - default: - clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30);break; - } - clone.setMilliseconds(0); - } else if (scale == "second") { - //noinspection FallthroughInSwitchStatementJS - switch (step) { - case 15: - case 10: - clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); - clone.setMilliseconds(0); - break; - case 5: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000);break; - default: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500);break; + // apply sampling, if disabled, it will pass through this function. + this._applySampling(groupIds, groupsData); + + // 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); + var MAX_CYCLES = 5; + if (changeCalled == true && this.COUNTER < MAX_CYCLES) { + DOMutil.cleanupElements(this.svgElements); + this.abortedGraphUpdate = true; + this.COUNTER++; + this.body.emitter.emit("change"); + return true; + } else { + if (this.COUNTER > MAX_CYCLES) { + console.log("WARNING: there may be an infinite loop in the _updateGraph emitter cycle."); + } + this.COUNTER = 0; + this.abortedGraphUpdate = false; + + // With the yAxis scaled correctly, use this to get the Y values of the points. + for (i = 0; i < groupIds.length; i++) { + group = this.groups[groupIds[i]]; + processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group); + } + + // draw the groups + for (i = 0; i < groupIds.length; i++) { + group = this.groups[groupIds[i]]; + if (group.options.style != "bar") { + // bar needs to be drawn enmasse + group.draw(processedGroupData[groupIds[i]], group, this.framework); + } + } + BarGraphFunctions.draw(groupIds, processedGroupData, this.framework); + } } - } else if (scale == "millisecond") { - var _step = step > 5 ? step / 2 : 1; - clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); } - return clone; + // cleanup unused svg elements + DOMutil.cleanupElements(this.svgElements); + return false; }; + /** - * 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. + * first select and preprocess the data from the datasets. + * the groups have their preselection of data, we now loop over this data to see + * what data we need to draw. Sorted data is much faster. + * more optimization is possible by doing the sampling before and using the binary search + * to find the end date to determine the increment. + * + * @param {array} groupIds + * @param {object} groupsData + * @param {date} minDate + * @param {date} maxDate + * @private */ - TimeStep.prototype.isMajor = function () { - if (this.switchedYear == true) { - this.switchedYear = false; - switch (this.scale) { - case "year": - case "month": - case "weekday": - case "day": - case "hour": - case "minute": - case "second": - case "millisecond": - return true; - default: - return false; - } - } else if (this.switchedMonth == true) { - this.switchedMonth = false; - switch (this.scale) { - case "weekday": - case "day": - case "hour": - case "minute": - case "second": - case "millisecond": - return true; - default: - return false; - } - } else if (this.switchedDay == true) { - this.switchedDay = false; - switch (this.scale) { - case "millisecond": - case "second": - case "minute": - case "hour": - return true; - default: - return false; + LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { + var group, i, j, item; + if (groupIds.length > 0) { + for (i = 0; i < groupIds.length; i++) { + group = this.groups[groupIds[i]]; + groupsData[groupIds[i]] = []; + var dataContainer = groupsData[groupIds[i]]; + // optimization for sorted data + if (group.options.sort == true) { + var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, "x", "before")); + for (j = guess; j < group.itemsData.length; j++) { + item = group.itemsData[j]; + if (item !== undefined) { + if (item.x > maxDate) { + dataContainer.push(item); + break; + } else { + dataContainer.push(item); + } + } + } + } else { + for (j = 0; j < group.itemsData.length; j++) { + item = group.itemsData[j]; + if (item !== undefined) { + if (item.x > minDate && item.x < maxDate) { + dataContainer.push(item); + } + } + } + } } } - - switch (this.scale) { - case "millisecond": - return this.current.getMilliseconds() == 0; - case "second": - return this.current.getSeconds() == 0; - case "minute": - return this.current.getHours() == 0 && this.current.getMinutes() == 0; - case "hour": - return this.current.getHours() == 0; - case "weekday": - // intentional fall through - case "day": - return this.current.getDate() == 1; - case "month": - return this.current.getMonth() == 0; - case "year": - return false; - default: - return false; - } }; /** - * 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 + * + * @param groupIds + * @param groupsData + * @private */ - TimeStep.prototype.getLabelMinor = function (date) { - if (date == undefined) { - date = this.current; - } + LineGraph.prototype._applySampling = function (groupIds, groupsData) { + var group; + if (groupIds.length > 0) { + for (var i = 0; i < groupIds.length; i++) { + group = this.groups[groupIds[i]]; + if (group.options.sampling == true) { + var dataContainer = groupsData[groupIds[i]]; + if (dataContainer.length > 0) { + var increment = 1; + var amountOfPoints = dataContainer.length; - var format = this.format.minorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ""; + // 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))); + + var sampledData = []; + for (var j = 0; j < amountOfPoints; j += increment) { + sampledData.push(dataContainer[j]); + } + groupsData[groupIds[i]] = sampledData; + } + } + } + } }; + /** - * 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 + * + * + * @param {array} groupIds + * @param {object} groupsData + * @param {object} groupRanges | this is being filled here + * @private */ - TimeStep.prototype.getLabelMajor = function (date) { - if (date == undefined) { - date = this.current; - } + LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { + var groupData, group, i; + var barCombinedDataLeft = []; + var barCombinedDataRight = []; + var options; + if (groupIds.length > 0) { + for (i = 0; i < groupIds.length; i++) { + groupData = groupsData[groupIds[i]]; + options = this.groups[groupIds[i]].options; + if (groupData.length > 0) { + group = this.groups[groupIds[i]]; + // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. + if (options.barChart.handleOverlap == "stack" && options.style == "bar") { + if (options.yAxisOrientation == "left") { + barCombinedDataLeft = barCombinedDataLeft.concat(group.getYRange(groupData)); + } else { + barCombinedDataRight = barCombinedDataRight.concat(group.getYRange(groupData)); + } + } else { + groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); + } + } + } - var format = this.format.majorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ""; + // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. + BarGraphFunctions.getStackedBarYRange(barCombinedDataLeft, groupRanges, groupIds, "__barchartLeft", "left"); + BarGraphFunctions.getStackedBarYRange(barCombinedDataRight, groupRanges, groupIds, "__barchartRight", "right"); + } }; - TimeStep.prototype.getClassName = function () { - var m = moment(this.current); - var date = m.locale ? m.locale("en") : m.lang("en"); // old versions of moment have .lang() function - var step = this.step; - function even(value) { - return value / step % 2 == 0 ? " even" : " odd"; - } + /** + * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden. + * @param {Array} groupIds + * @param {Object} groupRanges + * @private + */ + LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) { + var resized = false; + var yAxisLeftUsed = false; + var yAxisRightUsed = false; + var minLeft = 1000000000, + minRight = 1000000000, + maxLeft = -1000000000, + maxRight = -1000000000, + minVal, + maxVal; + // if groups are present + if (groupIds.length > 0) { + // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop. + for (var i = 0; i < groupIds.length; i++) { + var group = this.groups[groupIds[i]]; + if (group && group.options.yAxisOrientation != "right") { + yAxisLeftUsed = true; + minLeft = 0; + maxLeft = 0; + } else if (group && group.options.yAxisOrientation) { + yAxisRightUsed = true; + minRight = 0; + maxRight = 0; + } + } - function today(date) { - if (date.isSame(new Date(), "day")) { - return " today"; + // if there are items: + for (var i = 0; i < groupIds.length; i++) { + if (groupRanges.hasOwnProperty(groupIds[i])) { + if (groupRanges[groupIds[i]].ignore !== true) { + minVal = groupRanges[groupIds[i]].min; + maxVal = groupRanges[groupIds[i]].max; + + if (groupRanges[groupIds[i]].yAxisOrientation != "right") { + yAxisLeftUsed = true; + minLeft = minLeft > minVal ? minVal : minLeft; + maxLeft = maxLeft < maxVal ? maxVal : maxLeft; + } else { + yAxisRightUsed = true; + minRight = minRight > minVal ? minVal : minRight; + maxRight = maxRight < maxVal ? maxVal : maxRight; + } + } + } } - if (date.isSame(moment().add(1, "day"), "day")) { - return " tomorrow"; + + if (yAxisLeftUsed == true) { + this.yAxisLeft.setRange(minLeft, maxLeft); } - if (date.isSame(moment().add(-1, "day"), "day")) { - return " yesterday"; + if (yAxisRightUsed == true) { + this.yAxisRight.setRange(minRight, maxRight); } - return ""; } + resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized; + resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized; - function currentWeek(date) { - return date.isSame(new Date(), "week") ? " current-week" : ""; + if (yAxisRightUsed == true && yAxisLeftUsed == true) { + this.yAxisLeft.drawIcons = true; + this.yAxisRight.drawIcons = true; + } else { + this.yAxisLeft.drawIcons = false; + this.yAxisRight.drawIcons = false; } + this.yAxisRight.master = !yAxisLeftUsed; + if (this.yAxisRight.master == false) { + if (yAxisRightUsed == true) { + this.yAxisLeft.lineOffset = this.yAxisRight.width; + } else { + this.yAxisLeft.lineOffset = 0; + } - function currentMonth(date) { - return date.isSame(new Date(), "month") ? " current-month" : ""; + resized = this.yAxisLeft.redraw() || resized; + this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels; + this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing; + resized = this.yAxisRight.redraw() || resized; + } else { + resized = this.yAxisRight.redraw() || resized; } - function currentYear(date) { - return date.isSame(new Date(), "year") ? " current-year" : ""; + // 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); } - switch (this.scale) { - case "millisecond": - return even(date.milliseconds()).trim(); + return resized; + }; - case "second": - return even(date.seconds()).trim(); - case "minute": - return even(date.minutes()).trim(); + /** + * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function + * + * @param {boolean} axisUsed + * @returns {boolean} + * @private + * @param axis + */ + LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { + var changed = false; + if (axisUsed == false) { + if (axis.dom.frame.parentNode && axis.hidden == false) { + axis.hide(); + changed = true; + } + } else { + if (!axis.dom.frame.parentNode && axis.hidden == true) { + axis.show(); + changed = true; + } + } + return changed; + }; + - case "hour": - var hours = date.hours(); - if (this.step == 4) { - hours = hours + "-h" + (hours + 4); - } - return "h" + hours + today(date) + even(date.hours()); + /** + * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the + * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for + * the yAxis. + * + * @param datapoints + * @returns {Array} + * @private + */ + LineGraph.prototype._convertXcoordinates = function (datapoints) { + var extractedData = []; + var xValue, yValue; + var toScreen = this.body.util.toScreen; - case "weekday": - return date.format("dddd").toLowerCase() + today(date) + currentWeek(date) + even(date.date()); + for (var i = 0; i < datapoints.length; i++) { + xValue = toScreen(datapoints[i].x) + this.props.width; + yValue = datapoints[i].y; + extractedData.push({ x: xValue, y: yValue }); + } - case "day": - var day = date.date(); - var month = date.format("MMMM").toLowerCase(); - return "day" + day + " " + month + currentMonth(date) + even(day - 1); + return extractedData; + }; - case "month": - return date.format("MMMM").toLowerCase() + currentMonth(date) + even(date.month()); - case "year": - var year = date.year(); - return "year" + year + currentYear(date) + even(year); + /** + * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the + * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for + * the yAxis. + * + * @param datapoints + * @param group + * @returns {Array} + * @private + */ + LineGraph.prototype._convertYcoordinates = function (datapoints, group) { + var extractedData = []; + var xValue, yValue; + var toScreen = this.body.util.toScreen; + var axis = this.yAxisLeft; + var svgHeight = Number(this.svg.style.height.replace("px", "")); + if (group.options.yAxisOrientation == "right") { + axis = this.yAxisRight; + } - default: - return ""; + for (var i = 0; i < datapoints.length; i++) { + var labelValue; + //if (datapoints[i].label) { + // labelValue = datapoints[i].label; + //} + //else { + // labelValue = null; + //} + labelValue = datapoints[i].label ? datapoints[i].label : null; + xValue = toScreen(datapoints[i].x) + this.props.width; + yValue = Math.round(axis.convertValue(datapoints[i].y)); + extractedData.push({ x: xValue, y: yValue, label: labelValue }); } + + group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); + + return extractedData; }; - module.exports = TimeStep; + + module.exports = LineGraph; /***/ }, -/* 30 */ +/* 35 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(1); - var stack = __webpack_require__(31); - var RangeItem = __webpack_require__(32); + var Component = __webpack_require__(25); + var TimeStep = __webpack_require__(19); + var DateUtil = __webpack_require__(15); + var moment = __webpack_require__(40); /** - * @constructor Group - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet + * 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 Group(groupId, data, itemSet) { - this.groupId = groupId; - this.subgroups = {}; - this.subgroupIndex = 0; - this.subgroupOrderer = data && data.subgroupOrder; - this.itemSet = itemSet; - - this.dom = {}; - this.props = { - label: { - width: 0, - height: 0 + function TimeAxis(body, options) { + this.dom = { + foreground: null, + lines: [], + majorTexts: [], + minorTexts: [], + redundant: { + lines: [], + majorTexts: [], + minorTexts: [] } }; - this.className = null; + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 + }, + lineTop: 0 + }; - this.items = {}; // items filtered by groupId of this group - this.visibleItems = []; // items currently visible in window - this.orderedItems = { - byStart: [], - byEnd: [] + this.defaultOptions = { + orientation: "bottom", // supported: 'top', 'bottom' + // TODO: implement timeaxis orientations 'left' and 'right' + showMinorLabels: true, + showMajorLabels: true, + format: null, + timeAxis: null }; - this.checkRangedItems = false; // needed to refresh the ranged items if the window is programatically changed with NO overlap. - var me = this; - this.itemSet.body.emitter.on("checkRangedItems", function () { - me.checkRangedItems = true; - }); + this.options = util.extend({}, this.defaultOptions); + + this.body = body; + // create the HTML DOM this._create(); - this.setData(data); + this.setOptions(options); } + TimeAxis.prototype = new Component(); + /** - * Create DOM elements for the group - * @private + * Set options for the TimeAxis. + * Parameters will be merged in current options. + * @param {Object} options Available options: + * {string} [orientation] + * {boolean} [showMinorLabels] + * {boolean} [showMajorLabels] */ - Group.prototype._create = function () { - var label = document.createElement("div"); - label.className = "vlabel"; - this.dom.label = label; - - var inner = document.createElement("div"); - inner.className = "inner"; - label.appendChild(inner); - this.dom.inner = inner; + TimeAxis.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(["orientation", "showMinorLabels", "showMajorLabels", "hiddenDates", "format", "timeAxis"], this.options, options); - var foreground = document.createElement("div"); - foreground.className = "group"; - foreground["timeline-group"] = this; - this.dom.foreground = foreground; + // 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); + } + } + } + }; + /** + * 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.background.className = "group"; - - this.dom.axis = document.createElement("div"); - this.dom.axis.className = "group"; - // create a hidden marker to detect when the Timelines container is attached - // to the DOM, or the style of a parent of the Timeline is changed from - // display:none is changed to visible. - this.dom.marker = document.createElement("div"); - this.dom.marker.style.visibility = "hidden"; // TODO: ask jos why this is not none? - this.dom.marker.innerHTML = "?"; - this.dom.background.appendChild(this.dom.marker); + this.dom.foreground.className = "timeaxis foreground"; + this.dom.background.className = "timeaxis background"; }; /** - * Set the group data for this group - * @param {Object} data Group data, can contain properties content and className + * Destroy the TimeAxis */ - 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 + 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); } - // update title - this.dom.label.title = data && data.title || ""; + this.body = null; + }; - if (!this.dom.inner.firstChild) { - util.addClassName(this.dom.inner, "hidden"); - } else { - util.removeClassName(this.dom.inner, "hidden"); - } + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + TimeAxis.prototype.redraw = function () { + var options = this.options; + var props = this.props; + var foreground = this.dom.foreground; + var background = this.dom.background; - // update className - var className = data && data.className || null; - if (className != this.className) { - if (this.className) { - util.removeClassName(this.dom.label, this.className); - util.removeClassName(this.dom.foreground, this.className); - util.removeClassName(this.dom.background, this.className); - util.removeClassName(this.dom.axis, this.className); - } - util.addClassName(this.dom.label, className); - util.addClassName(this.dom.foreground, className); - util.addClassName(this.dom.background, className); - util.addClassName(this.dom.axis, className); - this.className = className; - } + // 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; - // update style - if (this.style) { - util.removeCssText(this.dom.label, this.style); - this.style = null; + // calculate character width and height + this._calculateCharSize(); + + // 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; + + // 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 (data && data.style) { - util.addCssText(this.dom.label, data.style); - this.style = data.style; + if (backgroundNextSibling) { + this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling); + } else { + this.body.dom.backgroundVertical.appendChild(background); } + + return this._isResized() || parentChanged; }; /** - * Get the width of the group label - * @return {number} width + * Repaint major and minor text labels and vertical grid lines + * @private */ - Group.prototype.getLabelWidth = function () { - return this.props.label.width; - }; + TimeAxis.prototype._repaintLabels = function () { + var orientation = this.options.orientation; + // calculate range and step (step such that we have space for 7 characters per label) + var start = util.convert(this.body.range.start, "Number"); + var end = util.convert(this.body.range.end, "Number"); + var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf(); + var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this.body.range, timeLabelsize); + minimumStep -= this.body.util.toTime(0).valueOf(); - /** - * Repaint this group - * @param {{start: number, end: number}} range - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @param {boolean} [restack=false] Force restacking of all items - * @return {boolean} Returns true if the group is resized - */ - Group.prototype.redraw = function (range, margin, restack) { - var resized = false; + var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates); + if (this.options.format) { + step.setFormat(this.options.format); + } + if (this.options.timeAxis) { + step.setScale(this.options.timeAxis); + } + this.step = step; - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + // Move all DOM elements to a "redundant" list, where they + // can be picked for re-use, and clear the lists with lines and texts. + // At the end of the function _repaintLabels, left over elements will be cleaned up + var dom = this.dom; + dom.redundant.lines = dom.lines; + dom.redundant.majorTexts = dom.majorTexts; + dom.redundant.minorTexts = dom.minorTexts; + dom.lines = []; + dom.majorTexts = []; + dom.minorTexts = []; - // 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; + var cur; + var x = 0; + var isMajor; + var xPrev = 0; + var width = 0; + var prevLine; + var xFirstMajorLabel = undefined; + var max = 0; + var className; - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); + step.first(); + while (step.hasNext() && max < 1000) { + max++; - restack = true; - } + cur = step.getCurrent(); + isMajor = step.isMajor(); + className = step.getClassName(); - // reposition visible items vertically - if (this.itemSet.options.stack) { - // TODO: ugly way to access options... - stack.stack(this.visibleItems, margin, restack); - } else { - // no stacking - stack.nostack(this.visibleItems, margin, this.subgroups); + xPrev = x; + x = this.body.util.toScreen(cur); + width = x - xPrev; + if (prevLine) { + prevLine.style.width = width + "px"; + } + + if (this.options.showMinorLabels) { + this._repaintMinorText(x, step.getLabelMinor(), orientation, className); + } + + if (isMajor && this.options.showMajorLabels) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this._repaintMajorText(x, step.getLabelMajor(), orientation, className); + } + prevLine = this._repaintMajorLine(x, orientation, className); + } else { + prevLine = this._repaintMinorLine(x, orientation, className); + } + + step.next(); } - // recalculate the height of the group - var height = this._calculateHeight(margin); + // 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 - // 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 (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText, orientation, className); + } + } - // 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; + // 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); + } + } + }); + }; - // apply new height - this.dom.background.style.height = height + "px"; - this.dom.foreground.style.height = height + "px"; - this.dom.label.style.height = height + "px"; + /** + * Create a minor label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className + * @private + */ + TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); - // update vertical position of items after they are re-stacked and the height of the group is calculated - for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { - var item = this.visibleItems[i]; - item.repositionY(margin); + if (!label) { + // create new label + var content = document.createTextNode(""); + label = document.createElement("div"); + label.appendChild(content); + this.dom.foreground.appendChild(label); } + this.dom.minorTexts.push(label); - return resized; + label.childNodes[0].nodeValue = text; + + label.style.top = orientation == "top" ? this.props.majorLabelHeight + "px" : "0"; + label.style.left = x + "px"; + label.className = "text minor " + className; + //label.title = title; // TODO: this is a heavy operation }; /** - * recalculate the height of the group - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @returns {number} Returns the height + * Create a Major label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className * @private */ - Group.prototype._calculateHeight = function (margin) { - // recalculate the height of the group - var height; - var visibleItems = this.visibleItems; - //var visibleSubgroups = []; - //this.visibleSubgroups = 0; - this.resetSubgroups(); - var me = this; - if (visibleItems.length > 0) { - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, item.top + item.height); - if (item.data.subgroup !== undefined) { - me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); - me.subgroups[item.data.subgroup].visible = true; - } - }); - if (min > margin.axis) { - // there is an empty gap between the lowest item and the axis - var offset = min - margin.axis; - max -= offset; - util.forEach(visibleItems, function (item) { - item.top -= offset; - }); - } - height = max + margin.item.vertical / 2; - } else { - height = margin.axis + margin.item.vertical; + TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); + + if (!label) { + // create label + var content = document.createTextNode(text); + label = document.createElement("div"); + label.appendChild(content); + this.dom.foreground.appendChild(label); } - height = Math.max(height, this.props.label.height); + this.dom.majorTexts.push(label); - return height; + label.childNodes[0].nodeValue = text; + label.className = "text major " + className; + //label.title = title; // TODO: this is a heavy operation + + label.style.top = orientation == "top" ? "0" : this.props.minorLabelHeight + "px"; + label.style.left = x + "px"; }; /** - * Show this group: attach to the DOM + * Create a minor line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className + * @return {Element} Returns the created line + * @private */ - Group.prototype.show = function () { - if (!this.dom.label.parentNode) { - this.itemSet.dom.labelSet.appendChild(this.dom.label); + TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) { + // reuse redundant line + var line = this.dom.redundant.lines.shift(); + if (!line) { + // create vertical line + line = document.createElement("div"); + this.dom.background.appendChild(line); } + this.dom.lines.push(line); - if (!this.dom.foreground.parentNode) { - this.itemSet.dom.foreground.appendChild(this.dom.foreground); + 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"; - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } + line.className = "grid vertical minor " + className; - if (!this.dom.axis.parentNode) { - this.itemSet.dom.axis.appendChild(this.dom.axis); - } + return line; }; /** - * Hide this group: remove from the DOM + * Create a Major line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className + * @return {Element} Returns the created line + * @private */ - Group.prototype.hide = function () { - var label = this.dom.label; - if (label.parentNode) { - label.parentNode.removeChild(label); + TimeAxis.prototype._repaintMajorLine = function (x, orientation, className) { + // reuse redundant line + var line = this.dom.redundant.lines.shift(); + if (!line) { + // create vertical line + line = document.createElement("div"); + this.dom.background.appendChild(line); } + this.dom.lines.push(line); - var foreground = this.dom.foreground; - if (foreground.parentNode) { - foreground.parentNode.removeChild(foreground); + 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"; - var background = this.dom.background; - if (background.parentNode) { - background.parentNode.removeChild(background); - } + line.className = "grid vertical major " + className; - var axis = this.dom.axis; - if (axis.parentNode) { - axis.parentNode.removeChild(axis); - } + return line; }; /** - * Add an item to the group - * @param {Item} item + * 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 */ - Group.prototype.add = function (item) { - this.items[item.id] = item; - item.setParent(this); + 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. - // add to - if (item.data.subgroup !== undefined) { - if (this.subgroups[item.data.subgroup] === undefined) { - this.subgroups[item.data.subgroup] = { height: 0, visible: false, index: this.subgroupIndex, items: [] }; - this.subgroupIndex++; - } - this.subgroups[item.data.subgroup].items.push(item); - } - this.orderSubgroups(); + // 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"; - 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.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; - Group.prototype.orderSubgroups = function () { - if (this.subgroupOrderer !== undefined) { - var sortArray = []; - if (typeof this.subgroupOrderer == "string") { - for (var subgroup in this.subgroups) { - sortArray.push({ subgroup: subgroup, sortField: this.subgroups[subgroup].items[0].data[this.subgroupOrderer] }); - } - sortArray.sort(function (a, b) { - return a.sortField - b.sortField; - }); - } else if (typeof this.subgroupOrderer == "function") { - for (var subgroup in this.subgroups) { - sortArray.push(this.subgroups[subgroup].items[0].data); - } - sortArray.sort(this.subgroupOrderer); - } + // 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 major measure"; + this.dom.measureCharMajor.style.position = "absolute"; - if (sortArray.length > 0) { - for (var i = 0; i < sortArray.length; i++) { - this.subgroups[sortArray[i].subgroup].index = i; - } - } + 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; }; - Group.prototype.resetSubgroups = function () { - for (var subgroup in this.subgroups) { - if (this.subgroups.hasOwnProperty(subgroup)) { - this.subgroups[subgroup].visible = false; - } - } - }; + module.exports = TimeAxis; - /** - * Remove an item from the group - * @param {Item} item - */ - Group.prototype.remove = function (item) { - delete this.items[item.id]; - item.setParent(null); +/***/ }, +/* 36 */ +/***/ function(module, exports, __webpack_require__) { - // remove from visible items - var index = this.visibleItems.indexOf(item); - if (index != -1) this.visibleItems.splice(index, 1); + "use strict"; - // TODO: also remove from ordered items? - }; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + // Load custom shapes into CanvasRenderingContext2D + __webpack_require__(48); + + var Emitter = __webpack_require__(62); + var Hammer = __webpack_require__(41); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var dotparser = __webpack_require__(38); + var gephiParser = __webpack_require__(39); + var Images = __webpack_require__(37); + var Activator = __webpack_require__(49); + + var Groups = _interopRequire(__webpack_require__(50)); + + var NodesHandler = _interopRequire(__webpack_require__(51)); + + var EdgesHandler = _interopRequire(__webpack_require__(52)); + + var PhysicsEngine = _interopRequire(__webpack_require__(53)); + + var ClusterEngine = _interopRequire(__webpack_require__(54)); + + var CanvasRenderer = _interopRequire(__webpack_require__(55)); + + var Canvas = _interopRequire(__webpack_require__(56)); + + var View = _interopRequire(__webpack_require__(57)); + + var InteractionHandler = _interopRequire(__webpack_require__(58)); + + var SelectionHandler = _interopRequire(__webpack_require__(59)); + + var LayoutEngine = _interopRequire(__webpack_require__(60)); + var ManipulationSystem = _interopRequire(__webpack_require__(61)); /** - * Remove an item from the corresponding DataSet - * @param {Item} item + * @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 */ - Group.prototype.removeFromDataSet = function (item) { - this.itemSet.removeItem(item.id); - }; + function Network(container, data, options) { + var _this = this; + if (!(this instanceof Network)) { + throw new SyntaxError("Constructor must be called with the new operator"); + } + + // set constant values + this.options = {}; + this.defaultOptions = { + clickToUse: false + }; + util.extend(this.options, this.defaultOptions); + + // containers for nodes and edges + this.body = { + nodes: {}, + nodeIndices: [], + edges: {}, + edgeIndices: [], + data: { + nodes: null, // A DataSet or DataView + edges: null // A DataSet or DataView + }, + functions: { + createNode: function () {}, + createEdge: function () {}, + getPointer: function () {} + }, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this), + once: this.once.bind(this) + }, + eventListeners: { + onTap: function () {}, + onTouch: function () {}, + onDoubleTap: function () {}, + onHold: function () {}, + onDragStart: function () {}, + onDrag: function () {}, + onDragEnd: function () {}, + onMouseWheel: function () {}, + onPinch: function () {}, + onMouseMove: function () {}, + onRelease: function () {} + }, + container: container, + view: { + scale: 1, + translation: { x: 0, y: 0 } + } + }; + + // bind the event listeners + this.bindEventListeners(); + + // setting up all modules + var images = new Images(function () { + return _this.body.emitter.emit("_requestRedraw"); + }); // object with images + + this.groups = new Groups(); // object with groups + this.canvas = new Canvas(this.body); // DOM handler + this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler + this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key + this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms + this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into + this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations + this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout + this.clustering = new ClusterEngine(this.body); // clustering api + this.manipulation = new ManipulationSystem(this.body, this.canvas, this.selectionHandler); // data manipulation system + + this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options + this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options + + // create the DOM elements + this.canvas.create(); + + // apply options + this.setOptions(options); + + // load data (the disable start variable will be the same as the enabled clustering) + this.setData(data); + } + + // Extend Network with an Emitter mixin + Emitter(Network.prototype); + /** - * Reorder the items + * Set options + * @param {Object} options */ - Group.prototype.order = function () { - var array = util.toArray(this.items); - var startArray = []; - var endArray = []; + Network.prototype.setOptions = function (options) { + if (options !== undefined) { + // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. + options = this.layoutEngine.setOptions(options.layout, options); - for (var i = 0; i < array.length; i++) { - if (array[i].data.end !== undefined) { - endArray.push(array[i]); + this.groups.setOptions(options.groups); + this.nodesHandler.setOptions(options.nodes); + this.edgesHandler.setOptions(options.edges); + this.physics.setOptions(options.physics); + this.canvas.setOptions(options.canvas); + this.renderer.setOptions(options.rendering); + this.view.setOptions(options.view); + this.interactionHandler.setOptions(options.interaction); + this.selectionHandler.setOptions(options.selection); + this.clustering.setOptions(options.clustering); + this.manipulation.setOptions(options.manipulation); + + + if (options.clickToUse !== undefined) { + if (options.clickToUse === true) { + if (this.activator === undefined) { + this.activator = new Activator(this.frame); + this.activator.on("change", this._createKeyBinds.bind(this)); + } + } else { + if (this.activator !== undefined) { + this.activator.destroy(); + delete this.activator; + } + this.body.emitter.emit("activate"); + } + } else { + this.body.emitter.emit("activate"); } - startArray.push(array[i]); - } - this.orderedItems = { - byStart: startArray, - byEnd: endArray - }; - stack.orderByStart(this.orderedItems.byStart); - stack.orderByEnd(this.orderedItems.byEnd); + this.canvas.setSize(); + } }; /** - * 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. + * Update the this.body.nodeIndices with the most recent node index list * @private */ - Group.prototype._updateVisibleItems = function (orderedItems, oldVisibleItems, range) { - var visibleItems = []; - var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems - var interval = (range.end - range.start) / 4; - var lowerBound = range.start - interval; - var upperBound = range.end + interval; - var item, i; + Network.prototype._updateVisibleIndices = function () { + var nodes = this.body.nodes; + var edges = this.body.edges; + this.body.nodeIndices = []; + this.body.edgeIndices = []; - // this function is used to do the binary search. - var searchFunction = function (value) { - if (value < lowerBound) { - return -1; - } else if (value <= upperBound) { - return 0; - } else { - return 1; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].options.hidden === false) { + this.body.nodeIndices.push(nodeId); + } } - }; + } - // first check if the items that were in view previously are still in view. - // IMPORTANT: this handles the case for the items with startdate before the window and enddate after the window! - // also cleans up invisible items. - if (oldVisibleItems.length > 0) { - for (i = 0; i < oldVisibleItems.length; i++) { - this._checkIfVisibleWithReference(oldVisibleItems[i], visibleItems, visibleItemsLookup, range); + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].options.hidden === false) { + this.body.edgeIndices.push(edgeId); + } } } + }; - // we do a binary search for the items that have only start values. - var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, "data", "start"); + Network.prototype.bindEventListeners = function () { + var _this = this; + // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. + this.body.emitter.on("_dataChanged", function (params) { + var t0 = new Date().valueOf(); + // update shortcut lists + _this._updateVisibleIndices(); + _this.physics.updatePhysicsIndices(); - // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. - this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { - return item.data.start < lowerBound || item.data.start > upperBound; + // call the dataUpdated event because the only difference between the two is the updating of the indices + _this.body.emitter.emit("_dataUpdated"); + + console.log("_dataChanged took:", new Date().valueOf() - t0); }); - // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. - // We therefore have to brute force check all items in the byEnd list - if (this.checkRangedItems == true) { - this.checkRangedItems = false; - for (i = 0; i < orderedItems.byEnd.length; i++) { - this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on("_dataUpdated", function () { + var t0 = new Date().valueOf(); + // update values + _this._updateValueRange(_this.body.nodes); + _this._updateValueRange(_this.body.edges); + // start simulation (can be called safely, even if already running) + _this.body.emitter.emit("startSimulation"); + + console.log("_dataUpdated took:", new Date().valueOf() - t0); + }); + }; + + /** + * 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. + */ + Network.prototype.setData = function (data) { + // reset the physics engine. + this.body.emitter.emit("resetPhysics"); + this.body.emitter.emit("_resetData"); + + // unselect all to ensure no selections from old data are carried over. + this.selectionHandler.unselectAll(); + + 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; } } else { - // we do a binary search for the items that have defined end times. - var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, "data", "end"); - - // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. - this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { - return item.data.end < lowerBound || item.data.end > upperBound; - }); + this.nodesHandler.setData(data && data.nodes, true); + this.edgesHandler.setData(data && data.edges, true); } + // emit change in data + this.body.emitter.emit("_dataChanged"); - // finally, we reposition all the visible items. - for (i = 0; i < visibleItems.length; i++) { - item = visibleItems[i]; - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); - } + // find a stable position or start animating to a stable position + this.body.emitter.emit("initPhysics"); + }; - // debug - //console.log("new line") - //if (this.groupId == null) { - // for (i = 0; i < orderedItems.byStart.length; i++) { - // item = orderedItems.byStart[i].data; - // console.log('start',i,initialPosByStart, item.start.valueOf(), item.content, item.start >= lowerBound && item.start <= upperBound,i == initialPosByStart ? "<------------------- HEREEEE" : "") - // } - // for (i = 0; i < orderedItems.byEnd.length; i++) { - // item = orderedItems.byEnd[i].data; - // console.log('rangeEnd',i,initialPosByEnd, item.end.valueOf(), item.content, item.end >= range.start && item.end <= range.end,i == initialPosByEnd ? "<------------------- HEREEEE" : "") - // } - //} - return visibleItems; + /** + * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. + * var network = new vis.Network(..); + * network.destroy(); + * network = null; + */ + Network.prototype.destroy = function () { + this.body.emitter.emit("destroy"); + + // clear events + this.body.emitter.off(); + + // remove the container and everything inside it recursively + util.recursiveDOMDelete(this.body.container); }; - Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { - var item; - var i; - if (initialPos != -1) { - for (i = initialPos; i >= 0; i--) { - item = items[i]; - if (breakCondition(item)) { - break; - } else { - if (visibleItemsLookup[item.id] === undefined) { - visibleItemsLookup[item.id] = true; - visibleItems.push(item); - } + + /** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Object} obj An object containing a set of Edges or Nodes + * The objects must have a method getValue() and + * setValueRange(min, max). + * @private + */ + Network.prototype._updateValueRange = function (obj) { + var id; + + // determine the range of the objects + var valueMin = undefined; + var valueMax = undefined; + var valueTotal = 0; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + var value = obj[id].getValue(); + if (value !== undefined) { + valueMin = valueMin === undefined ? value : Math.min(value, valueMin); + valueMax = valueMax === undefined ? value : Math.max(value, valueMax); + valueTotal += value; } } + } - for (i = initialPos + 1; i < items.length; i++) { - item = items[i]; - if (breakCondition(item)) { - break; - } else { - if (visibleItemsLookup[item.id] === undefined) { - visibleItemsLookup[item.id] = true; - visibleItems.push(item); - } + // adjust the range of all objects + if (valueMin !== undefined && valueMax !== undefined) { + for (id in obj) { + if (obj.hasOwnProperty(id)) { + obj[id].setValueRange(valueMin, valueMax, valueTotal); } } } @@ -17649,1993 +15808,2151 @@ return /******/ (function(modules) { // webpackBootstrap /** - * 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 + * Scale the network + * @param {Number} scale Scaling factor 1.0 is unscaled * @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(); - } + Network.prototype._setScale = function (scale) { + this.body.view.scale = scale; }; - /** - * 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 + * Get the current scale of the network + * @return {Number} scale Scaling factor 1.0 is unscaled * @private */ - Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { - if (item.isVisible(range)) { - if (visibleItemsLookup[item.id] === undefined) { - visibleItemsLookup[item.id] = true; - visibleItems.push(item); + Network.prototype._getScale = function () { + return this.body.view.scale; + }; + + + /** + * Load the XY positions of the nodes into the dataset. + */ + Network.prototype.storePositions = function () { + // todo: incorporate fixed instead of allowedtomove, add support for clusters and hierarchical. + var dataArray = []; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + var allowedToMoveX = !this.body.nodes.xFixed; + var allowedToMoveY = !this.body.nodes.yFixed; + if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._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 }); + } } - } else { - if (item.displayed) item.hide(); } + this.body.data.nodes.update(dataArray); }; + /** + * Return the positions of the nodes. + */ + Network.prototype.getPositions = function (ids) { + var dataArray = {}; + if (ids !== undefined) { + if (Array.isArray(ids) == true) { + for (var i = 0; i < ids.length; i++) { + if (this.body.nodes[ids[i]] !== undefined) { + var node = this.body.nodes[ids[i]]; + dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } else { + if (this.body.nodes[ids] !== undefined) { + var node = this.body.nodes[ids]; + dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; + } + } + } + return dataArray; + }; - module.exports = Group; + /** + * Returns true when the Network is active. + * @returns {boolean} + */ + Network.prototype.isActive = function () { + return !this.activator || this.activator.active; + }; -/***/ }, -/* 31 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + /** + * Sets the scale + * @returns {Number} + */ + Network.prototype.setScale = function () { + return this._setScale(); + }; - // Utility functions for ordering and stacking of items - var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors /** - * Order items by their start data - * @param {Item[]} items + * Returns the scale + * @returns {Number} */ - exports.orderByStart = function (items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); + Network.prototype.getScale = function () { + return this._getScale(); }; + /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items + * Check if a node is a cluster. + * @param nodeId + * @returns {*} */ - 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; - }); + Network.prototype.isCluster = function (nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster; + } else { + console.log("Node does not exist."); + return false; + } }; /** - * 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 + * Returns the scale + * @returns {Number} */ - exports.stack = function (items, margin, force) { - var i, iMax; + Network.prototype.getCenterCoordinates = function () { + return this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); + }; - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } - } - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.stack && item.top === null) { - // initialize top position - item.top = margin.axis; + Network.prototype.getBoundingBox = function (nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].boundingBox; + } + }; - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - var collidingItem = null; - for (var j = 0, jj = items.length; j < jj; j++) { - var other = items[j]; - if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item)) { - collidingItem = other; - break; - } + Network.prototype.getConnectedNodes = function (nodeId) { + var nodeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + var nodeObj = { nodeId: true }; // used to quickly check if node already exists + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + if (edge.toId == nodeId) { + if (nodeObj[edge.fromId] === undefined) { + nodeList.push(edge.fromId); + nodeObj[edge.fromId] = true; } - - if (collidingItem != null) { - // There is a collision. Reposition the items above the colliding element - item.top = collidingItem.top + collidingItem.height + margin.item.vertical; + } else if (edge.fromId == nodeId) { + if (nodeObj[edge.toId] === undefined) { + nodeList.push(edge.toId); + nodeObj[edge.toId] = true; } - } while (collidingItem); + } } } + return nodeList; }; - /** - * Adjust vertical positions of the items without stacking them - * @param {Item[]} items - * All visible items - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * Margins between items and between items and the axis. - */ - exports.nostack = function (items, margin, subgroups) { - var i, iMax, newTop; - - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - if (items[i].data.subgroup !== undefined) { - newTop = margin.axis; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroups[items[i].data.subgroup].index) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } - } - items[i].top = newTop; - } else { - items[i].top = margin.axis; + Network.prototype.getEdgesFromNode = function (nodeId) { + var edgesList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + for (var i = 0; i < node.edges.length; i++) { + edgesList.push(node.edges[i].id); } } + return edgesList; }; - /** - * Test if the two provided items collide - * The items must have parameters left, width, top, and height. - * @param {Item} a The first item - * @param {Item} b The second item - * @param {{horizontal: number, vertical: number}} margin - * An object containing a horizontal and vertical - * minimum required margin. - * @return {boolean} true if a and b collide, else false - */ - exports.collision = function (a, b, margin) { - return a.left - margin.horizontal + EPSILON < b.left + b.width && a.left + a.width + margin.horizontal - EPSILON > b.left && a.top - margin.vertical + EPSILON < b.top + b.height && a.top + a.height + margin.vertical - EPSILON > b.top; + Network.prototype.generateColorObject = function (color) { + return util.parseColor(color); }; + module.exports = Network; + /***/ }, -/* 32 */ +/* 37 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Hammer = __webpack_require__(19); - var Item = __webpack_require__(33); + /** + * @class Images + * This class loads images and keeps them stored. + */ + function Images(callback) { + this.images = {}; + this.imageBroken = {}; + this.callback = callback; + } /** - * @constructor RangeItem - * @extends Item - * @param {Object} data Object containing parameters start, end - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe options + * + * @param {string} url Url of the image + * @param {string} url Url of an image to use if the url image is not found + * @return {Image} img The image object */ - function RangeItem(data, conversion, options) { - this.props = { - content: { - width: 0 - } - }; - this.overflow = false; // if contents can overflow (css styling), this flag is set to true + Images.prototype.load = function (url, brokenUrl) { + var img = this.images[url]; // make a pointer + if (img === undefined) { + // create the image + var me = this; + img = new Image(); + img.onload = function () { + // IE11 fix -- thanks dponch! + if (this.width == 0) { + document.body.appendChild(this); + this.width = this.offsetWidth; + this.height = this.offsetHeight; + document.body.removeChild(this); + } - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data.id); - } - if (data.end == undefined) { - throw new Error("Property \"end\" missing in item " + data.id); - } + if (me.callback) { + me.images[url] = img; + me.callback(this); + } + }; + + img.onerror = function () { + if (brokenUrl === undefined) { + console.error("Could not load image:", url); + delete this.src; + if (me.callback) { + me.callback(this); + } + } else { + if (me.imageBroken[url] === true) { + console.error("Could not load brokenImage:", brokenUrl); + delete this.src; + if (me.callback) { + me.callback(this); + } + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + me.imageBroken[url] = true; + } + } + }; + + img.src = url; } - Item.call(this, data, conversion, options); - } + return img; + }; - RangeItem.prototype = new Item(null, null, null); + module.exports = Images; - RangeItem.prototype.baseClassName = "item range"; +/***/ }, +/* 38 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - RangeItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; - }; + "use strict"; /** - * Repaint the item + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges */ - RangeItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; - - // background box - dom.box = document.createElement("div"); - // className is updated in redraw() + function parseDOT(data) { + dot = data; + return parseGraph(); + } - // contents box - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); + // token types enumeration + var TOKENTYPE = { + NULL: 0, + DELIMITER: 1, + IDENTIFIER: 2, + UNKNOWN: 3 + }; - // attach this item as attribute - dom.box["timeline-item"] = this; + // map with all delimiters + var DELIMITERS = { + "{": true, + "}": true, + "[": true, + "]": true, + ";": true, + "=": true, + ",": true, - this.dirty = true; - } + "->": true, + "--": true + }; - // append DOM to parent DOM - if (!this.parent) { - throw new Error("Cannot redraw item: no parent attached"); - } - if (!dom.box.parentNode) { - var foreground = this.parent.dom.foreground; - if (!foreground) { - throw new Error("Cannot redraw item: parent has no foreground container element"); - } - foreground.appendChild(dom.box); - } - this.displayed = true; + var dot = ""; // current dot file + var index = 0; // current index in dot file + var c = ""; // current token character in expr + var token = ""; // current token + var tokenType = TOKENTYPE.NULL; // type of the token - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); + /** + * Get the first character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function first() { + index = 0; + c = dot.charAt(0); + } - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = this.baseClassName + className; + /** + * Get the next character from the dot file. + * The character is stored into the char c. If the end of the dot file is + * reached, the function puts an empty string in c. + */ + function next() { + index++; + c = dot.charAt(index); + } - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } - // recalculate size - // turn off max-width to be able to calculate the real width - // this causes an extra browser repaint/reflow, but so be it - this.dom.content.style.maxWidth = "none"; - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; - this.dom.content.style.maxWidth = ""; + /** + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric + */ + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); + } - this.dirty = false; + /** + * Merge all options of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + function merge(a, b) { + if (!a) { + a = {}; } - this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); - }; + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + return a; + } /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * Set a value in an object, where the provided parameter name can be a + * path with nested parameters. For example: + * + * var obj = {a: 2}; + * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} + * + * @param {Object} obj + * @param {String} path A parameter name or dot-separated parameter path, + * like "color.highlight.border". + * @param {*} value */ - RangeItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); + function setValue(obj, path, value) { + var keys = path.split("."); + var o = obj; + while (keys.length) { + var key = keys.shift(); + if (keys.length) { + // this isn't the end point + if (!o[key]) { + o[key] = {}; + } + o = o[key]; + } else { + // this is the end point + o[key] = value; + } } - }; + } /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed + * Add a node to a graph object. If there is already a node with + * the same id, their attributes will be merged. + * @param {Object} graph + * @param {Object} node */ - RangeItem.prototype.hide = function () { - if (this.displayed) { - var box = this.dom.box; + function addNode(graph, node) { + var i, len; + var current = null; - if (box.parentNode) { - box.parentNode.removeChild(box); + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; + } + + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; + } } + } - this.top = null; - this.left = null; + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } + } - this.displayed = false; + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; + + if (!g.nodes) { + g.nodes = []; + } + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); + } } - }; + + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); + } + } /** - * Reposition the item horizontally - * @Override + * Add an edge to a graph object + * @param {Object} graph + * @param {Object} edge */ - RangeItem.prototype.repositionX = function () { - var parentWidth = this.parent.width; - var start = this.conversion.toScreen(this.data.start); - var end = this.conversion.toScreen(this.data.end); - var contentLeft; - var contentWidth; - - // limit the width of the this, as browsers cannot draw very wide divs - if (start < -parentWidth) { - start = -parentWidth; + function addEdge(graph, edge) { + if (!graph.edges) { + graph.edges = []; } - if (end > 2 * parentWidth) { - end = 2 * parentWidth; + graph.edges.push(edge); + if (graph.edge) { + var attr = merge({}, graph.edge); // clone default attributes + edge.attr = merge(attr, edge.attr); // merge attributes } - var boxWidth = Math.max(end - start, 1); + } - if (this.overflow) { - this.left = start; - this.width = boxWidth + this.props.content.width; - contentWidth = this.props.content.width; + /** + * Create an edge to a graph object + * @param {Object} graph + * @param {String | Number | Object} from + * @param {String | Number | Object} to + * @param {String} type + * @param {Object | null} attr + * @return {Object} edge + */ + function createEdge(graph, from, to, type, attr) { + var edge = { + from: from, + to: to, + type: type + }; - // Note: The calculation of width is an optimistic calculation, giving - // a width which will not change when moving the Timeline - // So no re-stacking needed, which is nicer for the eye; - } else { - this.left = start; - this.width = boxWidth; - contentWidth = Math.min(end - start - 2 * this.options.padding, this.props.content.width); + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes } + edge.attr = merge(edge.attr || {}, attr); // merge attributes - this.dom.box.style.left = this.left + "px"; - this.dom.box.style.width = boxWidth + "px"; + return edge; + } - switch (this.options.align) { - case "left": - this.dom.content.style.left = "0"; - break; + /** + * Get next token in the current dot file. + * The token and token type are available as token and tokenType + */ + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ""; - case "right": - this.dom.content.style.left = Math.max(boxWidth - contentWidth - 2 * this.options.padding, 0) + "px"; - break; + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); + } - case "center": - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + "px"; - break; + do { + var isComment = false; - default: - // 'auto' - // when range exceeds left of the window, position the contents at the left of the visible area - if (this.overflow) { - if (end > 0) { - contentLeft = Math.max(-start, 0); - } else { - contentLeft = -contentWidth; // ensure it's not visible anymore + // skip comment + if (c == "#") { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == " " || dot.charAt(i) == "\t") { + i--; + } + if (dot.charAt(i) == "\n" || dot.charAt(i) == "") { + // the # is at the start of a line, this is indeed a line comment + while (c != "" && c != "\n") { + next(); } - } else { - if (start < 0) { - contentLeft = Math.min(-start, end - start - contentWidth - 2 * this.options.padding); - // TODO: remove the need for options.padding. it's terrible. + isComment = true; + } + } + if (c == "/" && nextPreview() == "/") { + // skip line comment + while (c != "" && c != "\n") { + next(); + } + isComment = true; + } + if (c == "/" && nextPreview() == "*") { + // skip block comment + while (c != "") { + if (c == "*" && nextPreview() == "/") { + // end of block comment found. skip these last two characters + next(); + next(); + break; } else { - contentLeft = 0; + next(); } } - this.dom.content.style.left = contentLeft + "px"; - } - }; + isComment = true; + } - /** - * Reposition the item vertically - * @Override - */ - RangeItem.prototype.repositionY = function () { - var orientation = this.options.orientation, - box = this.dom.box; + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); + } + } while (isComment); - if (orientation == "top") { - box.style.top = this.top + "px"; - } else { - box.style.top = this.parent.height - this.top - this.height + "px"; + // check for end of dot file + if (c == "") { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; } - }; - - /** - * Repaint a drag area on the left side of the range when the range is selected - * @protected - */ - RangeItem.prototype._repaintDragLeft = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) { - // create and show drag area - var dragLeft = document.createElement("div"); - dragLeft.className = "drag-left"; - dragLeft.dragLeftItem = this; - - //// TODO: this should be redundant? - //Hammer(dragLeft, { - // preventDefault: true - //}).on('drag', function () { - // //console.log('drag left') - // }); - this.dom.box.appendChild(dragLeft); - this.dom.dragLeft = dragLeft; - } else if (!this.selected && this.dom.dragLeft) { - // delete drag area - if (this.dom.dragLeft.parentNode) { - this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft); - } - this.dom.dragLeft = null; + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; } - }; - /** - * Repaint a drag area on the right side of the range when the range is selected - * @protected - */ - RangeItem.prototype._repaintDragRight = function () { - if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) { - // create and show drag area - var dragRight = document.createElement("div"); - dragRight.className = "drag-right"; - dragRight.dragRightItem = this; + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; + } - //// TODO: this should be redundant? - //Hammer(dragRight, { - // preventDefault: true - //}).on('drag', function () { - // //console.log('drag right') - //}); + // check for an identifier (number or string) + // TODO: more precise parsing of numbers/strings (and the port separator ':') + if (isAlphaNumeric(c) || c == "-") { + token += c; + next(); - 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); + while (isAlphaNumeric(c)) { + token += c; + next(); } - this.dom.dragRight = null; + if (token == "false") { + token = false; // convert to boolean + } else if (token == "true") { + token = true; // convert to boolean + } else if (!isNaN(Number(token))) { + token = Number(token); // convert to number + } + tokenType = TOKENTYPE.IDENTIFIER; + return; } - }; - - module.exports = RangeItem; - -/***/ }, -/* 33 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + // check for a string enclosed by double quotes + if (c == "\"") { + next(); + while (c != "" && (c != "\"" || c == "\"" && nextPreview() == "\"")) { + token += c; + if (c == "\"") { + // skip the escape character + next(); + } + next(); + } + if (c != "\"") { + throw newSyntaxError("End of string \" expected"); + } + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; + } - var Hammer = __webpack_require__(19); - var util = __webpack_require__(1); + // something unknown is found, wrong characters, a syntax error + tokenType = TOKENTYPE.UNKNOWN; + while (c != "") { + token += c; + next(); + } + throw new SyntaxError("Syntax error in part \"" + chop(token, 30) + "\""); + } /** - * @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 + * Parse a graph. + * @returns {Object} graph */ - 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; - } + function parseGraph() { + var graph = {}; - Item.prototype.stack = true; + first(); + getToken(); - /** - * Select current item - */ - Item.prototype.select = function () { - this.selected = true; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + // optional strict keyword + if (token == "strict") { + graph.strict = true; + getToken(); + } - /** - * Unselect current item - */ - Item.prototype.unselect = function () { - this.selected = false; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + // graph or digraph keyword + if (token == "graph" || token == "digraph") { + graph.type = token; + getToken(); + } - /** - * Set data for the item. Existing data will be updated. The id should not - * be changed. When the item is displayed, it will be redrawn immediately. - * @param {Object} data - */ - Item.prototype.setData = function (data) { - this.data = data; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } - /** - * Set a parent for the item - * @param {ItemSet | Group} parent - */ - Item.prototype.setParent = function (parent) { - if (this.displayed) { - this.hide(); - this.parent = parent; - if (this.parent) { - this.show(); - } - } else { - this.parent = parent; + // open angle bracket + if (token != "{") { + throw newSyntaxError("Angle bracket { expected"); } - }; + getToken(); - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - Item.prototype.isVisible = function (range) { - // Should be implemented by Item implementations - return false; - }; + // statements + parseStatements(graph); - /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed - */ - Item.prototype.show = function () { - return false; - }; + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); + } + getToken(); - /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed - */ - Item.prototype.hide = function () { - return false; - }; + // end of file + if (token !== "") { + throw newSyntaxError("End of file expected"); + } + getToken(); - /** - * Repaint the item - */ - Item.prototype.redraw = function () {}; + // remove temporary default options + delete graph.node; + delete graph.edge; + delete graph.graph; - /** - * Reposition the Item horizontally - */ - Item.prototype.repositionX = function () {}; + return graph; + } /** - * Reposition the Item vertically + * Parse a list with statements. + * @param {Object} graph */ - Item.prototype.repositionY = function () {}; + function parseStatements(graph) { + while (token !== "" && token != "}") { + parseStatement(graph); + if (token == ";") { + getToken(); + } + } + } /** - * Repaint a delete button on the top right of the item when the item is selected - * @param {HTMLElement} anchor - * @protected + * Parse a single statement. Can be a an attribute statement, node + * statement, a series of node statements and edge statements, or a + * parameter. + * @param {Object} graph */ - 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"; + function parseStatement(graph) { + // parse subgraph + var subgraph = parseSubgraph(graph); + if (subgraph) { + // edge statements + parseEdge(graph, subgraph); - // TODO: be able to destroy the delete button - new Hammer(deleteButton).on("tap", function (event) { - me.parent.removeFromDataSet(me); - event.stopPropagation(); - event.preventDefault(); - }); + 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; + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; } - }; - /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private - */ - Item.prototype._updateContents = function (element) { - var content; - if (this.options.template) { - var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset - content = this.options.template(itemData); - } else { - content = this.data.content; + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); } + var id = token; // id can be a string or a number + getToken(); - if (content !== this.content) { - // only replace the content when changed - if (content instanceof Element) { - element.innerHTML = ""; - element.appendChild(content); - } else if (content != undefined) { - element.innerHTML = content; - } else { - if (!(this.data.type == "background" && this.data.content === undefined)) { - throw new Error("Property \"content\" missing in item " + this.id); - } + if (token == "=") { + // id statement + getToken(); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); } - - this.content = content; + graph[id] = token; + getToken(); + // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " + } else { + parseNodeStatement(graph, id); } - }; + } /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph */ - Item.prototype._updateTitle = function (element) { - if (this.data.title != null) { - element.title = this.data.title || ""; - } else { - element.removeAttribute("title"); + function parseSubgraph(graph) { + var subgraph = null; + + // optional subgraph keyword + if (token == "subgraph") { + subgraph = {}; + subgraph.type = "subgraph"; + getToken(); + + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } } - }; - /** - * Process dataAttributes timeline option and set as data- attributes on dom.content - * @param {Element} element HTML element to which the attributes will be attached - * @private - */ - Item.prototype._updateDataAttributes = function (element) { - if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { - var attributes = []; + // open angle bracket + if (token == "{") { + getToken(); - if (Array.isArray(this.options.dataAttributes)) { - attributes = this.options.dataAttributes; - } else if (this.options.dataAttributes == "all") { - attributes = Object.keys(this.data); - } else { - return; + if (!subgraph) { + subgraph = {}; } + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; - for (var i = 0; i < attributes.length; i++) { - var name = attributes[i]; - var value = this.data[name]; + // statements + parseStatements(subgraph); - if (value != null) { - element.setAttribute("data-" + name, value); - } else { - element.removeAttribute("data-" + name); - } + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); + } + getToken(); + + // remove temporary default options + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; + + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; } + graph.subgraphs.push(subgraph); } - }; + + return subgraph; + } /** - * Update custom styles of the element - * @param element - * @private + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. */ - Item.prototype._updateStyle = function (element) { - // remove old styles - if (this.style) { - util.removeCssText(element, this.style); - this.style = null; - } - - // append new styles - if (this.data.style) { - util.addCssText(element, this.data.style); - this.style = this.data.style; - } - }; + function parseAttributeStatement(graph) { + // attribute statements + if (token == "node") { + getToken(); - module.exports = Item; - // should be implemented by the item - // should be implemented by the item - // should be implemented by the item + // node attributes + graph.node = parseAttributeList(); + return "node"; + } else if (token == "edge") { + getToken(); -/***/ }, -/* 34 */ -/***/ function(module, exports, __webpack_require__) { + // edge attributes + graph.edge = parseAttributeList(); + return "edge"; + } else if (token == "graph") { + getToken(); - "use strict"; + // graph attributes + graph.graph = parseAttributeList(); + return "graph"; + } - var util = __webpack_require__(1); - var Group = __webpack_require__(30); + return null; + } /** - * @constructor BackgroundGroup - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet + * parse a node statement + * @param {Object} graph + * @param {String | Number} id */ - function BackgroundGroup(groupId, data, itemSet) { - Group.call(this, groupId, data, itemSet); + function parseNodeStatement(graph, id) { + // node statement + var node = { + id: id + }; + var attr = parseAttributeList(); + if (attr) { + node.attr = attr; + } + addNode(graph, node); - this.width = 0; - this.height = 0; - this.top = 0; - this.left = 0; + // edge statements + parseEdge(graph, id); } - BackgroundGroup.prototype = Object.create(Group.prototype); - /** - * Repaint this group - * @param {{start: number, end: number}} range - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @param {boolean} [restack=false] Force restacking of all items - * @return {boolean} Returns true if the group is resized + * Parse an edge or a series of edges + * @param {Object} graph + * @param {String | Number} from Id of the from node */ - BackgroundGroup.prototype.redraw = function (range, margin, restack) { - var resized = false; + function parseEdge(graph, from) { + while (token == "->" || token == "--") { + var to; + var type = token; + getToken(); - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + var subgraph = parseSubgraph(graph); + if (subgraph) { + to = subgraph; + } else { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier or subgraph expected"); + } + to = token; + addNode(graph, { + id: to + }); + getToken(); + } - // calculate actual size - this.width = this.dom.background.offsetWidth; + // parse edge attributes + var attr = parseAttributeList(); - // apply new height (just always zero for BackgroundGroup - this.dom.background.style.height = "0"; + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); - // update vertical position of items after they are re-stacked and the height of the group is calculated - for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { - var item = this.visibleItems[i]; - item.repositionY(margin); + from = to; } - - return resized; - }; + } /** - * Show this group: attach to the DOM + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr */ - BackgroundGroup.prototype.show = function () { - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } - }; - - module.exports = BackgroundGroup; + function parseAttributeList() { + var attr = null; -/***/ }, -/* 35 */ -/***/ function(module, exports, __webpack_require__) { + while (token == "[") { + getToken(); + attr = {}; + while (token !== "" && token != "]") { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute name expected"); + } + var name = token; - "use strict"; + getToken(); + if (token != "=") { + throw newSyntaxError("Equal sign = expected"); + } + getToken(); - var Item = __webpack_require__(33); - var util = __webpack_require__(1); + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute value expected"); + } + var value = token; + setValue(attr, name, value); // name can be a path - /** - * @constructor BoxItem - * @extends Item - * @param {Object} data Object containing parameters start - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe available options - */ - function BoxItem(data, conversion, options) { - this.props = { - dot: { - width: 0, - height: 0 - }, - line: { - width: 0, - height: 0 + getToken(); + if (token == ",") { + getToken(); + } } - }; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data); + if (token != "]") { + throw newSyntaxError("Bracket ] expected"); } + getToken(); } - Item.call(this, data, conversion, options); + return attr; } - BoxItem.prototype = new Item(null, null, null); + /** + * Create a syntax error with extra information on current token and index. + * @param {String} message + * @returns {SyntaxError} err + */ + function newSyntaxError(message) { + return new SyntaxError(message + ", got \"" + chop(token, 30) + "\" (char " + index + ")"); + } /** - * 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 + * Chop off text after a maximum length + * @param {String} text + * @param {Number} maxLength + * @returns {String} */ - BoxItem.prototype.isVisible = function (range) { - // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return this.data.start > range.start - interval && this.data.start < range.end + interval; - }; + function chop(text, maxLength) { + return text.length <= maxLength ? text : text.substr(0, 27) + "..."; + } /** - * Repaint the item + * Execute a function fn for each pair of elements in two arrays + * @param {Array | *} array1 + * @param {Array | *} array2 + * @param {function} fn */ - BoxItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + function forEach2(array1, array2, fn) { + if (Array.isArray(array1)) { + array1.forEach(function (elem1) { + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(elem1, elem2); + }); + } else { + fn(elem1, array2); + } + }); + } else { + if (Array.isArray(array2)) { + array2.forEach(function (elem2) { + fn(array1, elem2); + }); + } else { + fn(array1, array2); + } + } + } - // create main box - dom.box = document.createElement("DIV"); + /** + * Convert a string containing a graph in DOT language into a map containing + * with nodes and edges in the format of graph. + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graphData + */ + function DOTToGraph(data) { + // parse the DOT file + var dotData = parseDOT(data); + var graphData = { + nodes: [], + edges: [], + options: {} + }; - // contents box (inside the background box). used for making margins - dom.content = document.createElement("DIV"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); + // copy the nodes + if (dotData.nodes) { + dotData.nodes.forEach(function (dotNode) { + var graphNode = { + id: dotNode.id, + label: String(dotNode.label || dotNode.id) + }; + merge(graphNode, dotNode.attr); + if (graphNode.image) { + graphNode.shape = "image"; + } + graphData.nodes.push(graphNode); + }); + } + + // copy the edges + if (dotData.edges) { + /** + * Convert an edge in DOT format to an edge with VisGraph format + * @param {Object} dotEdge + * @returns {Object} graphEdge + */ + var convertEdge = function (dotEdge) { + var graphEdge = { + from: dotEdge.from, + to: dotEdge.to + }; + merge(graphEdge, dotEdge.attr); + graphEdge.style = dotEdge.type == "->" ? "arrow" : "line"; + return graphEdge; + }; + + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; + } else { + from = { + id: dotEdge.from + }; + } - // line to axis - dom.line = document.createElement("DIV"); - dom.line.className = "line"; + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } else { + to = { + id: dotEdge.to + }; + } - // dot on axis - dom.dot = document.createElement("DIV"); - dom.dot.className = "dot"; + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } - // attach this item as attribute - dom.box["timeline-item"] = this; + forEach2(from, to, function (from, to) { + var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); - this.dirty = true; + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); } - // append DOM to parent DOM - if (!this.parent) { - throw new Error("Cannot redraw item: no parent attached"); - } - if (!dom.box.parentNode) { - var foreground = this.parent.dom.foreground; - if (!foreground) throw new Error("Cannot redraw item: parent has no foreground container element"); - foreground.appendChild(dom.box); - } - if (!dom.line.parentNode) { - var background = this.parent.dom.background; - if (!background) throw new Error("Cannot redraw item: parent has no background container element"); - background.appendChild(dom.line); - } - if (!dom.dot.parentNode) { - var axis = this.parent.dom.axis; - if (!background) throw new Error("Cannot redraw item: parent has no axis container element"); - axis.appendChild(dom.dot); + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; } - this.displayed = true; - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); + return graphData; + } - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = "item box" + className; - dom.line.className = "item line" + className; - dom.dot.className = "item dot" + className; + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; - // recalculate size - this.props.dot.height = dom.dot.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.line.width = dom.line.offsetWidth; - this.width = dom.box.offsetWidth; - this.height = dom.box.offsetHeight; +/***/ }, +/* 39 */ +/***/ function(module, exports, __webpack_require__) { - this.dirty = false; - } + "use strict"; - this._repaintDeleteButton(dom.box); - }; + function parseGephi(gephiJSON, options) { + var edges = []; + var nodes = []; + this.options = { + edges: { + inheritColor: true + }, + nodes: { + allowedToMove: false, + parseColor: false + } + }; - /** - * Show the item in the DOM (when not already displayed). The items DOM will - * be created when needed. - */ - BoxItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); + if (options !== undefined) { + this.options.nodes.allowedToMove = options.allowedToMove | false; + this.options.nodes.parseColor = options.parseColor | false; + this.options.edges.inheritColor = options.inheritColor | true; } - }; - - /** - * Hide the item from the DOM (when visible) - */ - BoxItem.prototype.hide = function () { - if (this.displayed) { - var dom = this.dom; - if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box); - if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line); - if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot); - - this.top = null; - this.left = null; - - this.displayed = false; + var gEdges = gephiJSON.edges; + var gNodes = gephiJSON.nodes; + for (var i = 0; i < gEdges.length; i++) { + var edge = {}; + var gEdge = gEdges[i]; + edge.id = gEdge.id; + edge.from = gEdge.source; + edge.to = gEdge.target; + edge.attributes = gEdge.attributes; + // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; + // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; + edge.color = gEdge.color; + edge.inheritColor = edge.color !== undefined ? false : this.options.inheritColor; + edges.push(edge); } - }; - - /** - * Reposition the item horizontally - * @Override - */ - BoxItem.prototype.repositionX = function () { - var start = this.conversion.toScreen(this.data.start); - var align = this.options.align; - var left; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; - // calculate left position of the box - if (align == "right") { - this.left = start - this.width; - } else if (align == "left") { - this.left = start; - } else { - // default or 'center' - this.left = start - this.width / 2; + for (var i = 0; i < gNodes.length; i++) { + var node = {}; + var gNode = gNodes[i]; + node.id = gNode.id; + node.attributes = gNode.attributes; + node.x = gNode.x; + node.y = gNode.y; + node.label = gNode.label; + if (this.options.nodes.parseColor == true) { + node.color = gNode.color; + } else { + node.color = gNode.color !== undefined ? { background: gNode.color, border: gNode.color } : undefined; + } + node.radius = gNode.size; + node.allowedToMoveX = this.options.nodes.allowedToMove; + node.allowedToMoveY = this.options.nodes.allowedToMove; + nodes.push(node); } - // reposition box - box.style.left = this.left + "px"; - - // reposition line - line.style.left = start - this.props.line.width / 2 + "px"; + return { nodes: nodes, edges: edges }; + } - // reposition dot - dot.style.left = start - this.props.dot.width / 2 + "px"; - }; + exports.parseGephi = parseGephi; - /** - * Reposition the item vertically - * @Override - */ - BoxItem.prototype.repositionY = function () { - var orientation = this.options.orientation; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; +/***/ }, +/* 40 */ +/***/ function(module, exports, __webpack_require__) { - if (orientation == "top") { - box.style.top = (this.top || 0) + "px"; + "use strict"; - 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; + // 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__(63); - box.style.top = (this.parent.height - this.top - this.height || 0) + "px"; - line.style.top = itemSetHeight - lineHeight + "px"; - line.style.bottom = "0"; - } +/***/ }, +/* 41 */ +/***/ function(module, exports, __webpack_require__) { - dot.style.top = -this.props.dot.height / 2 + "px"; - }; + "use strict"; - module.exports = BoxItem; + // Only load hammer.js when in a browser environment + // (loading hammer.js in a node.js environment gives errors) + if (typeof window !== "undefined") { + var propagating = __webpack_require__(65); + var Hammer = window.Hammer || __webpack_require__(64); + module.exports = propagating(Hammer); + } else { + module.exports = function () { + throw Error("hammer.js is only available in a browser, not in node.js."); + }; + } /***/ }, -/* 36 */ +/* 42 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Item = __webpack_require__(33); + var Emitter = __webpack_require__(62); + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(43); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var Range = __webpack_require__(17); + var ItemSet = __webpack_require__(32); + var Activator = __webpack_require__(49); + var DateUtil = __webpack_require__(15); + var CustomTime = __webpack_require__(27); /** - * @constructor PointItem - * @extends Item - * @param {Object} data Object containing parameters start - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe available options + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Core.setOptions for the available options. + * @constructor */ - function PointItem(data, conversion, options) { - this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, - content: { - height: 0, - marginLeft: 0 - } - }; - - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data); - } - } - - Item.call(this, data, conversion, options); - } + function Core() {} - PointItem.prototype = new Item(null, null, null); + // turn Core into an event emitter + Emitter(Core.prototype); /** - * 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 + * 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 */ - PointItem.prototype.isVisible = function (range) { - // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return this.data.start > range.start - interval && this.data.start < range.end + interval; - }; + Core.prototype._create = function (container) { + this.dom = {}; - /** - * Repaint the item - */ - PointItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + 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"); - // background box - dom.point = document.createElement("div"); - // className is updated in redraw() + this.dom.root.className = "vis timeline root"; + 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"; - // contents box, right from the dot - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.point.appendChild(dom.content); + 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); - // dot at start - dom.dot = document.createElement("div"); - dom.point.appendChild(dom.dot); + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); - // attach this item as attribute - dom.point["timeline-item"] = this; + 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.dirty = true; - } + this.on("rangechange", this.redraw.bind(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 item: parent has no foreground container element"); + var me = this; + this.on("change", function (properties) { + if (properties && properties.queue == true) { + // redraw once on next tick + if (!me._redrawTimer) { + me._redrawTimer = setTimeout(function () { + me._redrawTimer = null; + me._redraw(); + }, 0); + } + } else { + // redraw immediately + me._redraw(); + } + }); + + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = new Hammer(this.dom.root, { touchAction: "pan-y" }); + this.hammer.get("pinch").set({ enable: true }); + this.listeners = {}; + + var events = ["tap", "doubletap", "press", "pinch", "pan", "panstart", "panmove", "panend" + // TODO: cleanup + //'touch', 'pinch', + //'tap', 'doubletap', 'hold', + //'dragstart', 'drag', 'dragend', + //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (type) { + var listener = function (event) { + if (me.isActive()) { + me.emit(type, event); + } + }; + me.hammer.on(type, listener); + me.listeners[type] = listener; + }); + + // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) + hammerUtil.onTouch(this.hammer, (function (event) { + me.emit("touch", event); + }).bind(this)); + + function onMouseWheel(event) { + if (me.isActive()) { + me.emit("mousewheel", event); } - foreground.appendChild(dom.point); } - this.displayed = true; + this.dom.root.addEventListener("mousewheel", onMouseWheel); + this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel); - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.point); - this._updateDataAttributes(this.dom.point); - this._updateStyle(this.dom.point); + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.point.className = "item point" + className; - dom.dot.className = "item dot" + className; + this.redrawCount = 0; - // recalculate size - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; + // attach the root panel to the provided container + if (!container) throw new Error("No container provided"); + container.appendChild(this.dom.root); + }; - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + "px"; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + /** + * 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 + */ + Core.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ["width", "height", "minHeight", "maxHeight", "autoResize", "start", "end", "orientation", "clickToUse", "dataAttributes", "hiddenDates"]; + util.selectiveExtend(fields, this.options, options); + + if ("hiddenDates" in this.options) { + DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); + } - dom.dot.style.top = (this.height - this.props.dot.height) / 2 + "px"; - dom.dot.style.left = this.props.dot.width / 2 + "px"; + if ("clickToUse" in options) { + if (options.clickToUse) { + if (!this.activator) { + this.activator = new Activator(this.dom.root); + } + } else { + if (this.activator) { + this.activator.destroy(); + delete this.activator; + } + } + } - this.dirty = false; + // enable/disable autoResize + this._initAutoResize(); } - this._repaintDeleteButton(dom.point); - }; + // propagate options to all components + this.components.forEach(function (component) { + return component.setOptions(options); + }); - /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - */ - PointItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); + // 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(); }; /** - * Hide the item from the DOM (when visible) + * Returns true when the Timeline is active. + * @returns {boolean} */ - PointItem.prototype.hide = function () { - if (this.displayed) { - if (this.dom.point.parentNode) { - this.dom.point.parentNode.removeChild(this.dom.point); - } - - this.top = null; - this.left = null; - - this.displayed = false; - } + Core.prototype.isActive = function () { + return !this.activator || this.activator.active; }; /** - * Reposition the item horizontally - * @Override + * Destroy the Core, clean up all DOM elements and event listeners. */ - PointItem.prototype.repositionX = function () { - var start = this.conversion.toScreen(this.data.start); + Core.prototype.destroy = function () { + // unbind datasets + this.clear(); - this.left = start - this.props.dot.width; + // remove all event listeners + this.off(); - // reposition point - this.dom.point.style.left = this.left + "px"; - }; + // stop checking for changed size + this._stopAutoResize(); - /** - * Reposition the item vertically - * @Override - */ - PointItem.prototype.repositionY = function () { - var orientation = this.options.orientation, - point = this.dom.point; + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); + } + this.dom = null; - if (orientation == "top") { - point.style.top = this.top + "px"; - } else { - point.style.top = this.parent.height - this.top - this.height + "px"; + // remove Activator + if (this.activator) { + this.activator.destroy(); + delete this.activator; } - }; - module.exports = PointItem; + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; + } + } + this.listeners = null; + this.hammer = null; -/***/ }, -/* 37 */ -/***/ function(module, exports, __webpack_require__) { + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + return component.destroy(); + }); - "use strict"; + this.body = null; + }; - var Hammer = __webpack_require__(19); - var Item = __webpack_require__(33); - var BackgroundGroup = __webpack_require__(34); - var RangeItem = __webpack_require__(32); /** - * @constructor BackgroundItem - * @extends Item - * @param {Object} data Object containing parameters start, end - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe options + * Set a custom time bar + * @param {Date} time + * @param {int} id */ - // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation - function BackgroundItem(data, conversion, options) { - this.props = { - content: { - width: 0 - } - }; - this.overflow = false; // if contents can overflow (css styling), this flag is set to true - - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data.id); - } - if (data.end == undefined) { - throw new Error("Property \"end\" missing in item " + data.id); - } + Core.prototype.setCustomTime = function (time, id) { + if (!this.customTime) { + throw new Error("Cannot get custom time: Custom time bar is not enabled"); } - Item.call(this, data, conversion, options); - - this.emptyContent = false; - } - - BackgroundItem.prototype = new Item(null, null, null); - - BackgroundItem.prototype.baseClassName = "item background"; - BackgroundItem.prototype.stack = false; + var barId = id || 0; - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - BackgroundItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; + this.components.forEach(function (element, index, components) { + if (element instanceof CustomTime && element.options.id === barId) { + element.setCustomTime(time); + } + }); }; /** - * Repaint the item + * Retrieve the current custom time. + * @return {Date} customTime + * @param {int} id */ - BackgroundItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + Core.prototype.getCustomTime = function (id) { + if (!this.customTime) { + throw new Error("Cannot get custom time: Custom time bar is not enabled"); + } - // background box - dom.box = document.createElement("div"); - // className is updated in redraw() + var barId = id || 0, + customTime = this.customTime.getCustomTime(); - // contents box - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); + this.components.forEach(function (element, index, components) { + if (element instanceof CustomTime && element.options.id === barId) { + customTime = element.getCustomTime(); + } + }); - // Note: we do NOT attach this item as attribute to the DOM, - // such that background items cannot be selected - //dom.box['timeline-item'] = this; + return customTime; + }; - this.dirty = true; + /** + * Add custom vertical bar + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. Time point where the new bar should be placed + * @param {Number | String} ID of the new bar + * @return {Number | String} ID of the new bar + */ + Core.prototype.addCustomTime = function (time, id) { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); } - // append DOM to parent DOM - if (!this.parent) { - throw new Error("Cannot redraw item: no parent attached"); - } - if (!dom.box.parentNode) { - var background = this.parent.dom.background; - if (!background) { - throw new Error("Cannot redraw item: parent has no background container element"); - } - background.appendChild(dom.box); + if (time === undefined) { + throw new Error("Time parameter for the custom bar must be provided"); } - this.displayed = true; - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.content); - this._updateDataAttributes(this.dom.content); - this._updateStyle(this.dom.box); + var ts = util.convert(time, "Date").valueOf(), + numIds, + customTime, + customBarId; - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = this.baseClassName + className; + // All bar IDs are kept in 1 array, mixed types + // Bar with ID 0 is the default bar. + if (!this.customBarIds || this.customBarIds.constructor !== Array) { + this.customBarIds = [0]; + } - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; + // If the ID is not provided, generate one, otherwise just use it + if (id === undefined) { + numIds = this.customBarIds.filter(function (element) { + return util.isNumber(element); + }); - // recalculate size - this.props.content.width = this.dom.content.offsetWidth; - this.height = 0; // set height zero, so this item will be ignored when stacking items + customBarId = numIds.length > 0 ? Math.max.apply(null, numIds) + 1 : 1; + } else { + // Check for duplicates + this.customBarIds.forEach(function (element) { + if (element === id) { + throw new Error("Custom time ID already exists"); + } + }); - this.dirty = false; + customBarId = id; } - }; - /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - */ - BackgroundItem.prototype.show = RangeItem.prototype.show; + this.customBarIds.push(customBarId); - /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed - */ - BackgroundItem.prototype.hide = RangeItem.prototype.hide; + customTime = new CustomTime(this.body, { + showCustomTime: true, + time: ts, + id: customBarId + }); - /** - * Reposition the item horizontally - * @Override - */ - BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; + this.components.push(customTime); + this.redraw(); + + return customBarId; + }; /** - * Reposition the item vertically - * @Override + * Remove previously added custom bar + * @param {int} id ID of the custom bar to be removed + * @return {boolean} True if the bar exists and is removed, false otherwise */ - BackgroundItem.prototype.repositionY = function (margin) { - var onTop = this.options.orientation === "top"; - this.dom.content.style.top = onTop ? "" : "0"; - this.dom.content.style.bottom = onTop ? "0" : ""; - var height; - - // special positioning for subgroups - if (this.data.subgroup !== undefined) { - var itemSubgroup = this.data.subgroup; - var subgroups = this.parent.subgroups; - var subgroupIndex = subgroups[itemSubgroup].index; - // if the orientation is top, we need to take the difference in height into account. - if (onTop == true) { - // the first subgroup will have to account for the distance from the top to the first item. - height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - height += subgroupIndex == 0 ? margin.axis - 0.5 * margin.item.vertical : 0; - var newTop = this.parent.top; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index < subgroupIndex) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } - } + Core.prototype.removeCustomTime = function (id) { + var me = this; - // the others will have to be offset downwards with this same distance. - newTop += subgroupIndex != 0 ? margin.axis - 0.5 * margin.item.vertical : 0; - this.dom.box.style.top = newTop + "px"; - this.dom.box.style.bottom = ""; - } - // and when the orientation is bottom: - else { - var newTop = this.parent.top; - for (var subgroup in subgroups) { - if (subgroups.hasOwnProperty(subgroup)) { - if (subgroups[subgroup].visible == true && subgroups[subgroup].index > subgroupIndex) { - newTop += subgroups[subgroup].height + margin.item.vertical; - } - } + this.components.forEach(function (bar, index, components) { + if (bar instanceof CustomTime && bar.options.id === id) { + // Only the lines added by the user will be removed + if (bar.options.id !== 0) { + me.customBarIds.splice(me.customBarIds.indexOf(id), 1); + components.splice(index, 1); + bar.destroy(); } - height = this.parent.subgroups[itemSubgroup].height + margin.item.vertical; - this.dom.box.style.top = newTop + "px"; - this.dom.box.style.bottom = ""; - } - } - // and in the case of no subgroups: - else { - // we want backgrounds with groups to only show in groups. - if (this.parent instanceof BackgroundGroup) { - // if the item is not in a group: - height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); - this.dom.box.style.top = onTop ? "0" : ""; - this.dom.box.style.bottom = onTop ? "" : "0"; - } else { - height = this.parent.height; - // same alignment for items when orientation is top or bottom - this.dom.box.style.top = this.parent.top + "px"; - this.dom.box.style.bottom = ""; } - } - this.dom.box.style.height = height + "px"; + }); }; - module.exports = BackgroundItem; - -/***/ }, -/* 38 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var keycharm = __webpack_require__(39); - var Emitter = __webpack_require__(11); - var Hammer = __webpack_require__(19); - var util = __webpack_require__(1); /** - * Turn an element into an clickToUse element. - * When not active, the element has a transparent overlay. When the overlay is - * clicked, the mode is changed to active. - * When active, the element is displayed with a blue border around it, and - * the interactive contents of the element can be used. When clicked outside - * the element, the elements mode is changed to inactive. - * @param {Element} container - * @constructor + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - function Activator(container) { - this.active = false; + Core.prototype.getVisibleItems = function () { + return this.itemSet && this.itemSet.getVisibleItems() || []; + }; - this.dom = { - container: container - }; - this.dom.overlay = document.createElement("div"); - this.dom.overlay.className = "overlay"; - this.dom.container.appendChild(this.dom.overlay); + /** + * 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); + } - this.hammer = Hammer(this.dom.overlay, { prevent_default: false }); - this.hammer.on("tap", this._onTapOverlay.bind(this)); + // clear groups + if (!what || what.groups) { + this.setGroups(null); + } - // block all touch events (except tap) - var me = this; - var events = ["touch", "pinch", "doubletap", "hold", "dragstart", "drag", "dragend", "mousewheel", "DOMMouseScroll" // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (event) { - me.hammer.on(event, function (event) { - event.stopPropagation(); + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + return component.setOptions(component.defaultOptions); }); - }); - - // attach a tap event to the window, in order to deactivate when clicking outside the timeline - this.windowHammer = Hammer(window, { prevent_default: false }); - this.windowHammer.on("tap", function (event) { - // deactivate when clicked outside the container - if (!_hasParent(event.target, container)) { - me.deactivate(); - } - }); - if (this.keycharm !== undefined) { - this.keycharm.destroy(); + this.setOptions(this.defaultOptions); // this will also do a redraw } - this.keycharm = keycharm(); + }; - // keycharm listener only bounded when active) - this.escListener = this.deactivate.bind(this); - } + /** + * Set Core window such that it fits all items + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. + */ + Core.prototype.fit = function (options) { + var range = this._getDataRange(); - // turn into an event emitter - Emitter(Activator.prototype); + // skip range set if there is no start and end date + if (range.start === null && range.end === null) { + return; + } - // The currently active activator - Activator.current = null; + var animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(range.start, range.end, animate); + }; /** - * Destroy the activator. Cleans up all created DOM and event listeners + * Calculate the data range of the items and applies a 5% window around it. + * @returns {{start: Date | null, end: Date | null}} + * @protected */ - Activator.prototype.destroy = function () { - this.deactivate(); + Core.prototype._getDataRange = function () { + // apply the data range as range + var dataRange = this.getItemRange(); - // remove dom - this.dom.overlay.parentNode.removeChild(this.dom.overlay); + // 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); + } - // cleanup hammer instances - this.hammer = null; - this.windowHammer = null; - // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + return { + start: start, + end: end + }; + }; + + /** + * Set the visible window. Both parameters are optional, you can change only + * start or only end. Syntax: + * + * TimeLine.setWindow(start, end) + * TimeLine.setWindow(start, end, options) + * TimeLine.setWindow(range) + * + * Where start and end can be a Date, number, or string, and range is an + * object with properties start and end. + * + * @param {Date | Number | String | Object} [start] Start date of visible window + * @param {Date | Number | String} [end] End date of visible window + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. + */ + Core.prototype.setWindow = function (start, end, options) { + var animate; + if (arguments.length == 1) { + var range = arguments[0]; + animate = range.animate !== undefined ? range.animate : true; + this.range.setRange(range.start, range.end, animate); + } else { + animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(start, end, animate); + } }; /** - * Activate the element - * Overlay is hidden, element is decorated with a blue shadow border + * Move the window such that given time is centered on screen. + * @param {Date | Number | String} time + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. */ - Activator.prototype.activate = function () { - // we allow only one active activator at a time - if (Activator.current) { - Activator.current.deactivate(); - } - Activator.current = this; - - this.active = true; - this.dom.overlay.style.display = "none"; - util.addClassName(this.dom.container, "vis-active"); + Core.prototype.moveTo = function (time, options) { + var interval = this.range.end - this.range.start; + var t = util.convert(time, "Date").valueOf(); - this.emit("change"); - this.emit("activate"); + var start = t - interval / 2; + var end = t + interval / 2; + var animate = options && options.animate !== undefined ? options.animate : true; - // ugly hack: bind ESC after emitting the events, as the Network rebinds all - // keyboard events on a 'change' event - this.keycharm.bind("esc", this.escListener); + this.range.setRange(start, end, animate); }; /** - * Deactivate the element - * Overlay is displayed on top of the element + * Get the visible window + * @return {{start: Date, end: Date}} Visible range */ - Activator.prototype.deactivate = function () { - this.active = false; - this.dom.overlay.style.display = ""; - util.removeClassName(this.dom.container, "vis-active"); - this.keycharm.unbind("esc", this.escListener); - - this.emit("change"); - this.emit("deactivate"); + Core.prototype.getWindow = function () { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; }; /** - * Handle a tap event: activate the container - * @param event - * @private + * Force a redraw. Can be overridden by implementations of Core */ - Activator.prototype._onTapOverlay = function (event) { - // activate the container - this.activate(); - event.stopPropagation(); + Core.prototype.redraw = function () { + this._redraw(); }; /** - * Test whether the element has the requested parent element somewhere in - * its chain of parent nodes. - * @param {HTMLElement} element - * @param {HTMLElement} parent - * @returns {boolean} Returns true when the parent is found somewhere in the - * chain of parent nodes. - * @private + * Redraw for internal use. Redraws all components. See also the public + * method redraw. + * @protected */ - function _hasParent(element, parent) { - while (element) { - if (element === parent) { - return true; - } - element = element.parentNode; - } - return false; - } - - module.exports = Activator; + Core.prototype._redraw = function () { + var resized = false; + var options = this.options; + var props = this.props; + var dom = this.dom; -/***/ }, -/* 39 */ -/***/ function(module, exports, __webpack_require__) { + if (!dom) return; // when destroyed - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; - /** - * Created by Alex on 11/6/2014. - */ + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 - // if the module has no dependencies, the above pattern can be simplified to - (function (root, factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); + // update class names + if (options.orientation == "top") { + util.addClassName(dom.root, "top"); + util.removeClassName(dom.root, "bottom"); } else { - // Browser globals (root is window) - root.keycharm = factory(); + util.removeClassName(dom.root, "top"); + util.addClassName(dom.root, "bottom"); } - }(this, function () { - - function keycharm(options) { - var preventDefault = options && options.preventDefault || false; - - var container = options && options.container || window; - var _exportFunctions = {}; - var _bound = {keydown:{}, keyup:{}}; - var _keys = {}; - var i; - - // a - z - for (i = 97; i <= 122; i++) {_keys[String.fromCharCode(i)] = {code:65 + (i - 97), shift: false};} - // A - Z - for (i = 65; i <= 90; i++) {_keys[String.fromCharCode(i)] = {code:i, shift: true};} - // 0 - 9 - for (i = 0; i <= 9; i++) {_keys['' + i] = {code:48 + i, shift: false};} - // F1 - F12 - for (i = 1; i <= 12; i++) {_keys['F' + i] = {code:111 + i, shift: false};} - // num0 - num9 - for (i = 0; i <= 9; i++) {_keys['num' + i] = {code:96 + i, shift: false};} - - // numpad misc - _keys['num*'] = {code:106, shift: false}; - _keys['num+'] = {code:107, shift: false}; - _keys['num-'] = {code:109, shift: false}; - _keys['num/'] = {code:111, shift: false}; - _keys['num.'] = {code:110, shift: false}; - // arrows - _keys['left'] = {code:37, shift: false}; - _keys['up'] = {code:38, shift: false}; - _keys['right'] = {code:39, shift: false}; - _keys['down'] = {code:40, shift: false}; - // extra keys - _keys['space'] = {code:32, shift: false}; - _keys['enter'] = {code:13, shift: false}; - _keys['shift'] = {code:16, shift: undefined}; - _keys['esc'] = {code:27, shift: false}; - _keys['backspace'] = {code:8, shift: false}; - _keys['tab'] = {code:9, shift: false}; - _keys['ctrl'] = {code:17, shift: false}; - _keys['alt'] = {code:18, shift: false}; - _keys['delete'] = {code:46, shift: false}; - _keys['pageup'] = {code:33, shift: false}; - _keys['pagedown'] = {code:34, shift: false}; - // symbols - _keys['='] = {code:187, shift: false}; - _keys['-'] = {code:189, shift: false}; - _keys[']'] = {code:221, shift: false}; - _keys['['] = {code:219, shift: false}; - + // update root width and height options + dom.root.style.maxHeight = util.option.asSize(options.maxHeight, ""); + dom.root.style.minHeight = util.option.asSize(options.minHeight, ""); + dom.root.style.width = util.option.asSize(options.width, ""); - var down = function(event) {handleEvent(event,'keydown');}; - var up = function(event) {handleEvent(event,'keyup');}; + // 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; - // handle the actualy bound key with the event - var handleEvent = function(event,type) { - if (_bound[type][event.keyCode] !== undefined) { - var bound = _bound[type][event.keyCode]; - for (var i = 0; i < bound.length; i++) { - if (bound[i].shift === undefined) { - bound[i].fn(event); - } - else if (bound[i].shift == true && event.shiftKey == true) { - bound[i].fn(event); - } - else if (bound[i].shift == false && event.shiftKey == false) { - bound[i].fn(event); - } - } + // workaround for a bug in IE: the clientWidth of an element with + // a height:0px and overflow:hidden is not calculated and always has value 0 + if (dom.centerContainer.clientHeight === 0) { + props.border.left = props.border.top; + props.border.right = props.border.left; + } + if (dom.root.clientHeight === 0) { + borderRootWidth = borderRootHeight; + } - if (preventDefault == true) { - event.preventDefault(); - } - } - }; + // 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; - // bind a key to a callback - _exportFunctions.bind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; - } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); - } - if (_bound[type][_keys[key].code] === undefined) { - _bound[type][_keys[key].code] = []; - } - _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); - }; + // 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"); - // bind all keys to a call back (demo purposes) - _exportFunctions.bindAll = function(callback, type) { - if (type === undefined) { - type = 'keydown'; - } - for (var key in _keys) { - if (_keys.hasOwnProperty(key)) { - _exportFunctions.bind(key,callback,type); - } - } - }; + // 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; - // get the key label from an event - _exportFunctions.getKey = function(event) { - for (var key in _keys) { - if (_keys.hasOwnProperty(key)) { - if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { - return key; - } - else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { - return key; - } - else if (event.keyCode == _keys[key].code && key == 'shift') { - return key; - } - } - } - return "unknown key, currently not supported"; - }; + // 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; - // unbind either a specific callback from a key or all of them (by leaving callback undefined) - _exportFunctions.unbind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; - } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); - } - if (callback !== undefined) { - var newBindings = []; - var bound = _bound[type][_keys[key].code]; - if (bound !== undefined) { - for (var i = 0; i < bound.length; i++) { - if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { - newBindings.push(_bound[type][_keys[key].code][i]); - } - } - } - _bound[type][_keys[key].code] = newBindings; - } - else { - _bound[type][_keys[key].code] = []; - } - }; + // 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"; - // reset all bound variables. - _exportFunctions.reset = function() { - _bound = {keydown:{}, keyup:{}}; - }; + 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"; - // unbind all listeners and reset all variables. - _exportFunctions.destroy = function() { - _bound = {keydown:{}, keyup:{}}; - container.removeEventListener('keydown', down, true); - container.removeEventListener('keyup', up, true); - }; + // reposition the panels + dom.background.style.left = "0"; + dom.background.style.top = "0"; + dom.backgroundVertical.style.left = props.left.width + props.border.left + "px"; + dom.backgroundVertical.style.top = "0"; + dom.backgroundHorizontal.style.left = "0"; + dom.backgroundHorizontal.style.top = props.top.height + "px"; + dom.centerContainer.style.left = props.left.width + "px"; + dom.centerContainer.style.top = props.top.height + "px"; + dom.leftContainer.style.left = "0"; + dom.leftContainer.style.top = props.top.height + "px"; + dom.rightContainer.style.left = props.left.width + props.center.width + "px"; + dom.rightContainer.style.top = props.top.height + "px"; + dom.top.style.left = props.left.width + "px"; + dom.top.style.top = "0"; + dom.bottom.style.left = props.left.width + "px"; + dom.bottom.style.top = props.top.height + props.centerContainer.height + "px"; - // create listeners. - container.addEventListener('keydown',down,true); - container.addEventListener('keyup',up,true); + // 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(); - // return the public functions. - return _exportFunctions; + // 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"; - return keycharm; - })); - - - + // 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; -/***/ }, -/* 40 */ -/***/ function(module, exports, __webpack_require__) { + // redraw all components + this.components.forEach(function (component) { + resized = component.redraw() || resized; + }); + if (resized) { + // keep repainting until all sizes are settled + var MAX_REDRAWS = 3; // maximum number of consecutive redraws + if (this.redrawCount < MAX_REDRAWS) { + this.redrawCount++; + this._redraw(); + } else { + console.log("WARNING: infinite loop in redraw?"); + } + this.redrawCount = 0; + } - "use strict"; + this.emit("finishedRedraw"); + }; - var Hammer = __webpack_require__(19); - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var moment = __webpack_require__(2); - var locales = __webpack_require__(41); + // TODO: deprecated since version 1.1.0, remove some day + Core.prototype.repaint = function () { + throw new Error("Function repaint is deprecated. Use redraw instead."); + }; /** - * A custom time bar - * @param {{range: Range, dom: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCustomTime] - * @constructor CustomTime - * @extends Component + * Set a current time. This can be used for example to ensure that a client's + * time is synchronized with a shared server time. + * Only applicable when option `showCurrentTime` is true. + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. */ - - function CustomTime(body, options) { - this.body = body; - - // default options - this.defaultOptions = { - showCustomTime: false, - locales: locales, - locale: "en", - id: 0 - }; - this.options = util.extend({}, this.defaultOptions); - - if (options && options.time) { - this.customTime = options.time; - } else { - this.customTime = new Date(); + Core.prototype.setCurrentTime = function (time) { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); } - this.eventParams = {}; // stores state parameters while dragging the bar + this.currentTime.setCurrentTime(time); + }; - // create the DOM - this._create(); + /** + * Get the current time. + * Only applicable when option `showCurrentTime` is true. + * @return {Date} Returns the current time. + */ + Core.prototype.getCurrentTime = function () { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); + } - this.setOptions(options); - } + return this.currentTime.getCurrentTime(); + }; - CustomTime.prototype = new Component(); + /** + * 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 + */ + // TODO: move this function to Range + Core.prototype._toTime = function (x) { + return DateUtil.toTime(this, x, this.props.center.width); + }; /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCustomTime] + * 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 */ - CustomTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(["showCustomTime", "locale", "locales", "id"], this.options, options); + // TODO: move this function to Range + Core.prototype._toGlobalTime = function (x) { + return DateUtil.toTime(this, x, this.props.root.width); + //var conversion = this.range.conversion(this.props.root.width); + //return new Date(x / conversion.scale + conversion.offset); + }; - // Triggered by addCustomTimeBar, redraw to add new bar - if (this.options.id) { - this.redraw(); - } - } + /** + * 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 + */ + // TODO: move this function to Range + Core.prototype._toScreen = function (time) { + return DateUtil.toScreen(this, time, this.props.center.width); }; + + /** - * Create the DOM for the custom time + * 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 */ - 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 = new Hammer(drag); - this.hammer.on("panstart", this._onDragStart.bind(this)); - this.hammer.on("panmove", this._onDrag.bind(this)); - this.hammer.on("panend", this._onDragEnd.bind(this)); - this.hammer.on("pan", function (event) { - event.preventDefault(); - }); + // TODO: move this function to Range + Core.prototype._toGlobalScreen = function (time) { + return DateUtil.toScreen(this, time, this.props.root.width); + //var conversion = this.range.conversion(this.props.root.width); + //return (time.valueOf() - conversion.offset) * conversion.scale; }; + /** - * Destroy the CustomTime bar + * Initialize watching when option autoResize is true + * @private */ - CustomTime.prototype.destroy = function () { - this.options.showCustomTime = false; - this.redraw(); // will remove the bar from the DOM - - this.hammer.enable(false); - this.hammer = null; - - this.body = null; + Core.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } else { + this._stopAutoResize(); + } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ - 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); - } + Core.prototype._startAutoResize = function () { + var me = this; - var x = this.body.util.toScreen(this.customTime); + this._stopAutoResize(); - 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._onResize = function () { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; + } - this.bar.style.left = x + "px"; - this.bar.title = title; - } else { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); + if (me.dom.root) { + // check whether the frame is resized + // Note: we compare offsetWidth here, not clientWidth. For some reason, + // IE does not restore the clientWidth from 0 to the actual width after + // changing the timeline's container display style from none to visible + if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { + me.props.lastWidth = me.dom.root.offsetWidth; + me.props.lastHeight = me.dom.root.offsetHeight; + + me.emit("change"); + } } - } + }; - return false; + // add event listener to window resize + util.addEventListener(window, "resize", this._onResize); + + this.watchTimer = setInterval(this._onResize, 1000); }; /** - * Set custom time. - * @param {Date | number | string} time + * Stop watching for a resize of the frame. + * @private */ - CustomTime.prototype.setCustomTime = function (time) { - this.customTime = util.convert(time, "Date"); - this.redraw(); + Core.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; + } + + // remove event listener on window.resize + util.removeEventListener(window, "resize", this._onResize); + this._onResize = null; }; /** - * Retrieve the current custom time. - * @return {Date} customTime + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop + * @private */ - CustomTime.prototype.getCustomTime = function () { - return new Date(this.customTime.valueOf()); + Core.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; }; /** - * Start moving horizontally - * @param {Event} event + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ - CustomTime.prototype._onDragStart = function (event) { - this.eventParams.dragging = true; - this.eventParams.customTime = this.customTime; + 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; + } + this.props.scrollTopMin = scrollTopMin; + } - event.stopPropagation(); - event.preventDefault(); + // 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; }; /** - * Perform moving operating. - * @param {Event} event + * Get the current scrollTop + * @returns {number} scrollTop * @private */ - CustomTime.prototype._onDrag = function (event) { - if (!this.eventParams.dragging) return; + Core.prototype._getScrollTop = function () { + return this.props.scrollTop; + }; - var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; - var time = this.body.util.toTime(x); + module.exports = Core; - this.setCustomTime(time); +/***/ }, +/* 43 */ +/***/ function(module, exports, __webpack_require__) { - // fire a timechange event - this.body.emitter.emit("timechange", { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + "use strict"; - event.stopPropagation(); - event.preventDefault(); + var Hammer = __webpack_require__(41); + + /** + * Register a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.onTouch = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFirst) { + callback(event); + } + }; + + hammer.on("hammer.input", callback.inputHandler); }; /** - * Stop moving operating. - * @param {Event} event - * @private + * Register a release event, taking place after a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - CustomTime.prototype._onDragEnd = function (event) { - if (!this.eventParams.dragging) return; + exports.onRelease = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFinal) { + callback(event); + } + }; - // fire a timechanged event - this.body.emitter.emit("timechanged", { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + return hammer.on("hammer.input", callback.inputHandler); + }; - event.stopPropagation(); - event.preventDefault(); + /** + * Unregister a touch event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.offTouch = function (hammer, callback) { + hammer.off("hammer.input", callback.inputHandler); }; - module.exports = CustomTime; + /** + * Unregister a release event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.offRelease = exports.offTouch; /***/ }, -/* 41 */ +/* 44 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -19657,5393 +17974,6033 @@ return /******/ (function(modules) { // webpackBootstrap exports.nl_BE = exports.nl; /***/ }, -/* 42 */ +/* 45 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var TimeStep = __webpack_require__(29); - var DateUtil = __webpack_require__(26); - var moment = __webpack_require__(2); - /** - * 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 + * Created by Alex on 11/11/2014. */ - function TimeAxis(body, options) { - this.dom = { - foreground: null, - lines: [], - majorTexts: [], - minorTexts: [], - redundant: { - lines: [], - majorTexts: [], - minorTexts: [] - } - }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 - }, - lineTop: 0 - }; - - this.defaultOptions = { - orientation: "bottom", // supported: 'top', 'bottom' - // TODO: implement timeaxis orientations 'left' and 'right' - showMinorLabels: true, - showMajorLabels: true, - format: null, - timeAxis: null - }; - this.options = util.extend({}, this.defaultOptions); - - this.body = body; - - // create the HTML DOM - this._create(); + var DOMutil = __webpack_require__(2); + var Points = __webpack_require__(47); - this.setOptions(options); + function Line(groupId, options) { + this.groupId = groupId; + this.options = options; } - TimeAxis.prototype = new Component(); - - /** - * 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", "hiddenDates", "format", "timeAxis"], 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); - } - } - } - }; - - /** - * 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"; - }; - - /** - * 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); + Line.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } - - this.body = null; + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; }; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - TimeAxis.prototype.redraw = function () { - var options = this.options; - var props = this.props; - var foreground = this.dom.foreground; - var background = this.dom.background; - - // determine the correct parent DOM element (depending on option orientation) - var parent = options.orientation == "top" ? this.body.dom.top : this.body.dom.bottom; - var parentChanged = foreground.parentNode !== parent; - - // calculate character width and height - this._calculateCharSize(); - - // TODO: recalculate sizes only needed when parent is resized or options is changed - var orientation = this.options.orientation, - showMinorLabels = this.options.showMinorLabels, - 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; - 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; - }; /** - * Repaint major and minor text labels and vertical grid lines - * @private + * draw a line graph + * + * @param dataset + * @param group */ - TimeAxis.prototype._repaintLabels = function () { - var orientation = this.options.orientation; - - // calculate range and step (step such that we have space for 7 characters per label) - var start = util.convert(this.body.range.start, "Number"); - var end = util.convert(this.body.range.end, "Number"); - var timeLabelsize = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf(); - var minimumStep = timeLabelsize - DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this.body.range, timeLabelsize); - minimumStep -= this.body.util.toTime(0).valueOf(); - - var step = new TimeStep(new Date(start), new Date(end), minimumStep, this.body.hiddenDates); - if (this.options.format) { - step.setFormat(this.options.format); - } - if (this.options.timeAxis) { - step.setScale(this.options.timeAxis); - } - this.step = step; - - // Move all DOM elements to a "redundant" list, where they - // can be picked for re-use, and clear the lists with lines and texts. - // At the end of the function _repaintLabels, left over elements will be cleaned up - var dom = this.dom; - dom.redundant.lines = dom.lines; - dom.redundant.majorTexts = dom.majorTexts; - dom.redundant.minorTexts = dom.minorTexts; - dom.lines = []; - dom.majorTexts = []; - dom.minorTexts = []; - - var cur; - var x = 0; - var isMajor; - var xPrev = 0; - var width = 0; - var prevLine; - var xFirstMajorLabel = undefined; - var max = 0; - var className; - - step.first(); - while (step.hasNext() && max < 1000) { - max++; - - cur = step.getCurrent(); - isMajor = step.isMajor(); - className = step.getClassName(); - - xPrev = x; - x = this.body.util.toScreen(cur); - width = x - xPrev; - if (prevLine) { - prevLine.style.width = width + "px"; - } - - if (this.options.showMinorLabels) { - this._repaintMinorText(x, step.getLabelMinor(), orientation, className); - } - - if (isMajor && this.options.showMajorLabels) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; - } - this._repaintMajorText(x, step.getLabelMajor(), orientation, className); + Line.prototype.draw = function (dataset, group, framework) { + if (dataset != null) { + if (dataset.length > 0) { + var path, d; + var svgHeight = Number(framework.svg.style.height.replace("px", "")); + path = DOMutil.getSVGElement("path", framework.svgElements, framework.svg); + path.setAttributeNS(null, "class", group.className); + if (group.style !== undefined) { + path.setAttributeNS(null, "style", group.style); } - prevLine = this._repaintMajorLine(x, orientation, className); - } else { - prevLine = this._repaintMinorLine(x, orientation, className); - } - - 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 + // construct path from dataset + if (group.options.catmullRom.enabled == true) { + d = Line._catmullRom(dataset, group); + } else { + d = Line._linear(dataset); + } - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText, orientation, className); - } - } + // append with points for fill and finalize the path + if (group.options.shaded.enabled == true) { + var fillPath = DOMutil.getSVGElement("path", framework.svgElements, framework.svg); + var dFill; + if (group.options.shaded.orientation == "top") { + dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0; + } else { + dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight; + } + fillPath.setAttributeNS(null, "class", group.className + " fill"); + if (group.options.shaded.style !== undefined) { + fillPath.setAttributeNS(null, "style", group.options.shaded.style); + } + fillPath.setAttributeNS(null, "d", dFill); + } + // copy properties to path for drawing. + path.setAttributeNS(null, "d", "M" + d); - // 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); + // draw points + if (group.options.drawPoints.enabled == true) { + Points.draw(dataset, group, framework); } } - }); + } }; + + /** - * Create a minor label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className + * 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 */ - TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); + Line._catmullRomUniform = function (data) { + // catmull rom + var p0, p1, p2, p3, bp1, bp2; + var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; + var normalization = 1 / 6; + var length = data.length; + for (var i = 0; i < length - 1; i++) { + p0 = i == 0 ? data[0] : data[i - 1]; + p1 = data[i]; + p2 = data[i + 1]; + p3 = i + 2 < length ? data[i + 2] : p2; - if (!label) { - // create new label - var content = document.createTextNode(""); - label = document.createElement("div"); - label.appendChild(content); - this.dom.foreground.appendChild(label); - } - this.dom.minorTexts.push(label); - label.childNodes[0].nodeValue = text; + // 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 - label.style.top = orientation == "top" ? this.props.majorLabelHeight + "px" : "0"; - label.style.left = x + "px"; - label.className = "text minor " + className; - //label.title = title; // TODO: this is a heavy operation + // 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; }; /** - * Create a Major label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className + * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm. + * By default, the centripetal parameterization is used because this gives the nicest results. + * These parameterizations are relatively heavy because the distance between 4 points have to be calculated. + * + * One optimization can be used to reuse distances since this is a sliding window approach. + * @param data + * @param group + * @returns {string} * @private */ - TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); + Line._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; - if (!label) { - // create label - var content = document.createTextNode(text); - label = document.createElement("div"); - label.appendChild(content); - this.dom.foreground.appendChild(label); - } - this.dom.majorTexts.push(label); + 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)); - label.childNodes[0].nodeValue = text; - label.className = "text major " + className; - //label.title = title; // TODO: this is a heavy operation + // Catmull-Rom to Cubic Bezier conversion matrix - label.style.top = orientation == "top" ? "0" : this.props.minorLabelHeight + "px"; - label.style.left = x + "px"; - }; + // A = 2d1^2a + 3d1^a * d2^a + d3^2a + // B = 2d3^2a + 3d3^a * d2^a + d2^2a - /** - * Create a minor line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className - * @return {Element} Returns the created line - * @private - */ - TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) { - // reuse redundant line - var line = this.dom.redundant.lines.shift(); - if (!line) { - // create vertical line - line = document.createElement("div"); - this.dom.background.appendChild(line); - } - this.dom.lines.push(line); + // [ 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 ] - 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"; + 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); - line.className = "grid vertical minor " + className; + 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; + } - return line; + bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, + y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + + bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, + y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; + + if (bp1.x == 0 && bp1.y == 0) { + bp1 = p1; + } + if (bp2.x == 0 && bp2.y == 0) { + bp2 = p2; + } + d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; + } + + return d; + } }; /** - * Create a Major line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className - * @return {Element} Returns the created line + * this generates the SVG path for a linear drawing between datapoints. + * @param data + * @returns {string} * @private */ - TimeAxis.prototype._repaintMajorLine = function (x, orientation, className) { - // reuse redundant line - var line = this.dom.redundant.lines.shift(); - if (!line) { - // create vertical line - line = document.createElement("div"); - this.dom.background.appendChild(line); + Line._linear = function (data) { + // linear + var d = ""; + for (var i = 0; i < data.length; i++) { + if (i == 0) { + d += data[i].x + "," + data[i].y; + } else { + d += " " + data[i].x + "," + data[i].y; + } } - this.dom.lines.push(line); + return d; + }; - 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"; + module.exports = Line; - line.className = "grid vertical major " + className; +/***/ }, +/* 46 */ +/***/ function(module, exports, __webpack_require__) { - return line; - }; + "use strict"; /** - * 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 + * Created by Alex on 11/11/2014. */ - 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"; - - 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; + var DOMutil = __webpack_require__(2); + var Points = __webpack_require__(47); - // 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 major measure"; - this.dom.measureCharMajor.style.position = "absolute"; + function Bargraph(groupId, options) { + this.groupId = groupId; + this.options = options; + } - this.dom.measureCharMajor.appendChild(document.createTextNode("0")); - this.dom.foreground.appendChild(this.dom.measureCharMajor); + Bargraph.prototype.getYRange = function (groupData) { + if (this.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; + } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + } else { + var barCombinedData = []; + for (var j = 0; j < groupData.length; j++) { + barCombinedData.push({ + x: groupData[j].x, + y: groupData[j].y, + groupId: this.groupId + }); + } + return barCombinedData; } - this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; - this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; }; - module.exports = TimeAxis; - -/***/ }, -/* 43 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var moment = __webpack_require__(2); - var locales = __webpack_require__(41); /** - * A current time bar - * @param {{range: Range, dom: Object, domProps: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCurrentTime] - * @constructor CurrentTime - * @extends Component + * draw a bar graph + * + * @param groupIds + * @param processedGroupData */ - function CurrentTime(body, options) { - this.body = body; + Bargraph.draw = function (groupIds, processedGroupData, framework) { + var combinedData = []; + var intersections = {}; + var coreDistance; + var key, drawData; + var group; + var i, j; + var barPoints = 0; - // default options - this.defaultOptions = { - showCurrentTime: true, + // combine all barchart data + for (i = 0; i < groupIds.length; i++) { + group = framework.groups[groupIds[i]]; + if (group.options.style == "bar") { + if (group.visible == true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] == true)) { + for (j = 0; j < processedGroupData[groupIds[i]].length; j++) { + combinedData.push({ + x: processedGroupData[groupIds[i]][j].x, + y: processedGroupData[groupIds[i]][j].y, + groupId: groupIds[i], + label: processedGroupData[groupIds[i]][j].label + }); + barPoints += 1; + } + } + } + } - locales: locales, - locale: "en" - }; - this.options = util.extend({}, this.defaultOptions); - this.offset = 0; + if (barPoints == 0) { + return; + } - this._create(); + // 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; + } + }); - this.setOptions(options); - } + // get intersections + Bargraph._getDataIntersections(intersections, combinedData); - CurrentTime.prototype = new Component(); + // plot barchart + for (i = 0; i < combinedData.length; i++) { + group = framework.groups[combinedData[i].groupId]; + var minWidth = 0.1 * group.options.barChart.width; - /** - * Create the HTML DOM for the current time bar - * @private - */ - CurrentTime.prototype._create = function () { - var bar = document.createElement("div"); - bar.className = "currenttime"; - bar.style.position = "absolute"; - bar.style.top = "0px"; - bar.style.height = "100%"; + key = combinedData[i].x; + var heightOffset = 0; + if (intersections[key] === undefined) { + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - key); + } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); + } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + } else { + var nextKey = i + (intersections[key].amount - intersections[key].resolved); + var prevKey = i - (intersections[key].resolved + 1); + if (nextKey < combinedData.length) { + coreDistance = Math.abs(combinedData[nextKey].x - key); + } + if (prevKey > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + intersections[key].resolved += 1; - this.bar = bar; + if (group.options.barChart.handleOverlap == "stack") { + heightOffset = intersections[key].accumulated; + intersections[key].accumulated += group.zeroPosition - combinedData[i].y; + } else if (group.options.barChart.handleOverlap == "sideBySide") { + drawData.width = drawData.width / intersections[key].amount; + drawData.offset += intersections[key].resolved * drawData.width - 0.5 * drawData.width * (intersections[key].amount + 1); + if (group.options.barChart.align == "left") { + drawData.offset -= 0.5 * drawData.width; + } else if (group.options.barChart.align == "right") { + drawData.offset += 0.5 * drawData.width; + } + } + } + DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + " bar", framework.svgElements, framework.svg); + // draw points + if (group.options.drawPoints.enabled == true) { + Points.draw([combinedData[i]], group, framework, drawData.offset); + //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); + } + } }; + /** - * Destroy the CurrentTime bar + * Fill the intersections object with counters of how many datapoints share the same x coordinates + * @param intersections + * @param combinedData + * @private */ - CurrentTime.prototype.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing - - this.body = null; + Bargraph._getDataIntersections = function (intersections, combinedData) { + // get intersections + var coreDistance; + for (var i = 0; i < combinedData.length; i++) { + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); + } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); + } + if (coreDistance == 0) { + if (intersections[combinedData[i].x] === undefined) { + intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulated: 0 }; + } + intersections[combinedData[i].x].amount += 1; + } + } }; + /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] + * Get the width and offset for bargraphs based on the coredistance between datapoints + * + * @param coreDistance + * @param group + * @param minWidth + * @returns {{width: Number, offset: Number}} + * @private */ - CurrentTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(["showCurrentTime", "locale", "locales"], this.options, options); + Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { + var width, offset; + if (coreDistance < group.options.barChart.width && coreDistance > 0) { + width = coreDistance < minWidth ? minWidth : coreDistance; + + offset = 0; // recalculate offset with the new width; + if (group.options.barChart.align == "left") { + offset -= 0.5 * coreDistance; + } else if (group.options.barChart.align == "right") { + offset += 0.5 * coreDistance; + } + } else { + // default settings + width = group.options.barChart.width; + offset = 0; + if (group.options.barChart.align == "left") { + offset -= 0.5 * group.options.barChart.width; + } else if (group.options.barChart.align == "right") { + offset += 0.5 * group.options.barChart.width; + } } + + return { width: width, offset: offset }; }; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - 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); + Bargraph.getStackedBarYRange = function (barCombinedData, groupRanges, groupIds, groupLabel, orientation) { + if (barCombinedData.length > 0) { + // sort by time and by group + barCombinedData.sort(function (a, b) { + if (a.x == b.x) { + return a.groupId - b.groupId; + } else { + return a.x - b.x; } - parent.appendChild(this.bar); + }); + var intersections = {}; - this.start(); + Bargraph._getDataIntersections(intersections, barCombinedData); + groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData); + groupRanges[groupLabel].yAxisOrientation = orientation; + groupIds.push(groupLabel); + } + }; + + Bargraph._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; } - - var now = new Date(new Date().valueOf() + this.offset); - 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; - } else { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); + } + 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; } - this.stop(); } - return false; + return { min: yMin, max: yMax }; }; - /** - * Start auto refreshing the current time bar - */ - CurrentTime.prototype.start = function () { - var me = this; + module.exports = Bargraph; - function update() { - me.stop(); +/***/ }, +/* 47 */ +/***/ function(module, exports, __webpack_require__) { - // 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; + "use strict"; - me.redraw(); + /** + * Created by Alex on 11/11/2014. + */ + var DOMutil = __webpack_require__(2); - // start a renderTimer to adjust for the new time - me.currentTimeTimer = setTimeout(update, interval); - } + function Points(groupId, options) { + this.groupId = groupId; + this.options = options; + } - update(); - }; - /** - * Stop auto refreshing the current time bar - */ - CurrentTime.prototype.stop = function () { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; + Points.prototype.getYRange = function (groupData) { + var yMin = groupData[0].y; + var yMax = groupData[0].y; + for (var j = 0; j < groupData.length; j++) { + yMin = yMin > groupData[j].y ? groupData[j].y : yMin; + yMax = yMax < groupData[j].y ? groupData[j].y : yMax; } + return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; }; - /** - * Set a current time. This can be used for example to ensure that a client's - * time is synchronized with a shared server time. - * @param {Date | String | Number} time A Date, unix timestamp, or - * ISO date string. - */ - CurrentTime.prototype.setCurrentTime = function (time) { - var t = util.convert(time, "Date").valueOf(); - var now = new Date().valueOf(); - this.offset = t - now; - this.redraw(); + Points.prototype.draw = function (dataset, group, framework, offset) { + Points.draw(dataset, group, framework, offset); }; /** - * Get the current time. - * @return {Date} Returns the current time. + * draw the data points + * + * @param {Array} dataset + * @param {Object} JSONcontainer + * @param {Object} svg | SVG DOM element + * @param {GraphGroup} group + * @param {Number} [offset] */ - CurrentTime.prototype.getCurrentTime = function () { - return new Date(new Date().valueOf() + this.offset); + Points.draw = function (dataset, group, framework, offset) { + if (offset === undefined) { + offset = 0; + } + for (var i = 0; i < dataset.length; i++) { + DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label); + } }; - module.exports = CurrentTime; + + module.exports = Points; /***/ }, -/* 44 */ +/* 48 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Emitter = __webpack_require__(11); - var Hammer = __webpack_require__(19); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); - var Range = __webpack_require__(23); - var Core = __webpack_require__(27); - var TimeAxis = __webpack_require__(42); - var CurrentTime = __webpack_require__(43); - var CustomTime = __webpack_require__(40); - var LineGraph = __webpack_require__(45); - /** - * Create a timeline visualization - * @param {HTMLElement} container - * @param {vis.DataSet | Array | google.visualization.DataTable} [items] - * @param {Object} [options] See Graph2d.setOptions for the available options. - * @constructor - * @extends Core + * Canvas shapes used by Network */ - function Graph2d(container, items, groups, options) { - // if the third element is options, the forth is groups (optionally); - if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) { - var forthArgument = options; - options = groups; - groups = forthArgument; - } - - var me = this; - this.defaultOptions = { - start: null, - end: null, - - autoResize: true, - - orientation: "bottom", - width: null, - height: null, - maxHeight: null, - minHeight: null + if (typeof CanvasRenderingContext2D !== "undefined") { + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function (x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2 * Math.PI, false); }; - this.options = util.deepExtend({}, this.defaultOptions); - - // Create the DOM, props, and emitter - this._create(container); - - // all components listed here will be repainted automatically - this.components = []; - this.body = { - dom: this.dom, - domProps: this.props, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this) - }, - hiddenDates: [], - util: { - toScreen: me._toScreen.bind(me), - toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width - toTime: me._toTime.bind(me), - toGlobalTime: me._toGlobalTime.bind(me) - } + /** + * Draw a square shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r size, width and height of the square + */ + CanvasRenderingContext2D.prototype.square = function (x, y, r) { + this.beginPath(); + this.rect(x - r, y - r, r * 2, r * 2); }; - // 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); - } - - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } - - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); - } - } - - // Extend the functionality from Core - Graph2d.prototype = new Core(); + /** + * Draw a triangle shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.triangle = function (x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); - /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items - */ - Graph2d.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; + // the change in radius and the offset is here to center the shape + r *= 1.15; + y += 0.275 * r; - // 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" - } - }); - } + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height - // set items - this.itemsData = newDataSet; - this.linegraph && this.linegraph.setItems(newDataSet); - if (initialLoad) { - if (this.options.start != undefined || this.options.end != undefined) { - var start = this.options.start != undefined ? this.options.start : null; - var end = this.options.end != undefined ? this.options.end : null; + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); - this.setWindow(start, end, { animate: false }); - } else { - this.fit({ animate: false }); - } - } - }; + }; - /** - * Set groups - * @param {vis.DataSet | Array | google.visualization.DataTable} groups - */ - Graph2d.prototype.setGroups = function (groups) { - // convert to type DataSet when needed - var newDataSet; - if (!groups) { - newDataSet = null; - } else if (groups instanceof DataSet || groups instanceof DataView) { - newDataSet = groups; - } else { - // turn an array into a dataset - newDataSet = new DataSet(groups); - } + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function (x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); - this.groupsData = newDataSet; - this.linegraph.setGroups(newDataSet); - }; + // the change in radius and the offset is here to center the shape + r *= 1.15; + y -= 0.275 * r; - /** - * Returns an object containing an SVG element with the icon of the group (size determined by iconWidth and iconHeight), the label of the group (content) and the yAxisOrientation of the group (left or right). - * @param groupId - * @param width - * @param height - */ - Graph2d.prototype.getLegend = function (groupId, width, height) { - if (width === undefined) { - width = 15; - } - if (height === undefined) { - height = 15; - } - if (this.linegraph.groups[groupId] !== undefined) { - return this.linegraph.groups[groupId].getLegend(width, height); - } else { - return "cannot find group:" + groupId; - } - }; + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height - /** - * This checks if the visible option of the supplied group (by ID) is true or false. - * @param groupId - * @returns {*} - */ - Graph2d.prototype.isGroupVisible = function (groupId) { - if (this.linegraph.groups[groupId] !== undefined) { - return this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true); - } else { - return false; - } - }; + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function (x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); - /** - * Get the data range of the item set. - * @returns {{min: Date, max: Date}} range A range with a start and end Date. - * When no minimum is found, min==null - * When no maximum is found, max==null - */ - Graph2d.prototype.getItemRange = function () { - var min = null; - var max = null; + // the change in radius and the offset is here to center the shape + r *= 0.82; + y += 0.1 * r; - // calculate min from start filed - for (var groupId in this.linegraph.groups) { - if (this.linegraph.groups.hasOwnProperty(groupId)) { - if (this.linegraph.groups[groupId].visible == true) { - for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) { - var item = this.linegraph.groups[groupId].itemsData[i]; - var value = util.convert(item.x, "Date").valueOf(); - min = min == null ? value : min > value ? value : min; - max = max == null ? value : max < value ? value : max; - } - } + for (var n = 0; n < 10; n++) { + var radius = n % 2 === 0 ? r * 1.3 : r * 0.5; + this.lineTo(x + radius * Math.sin(n * 2 * Math.PI / 10), y - radius * Math.cos(n * 2 * Math.PI / 10)); } - } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null + this.closePath(); }; - }; - + /** + * Draw a Diamond shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.diamond = function (x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); - module.exports = Graph2d; - -/***/ }, -/* 45 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(6); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); - var Component = __webpack_require__(25); - var DataAxis = __webpack_require__(46); - var GraphGroup = __webpack_require__(48); - var Legend = __webpack_require__(52); - var BarGraphFunctions = __webpack_require__(51); - - var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items + this.lineTo(x, y + r); + this.lineTo(x + r, y); + this.lineTo(x, y - r); + this.lineTo(x - r, y); - /** - * This is the constructor of the LineGraph. It requires a Timeline body and options. - * - * @param body - * @param options - * @constructor - */ - function LineGraph(body, options) { - this.id = util.randomUUID(); - this.body = body; - this.defaultOptions = { - yAxisOrientation: "left", - defaultGroup: "default", - sort: true, - sampling: true, - graphHeight: "400px", - shaded: { - enabled: false, - orientation: "bottom" // top, bottom - }, - style: "line", // line, bar - barChart: { - width: 50, - handleOverlap: "overlap", - align: "center" // left, center, right - }, - catmullRom: { - enabled: true, - parametrization: "centripetal", // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5) - alpha: 0.5 - }, - drawPoints: { - enabled: true, - size: 6, - style: "square" // square, circle - }, - dataAxis: { - showMinorLabels: true, - showMajorLabels: true, - icons: false, - width: "40px", - visible: true, - alignZeros: true, - customRange: { - left: { min: undefined, max: undefined }, - right: { min: undefined, max: undefined } - } - //, these options are not set by default, but this shows the format they will be in - //format: { - // left: {decimals: 2}, - // right: {decimals: 2} - //}, - //title: { - // left: { - // text: 'left', - // style: 'color:black;' - // }, - // right: { - // text: 'right', - // style: 'color:black;' - // } - //} - }, - legend: { - enabled: false, - icons: true, - left: { - visible: true, - position: "top-left" // top/bottom - left,right - }, - right: { - visible: true, - position: "top-right" // top/bottom - left,right - } - }, - groups: { - visibility: {} - } + this.closePath(); }; - // options is shared by this ItemSet and all its items - this.options = util.extend({}, this.defaultOptions); - this.dom = {}; - this.props = {}; - this.hammer = null; - this.groups = {}; - this.abortedGraphUpdate = false; - this.updateSVGheight = false; - this.updateSVGheightOnResize = false; + /** + * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas + */ + CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { + var r2d = Math.PI / 180; + if (w - 2 * r < 0) { + r = w / 2; + } //ensure that the radius isn't too large for x + if (h - 2 * r < 0) { + r = h / 2; + } //ensure that the radius isn't too large for y + this.beginPath(); + this.moveTo(x + r, y); + this.lineTo(x + w - r, y); + this.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); + this.lineTo(x + w, y + h - r); + this.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); + this.lineTo(x + r, y + h); + this.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false); + this.lineTo(x, y + r); + this.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); + }; - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.ellipse = function (x, y, w, h) { + var kappa = 0.5522848, + ox = w / 2 * kappa, + // control point offset horizontal + oy = h / 2 * kappa, + // control point offset vertical + xe = x + w, + // x-end + ye = y + h, + // y-end + xm = x + w / 2, + // x-middle + ym = y + h / 2; // y-middle - // 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); - } + this.beginPath(); + this.moveTo(x, ym); + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); }; - // listeners for the DataSet of the groups - this.groupListeners = { - add: function (event, params, senderId) { - me._onAddGroups(params.items); - }, - update: function (event, params, senderId) { - me._onUpdateGroups(params.items); - }, - remove: function (event, params, senderId) { - me._onRemoveGroups(params.items); - } - }; - this.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 + /** + * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas + */ + CanvasRenderingContext2D.prototype.database = function (x, y, w, h) { + var f = 1 / 3; + var wEllipse = w; + var hEllipse = h * f; - this.svgElements = {}; - this.setOptions(options); - this.groupsUsingDefaultStyles = [0]; - this.COUNTER = 0; - this.body.emitter.on("rangechanged", function () { - me.lastStart = me.body.range.start; - me.svg.style.left = util.option.asSize(-me.props.width); - me.redraw.call(me, true); - }); + var kappa = 0.5522848, + ox = wEllipse / 2 * kappa, + // control point offset horizontal + oy = hEllipse / 2 * kappa, + // control point offset vertical + xe = x + wEllipse, + // x-end + ye = y + hEllipse, + // y-end + xm = x + wEllipse / 2, + // x-middle + ym = y + hEllipse / 2, + // y-middle + ymb = y + (h - hEllipse / 2), + // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse - // create the HTML DOM - this._create(); - this.framework = { svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups }; - this.body.emitter.emit("change"); - } + this.beginPath(); + this.moveTo(xe, ym); - LineGraph.prototype = new Component(); + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - /** - * Create the HTML DOM for the ItemSet - */ - LineGraph.prototype._create = function () { - var frame = document.createElement("div"); - frame.className = "LineGraph"; - this.dom.frame = frame; + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - // 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); + this.lineTo(xe, ymb); - // data axis - this.options.dataAxis.orientation = "left"; - this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); - this.options.dataAxis.orientation = "right"; - this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - delete this.options.dataAxis.orientation; + this.lineTo(x, ym); + }; - // legends - this.legendLeft = new Legend(this.body, this.options.legend, "left", this.options.groups); - this.legendRight = new Legend(this.body, this.options.legend, "right", this.options.groups); - this.show(); - }; + /** + * Draw an arrow point (no line) + */ + CanvasRenderingContext2D.prototype.arrow = function (x, y, angle, length) { + // tail + var xt = x - length * Math.cos(angle); + var yt = y - length * Math.sin(angle); - /** - * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. - * @param {object} options - */ - LineGraph.prototype.setOptions = function (options) { - if (options) { - var fields = ["sampling", "defaultGroup", "height", "graphHeight", "yAxisOrientation", "style", "barChart", "dataAxis", "sort", "groups"]; - if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) { - this.updateSVGheight = true; - this.updateSVGheightOnResize = true; - } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { - if (parseInt((options.graphHeight + "").replace("px", "")) < this.body.domProps.centerContainer.height) { - this.updateSVGheight = true; - } - } - util.selectiveDeepExtend(fields, this.options, options); - util.mergeOptions(this.options, options, "catmullRom"); - util.mergeOptions(this.options, options, "drawPoints"); - util.mergeOptions(this.options, options, "shaded"); - util.mergeOptions(this.options, options, "legend"); + // inner tail + // TODO: allow to customize different shapes + var xi = x - length * 0.9 * Math.cos(angle); + var yi = y - length * 0.9 * Math.sin(angle); - 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; - } else { - this.options.catmullRom.parametrization = "centripetal"; - this.options.catmullRom.alpha = 0.5; - } - } - } - } + // left + var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); + var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); - if (this.yAxisLeft) { - if (options.dataAxis !== undefined) { - this.yAxisLeft.setOptions(this.options.dataAxis); - this.yAxisRight.setOptions(this.options.dataAxis); - } - } + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); - if (this.legendLeft) { - if (options.legend !== undefined) { - this.legendLeft.setOptions(this.options.legend); - this.legendRight.setOptions(this.options.legend); - } - } + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; - if (this.groups.hasOwnProperty(UNGROUPED)) { - this.groups[UNGROUPED].setOptions(options); + /** + * Sets up the dashedLine functionality for drawing + * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas + * @author David Jordan + * @date 2012-08-08 + */ + CanvasRenderingContext2D.prototype.dashedLine = function (x, y, x2, y2, dashArray) { + if (!dashArray) dashArray = [10, 5]; + if (dashLength == 0) dashLength = 0.001; // Hack for Safari + var dashCount = dashArray.length; + this.moveTo(x, y); + var dx = x2 - x, + dy = y2 - y; + var slope = dy / dx; + var distRemaining = Math.sqrt(dx * dx + dy * dy); + var dashIndex = 0, + draw = true; + while (distRemaining >= 0.1) { + var dashLength = dashArray[dashIndex++ % dashCount]; + if (dashLength > distRemaining) dashLength = distRemaining; + var xStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope)); + if (dx < 0) xStep = -xStep; + x += xStep; + y += slope * xStep; + this[draw ? "lineTo" : "moveTo"](x, y); + distRemaining -= dashLength; + draw = !draw; } - } + }; - // this is used to redraw the graph if the visibility of the groups is changed. - if (this.dom.frame) { - this.redraw(true); - } - }; + // TODO: add diamond shape + } - /** - * Hide the component from the DOM - */ - LineGraph.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } - }; +/***/ }, +/* 49 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; + + var keycharm = __webpack_require__(78); + var Emitter = __webpack_require__(62); + var Hammer = __webpack_require__(41); + var util = __webpack_require__(1); /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * Turn an element into an clickToUse element. + * When not active, the element has a transparent overlay. When the overlay is + * clicked, the mode is changed to active. + * When active, the element is displayed with a blue border around it, and + * the interactive contents of the element can be used. When clicked outside + * the element, the elements mode is changed to inactive. + * @param {Element} container + * @constructor */ - LineGraph.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } - }; + function Activator(container) { + this.active = false; + this.dom = { + container: container + }; - /** - * Set items - * @param {vis.DataSet | null} items - */ - LineGraph.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; + this.dom.overlay = document.createElement("div"); + this.dom.overlay.className = "overlay"; - // 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.dom.container.appendChild(this.dom.overlay); - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); + this.hammer = Hammer(this.dom.overlay, { prevent_default: false }); + this.hammer.on("tap", this._onTapOverlay.bind(this)); + + // block all touch events (except tap) + var me = this; + var events = ["touch", "pinch", "doubletap", "hold", "dragstart", "drag", "dragend", "mousewheel", "DOMMouseScroll" // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (event) { + me.hammer.on(event, function (event) { + event.stopPropagation(); }); + }); - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); + // attach a tap event to the window, in order to deactivate when clicking outside the timeline + this.windowHammer = Hammer(window, { prevent_default: false }); + this.windowHammer.on("tap", function (event) { + // deactivate when clicked outside the container + if (!_hasParent(event.target, container)) { + me.deactivate(); + } + }); + + if (this.keycharm !== undefined) { + this.keycharm.destroy(); } + this.keycharm = keycharm(); - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); - }); + // keycharm listener only bounded when active) + this.escListener = this.deactivate.bind(this); + } - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); - } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); - }; + // turn into an event emitter + Emitter(Activator.prototype); + // The currently active activator + Activator.current = null; /** - * Set groups - * @param {vis.DataSet} groups + * Destroy the activator. Cleans up all created DOM and event listeners */ - LineGraph.prototype.setGroups = function (groups) { - var me = this; - var ids; + Activator.prototype.destroy = function () { + this.deactivate(); - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + // remove dom + this.dom.overlay.parentNode.removeChild(this.dom.overlay); - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + // cleanup hammer instances + this.hammer = null; + this.windowHammer = null; + // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + }; - // 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"); + /** + * Activate the element + * Overlay is hidden, element is decorated with a blue shadow border + */ + Activator.prototype.activate = function () { + // we allow only one active activator at a time + if (Activator.current) { + Activator.current.deactivate(); } + Activator.current = this; - 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.active = true; + this.dom.overlay.style.display = "none"; + util.addClassName(this.dom.container, "vis-active"); - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); - } - this._onUpdate(); - }; + this.emit("change"); + this.emit("activate"); + // ugly hack: bind ESC after emitting the events, as the Network rebinds all + // keyboard events on a 'change' event + this.keycharm.bind("esc", this.escListener); + }; /** - * Update the data - * @param [ids] - * @private + * Deactivate the element + * Overlay is displayed on top of the element */ - LineGraph.prototype._onUpdate = function (ids) { - this._updateUngrouped(); - this._updateAllGroupData(); - //this._updateGraph(); - this.redraw(true); - }; - LineGraph.prototype._onAdd = function (ids) { - this._onUpdate(ids); - }; - LineGraph.prototype._onRemove = function (ids) { - this._onUpdate(ids); - }; - LineGraph.prototype._onUpdateGroups = function (groupIds) { - for (var i = 0; i < groupIds.length; i++) { - var group = this.groupsData.get(groupIds[i]); - this._updateGroup(group, groupIds[i]); - } + Activator.prototype.deactivate = function () { + this.active = false; + this.dom.overlay.style.display = ""; + util.removeClassName(this.dom.container, "vis-active"); + this.keycharm.unbind("esc", this.escListener); - //this._updateGraph(); - this.redraw(true); - }; - LineGraph.prototype._onAddGroups = function (groupIds) { - this._onUpdateGroups(groupIds); + this.emit("change"); + this.emit("deactivate"); }; - /** - * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph - * @param {Array} groupIds + * Handle a tap event: activate the container + * @param event * @private */ - LineGraph.prototype._onRemoveGroups = function (groupIds) { - for (var i = 0; i < groupIds.length; i++) { - if (this.groups.hasOwnProperty(groupIds[i])) { - if (this.groups[groupIds[i]].options.yAxisOrientation == "right") { - this.yAxisRight.removeGroup(groupIds[i]); - this.legendRight.removeGroup(groupIds[i]); - this.legendRight.redraw(); - } else { - this.yAxisLeft.removeGroup(groupIds[i]); - this.legendLeft.removeGroup(groupIds[i]); - this.legendLeft.redraw(); - } - delete this.groups[groupIds[i]]; - } - } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); + Activator.prototype._onTapOverlay = function (event) { + // activate the container + this.activate(); + event.stopPropagation(); }; - /** - * update a group object with the group dataset entree - * - * @param group - * @param groupId + * Test whether the element has the requested parent element somewhere in + * its chain of parent nodes. + * @param {HTMLElement} element + * @param {HTMLElement} parent + * @returns {boolean} Returns true when the parent is found somewhere in the + * chain of parent nodes. * @private */ - LineGraph.prototype._updateGroup = function (group, groupId) { - if (!this.groups.hasOwnProperty(groupId)) { - this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles); - if (this.groups[groupId].options.yAxisOrientation == "right") { - this.yAxisRight.addGroup(groupId, this.groups[groupId]); - this.legendRight.addGroup(groupId, this.groups[groupId]); - } else { - this.yAxisLeft.addGroup(groupId, this.groups[groupId]); - this.legendLeft.addGroup(groupId, this.groups[groupId]); - } - } else { - this.groups[groupId].update(group); - if (this.groups[groupId].options.yAxisOrientation == "right") { - this.yAxisRight.updateGroup(groupId, this.groups[groupId]); - this.legendRight.updateGroup(groupId, this.groups[groupId]); - } else { - this.yAxisLeft.updateGroup(groupId, this.groups[groupId]); - this.legendLeft.updateGroup(groupId, this.groups[groupId]); + function _hasParent(element, parent) { + while (element) { + if (element === parent) { + return true; } + element = element.parentNode; } - this.legendLeft.redraw(); - this.legendRight.redraw(); - }; + return false; + } + + module.exports = Activator; + +/***/ }, +/* 50 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var util = __webpack_require__(1); /** - * this updates all groups, it is used when there is an update the the itemset. - * - * @private + * @class Groups + * This class can store groups and options specific for groups. */ - LineGraph.prototype._updateAllGroupData = function () { - if (this.itemsData != null) { - var groupsContent = {}; - var groupId; - for (groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - groupsContent[groupId] = []; - } - } - for (var itemId in this.itemsData._data) { - if (this.itemsData._data.hasOwnProperty(itemId)) { - var item = this.itemsData._data[itemId]; - if (groupsContent[item.group] === undefined) { - throw new Error("Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups."); - } - item.x = util.convert(item.x, "Date"); - groupsContent[item.group].push(item); - } - } - for (groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - this.groups[groupId].setItems(groupsContent[groupId]); - } - } + var Groups = (function () { + function Groups() { + _classCallCheck(this, Groups); + + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; + + this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue + { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow + { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red + { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green + { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta + { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple + { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange + { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue + { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink + { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red + + { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange + { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue + { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green + { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta + { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple + { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue + { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink + { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint + + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; + + this.options = {}; + this.defaultOptions = { + useDefaultGroups: true + }; + util.extend(this.options, this.defaultOptions); } - }; + _prototypeProperties(Groups, null, { + setOptions: { + value: function setOptions(options) { + var optionFields = ["useDefaultGroups"]; - /** - * Create or delete the group holding all ungrouped items. This group is used when - * there are no groups specified. This anonymous group is called 'graph'. - * @protected - */ - LineGraph.prototype._updateUngrouped = function () { - if (this.itemsData && this.itemsData != null) { - var ungroupedCounter = 0; - for (var itemId in this.itemsData._data) { - if (this.itemsData._data.hasOwnProperty(itemId)) { - var item = this.itemsData._data[itemId]; - if (item != undefined) { - if (item.hasOwnProperty("group")) { - if (item.group === undefined) { - item.group = UNGROUPED; + if (options !== undefined) { + for (var groupName in options) { + if (options.hasOwnProperty(groupName)) { + if (optionFields.indexOf(groupName) == -1) { + var group = options[groupName]; + this.add(groupName, group); + } } + } + } + }, + writable: true, + configurable: true + }, + clear: { + + + /** + * Clear all groups + */ + value: function clear() { + this.groups = {}; + this.groupsArray = []; + }, + writable: true, + configurable: true + }, + get: { + + /** + * get group options of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group options + */ + value: function get(groupname) { + var group = this.groups[groupname]; + if (group == undefined) { + if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; } else { - item.group = UNGROUPED; + // create new group + var index = this.defaultIndex % this.defaultGroups.length; + this.defaultIndex++; + group = {}; + group.color = this.defaultGroups[index]; + this.groups[groupname] = group; } - ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter; } - } - } - if (ungroupedCounter == 0) { - delete this.groups[UNGROUPED]; - this.legendLeft.removeGroup(UNGROUPED); - this.legendRight.removeGroup(UNGROUPED); - this.yAxisLeft.removeGroup(UNGROUPED); - this.yAxisRight.removeGroup(UNGROUPED); - } else { - var group = { id: UNGROUPED, content: this.options.defaultGroup }; - this._updateGroup(group, UNGROUPED); + return group; + }, + writable: true, + configurable: true + }, + add: { + + /** + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ + value: function add(groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; + }, + writable: true, + configurable: true } - } else { - delete this.groups[UNGROUPED]; - this.legendLeft.removeGroup(UNGROUPED); - this.legendRight.removeGroup(UNGROUPED); - this.yAxisLeft.removeGroup(UNGROUPED); - this.yAxisRight.removeGroup(UNGROUPED); - } + }); - this.legendLeft.redraw(); - this.legendRight.redraw(); - }; + return Groups; + })(); + + module.exports = Groups; + // 20:bright red + +/***/ }, +/* 51 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Redraw the component, mandatory function - * @return {boolean} Returns true if the component is resized + * Created by Alex on 3/4/2015. */ - LineGraph.prototype.redraw = function (forceGraphUpdate) { - var resized = false; - // calculate actual size and position - this.props.width = this.dom.frame.offsetWidth; - this.props.height = this.body.domProps.centerContainer.height; + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); - // update the graph if there is no lastWidth or with, used for the initial draw - if (this.lastWidth === undefined && this.props.width) { - forceGraphUpdate = true; - } + var Node = _interopRequire(__webpack_require__(66)); - // check if this component is resized - resized = this._isResized() || resized; + var NodesHandler = (function () { + function NodesHandler(body, images, groups, layoutEngine) { + var _this = this; + _classCallCheck(this, NodesHandler); - // check whether zoomed (in that case we need to re-stack everything) - var visibleInterval = this.body.range.end - this.body.range.start; - var zoomed = visibleInterval != this.lastVisibleInterval; - this.lastVisibleInterval = visibleInterval; + this.body = body; + this.images = images; + this.groups = groups; + this.layoutEngine = layoutEngine; + // create the node API in the body container + this.body.functions.createNode = this.create.bind(this); - // the svg element is three times as big as the width, this allows for fully dragging left and right - // without reloading the graph. the controls for this are bound to events in the constructor - if (resized == true) { - this.svg.style.width = util.option.asSize(3 * this.props.width); - this.svg.style.left = util.option.asSize(-this.props.width); + this.nodesListeners = { + add: function (event, params) { + _this.add(params.items); + }, + update: function (event, params) { + _this.update(params.items, params.data); + }, + remove: function (event, params) { + _this.remove(params.items); + } + }; - // if the height of the graph is set as proportional, change the height of the svg - if ((this.options.height + "").indexOf("%") != -1 || this.updateSVGheightOnResize == true) { - this.updateSVGheight = true; - } - } - // update the height of the graph on each redraw of the graph. - if (this.updateSVGheight == true) { - if (this.options.graphHeight != this.body.domProps.centerContainer.height + "px") { - this.options.graphHeight = this.body.domProps.centerContainer.height + "px"; - this.svg.style.height = this.body.domProps.centerContainer.height + "px"; - } - this.updateSVGheight = false; - } else { - this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; + this.options = {}; + this.defaultOptions = { + borderWidth: 1, + borderWidthSelected: undefined, + color: { + border: "#2B7CE9", + background: "#97C2FC", + highlight: { + border: "#2B7CE9", + background: "#D2E5FF" + }, + hover: { + border: "#2B7CE9", + background: "#D2E5FF" + } + }, + fixed: { + x: false, + y: false + }, + font: { + color: "#343434", + size: 14, // px + face: "arial", + background: "none", + stroke: 0, // px + strokeColor: "white", + align: "horizontal" + }, + group: undefined, + hidden: false, + icon: { + face: undefined, //'FontAwesome', + code: undefined, //'\uf007', + size: undefined, //50, + color: undefined //'#aa00ff' + }, + image: undefined, // --> URL + label: undefined, + level: undefined, + mass: 1, + physics: true, + scaling: { + min: 10, + max: 30, + label: { + enabled: true, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 3 + }, + customScalingFunction: function (min, max, total, value) { + if (max == min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + } + }, + shape: "ellipse", + size: 25, + value: 1, + x: undefined, + y: undefined + }; + util.extend(this.options, this.defaultOptions); } - // zoomed is here to ensure that animations are shown correctly. - if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) { - resized = this._updateGraph() || resized; - } else { - // move the whole svg while dragging - if (this.lastStart != 0) { - var offset = this.body.range.start - this.lastStart; - var range = this.body.range.end - this.body.range.start; - if (this.props.width != 0) { - var rangePerPixelInv = this.props.width / range; - var xOffset = offset * rangePerPixelInv; - this.svg.style.left = -this.props.width - xOffset + "px"; - } - } - } + _prototypeProperties(NodesHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options) { + util.selectiveNotDeepExtend(["color"], this.options, options); + if (options.color) { + this.options.color = util.parseColor(options.color); + } + } + }, + writable: true, + configurable: true + }, + setData: { - this.legendLeft.redraw(); - this.legendRight.redraw(); - return resized; - }; + /** + * Set a data set with nodes for the network + * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * @private + */ + value: function setData(nodes) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var oldNodesData = this.body.data.nodes; + + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.body.data.nodes = nodes; + } else if (Array.isArray(nodes)) { + this.body.data.nodes = new DataSet(); + this.body.data.nodes.add(nodes); + } else if (!nodes) { + this.body.data.nodes = new DataSet(); + } else { + throw new TypeError("Array or DataSet expected"); + } + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.off(event, callback); + }); + } - /** - * Update and redraw the graph. - * - */ - LineGraph.prototype._updateGraph = function () { - // reset the svg elements - DOMutil.prepareElements(this.svgElements); - if (this.props.width != 0 && this.itemsData != null) { - var group, i; - var preprocessedGroupData = {}; - var processedGroupData = {}; - var groupRanges = {}; - var changeCalled = false; + // remove drawn nodes + this.body.nodes = {}; - // getting group Ids - var groupIds = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - group = this.groups[groupId]; - if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) { - groupIds.push(groupId); + if (this.body.data.nodes) { + // subscribe to new dataset + var me = this; + util.forEach(this.nodesListeners, function (callback, event) { + me.body.data.nodes.on(event, callback); + }); + + // draw all new nodes + var ids = this.body.data.nodes.getIds(); + this.add(ids, true); } - } - } - if (groupIds.length > 0) { - // this is the range of the SVG canvas - var minDate = this.body.util.toGlobalTime(-this.body.domProps.root.width); - var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width); - var groupsData = {}; - // fill groups data, this only loads the data we require based on the timewindow - this._getRelevantData(groupIds, groupsData, minDate, maxDate); - // apply sampling, if disabled, it will pass through this function. - this._applySampling(groupIds, groupsData); + if (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + add: { - // 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); + /** + * Add nodes + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var id; + var newNodes = []; + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + var properties = this.body.data.nodes.get(id); + var node = this.create(properties);; + newNodes.push(node); + this.body.nodes[id] = node; // note: this may replace an existing node + } - // update the Y axis first, we use this data to draw at the correct Y points - // changeCalled is required to clean the SVG on a change emit. - changeCalled = this._updateYAxis(groupIds, groupRanges); - var MAX_CYCLES = 5; - if (changeCalled == true && this.COUNTER < MAX_CYCLES) { - DOMutil.cleanupElements(this.svgElements); - this.abortedGraphUpdate = true; - this.COUNTER++; - this.body.emitter.emit("change"); - return true; - } else { - if (this.COUNTER > MAX_CYCLES) { - console.log("WARNING: there may be an infinite loop in the _updateGraph emitter cycle."); + this.layoutEngine.positionInitially(newNodes); + + if (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + update: { + + /** + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + value: function update(ids, changedData) { + var nodes = this.body.nodes; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = changedData[i]; + if (node !== undefined) { + // update node + node.setOptions(data, this.constants); + } else { + dataChanged = true; + // create node + node = this.create(properties); + nodes[id] = node; + } } - this.COUNTER = 0; - this.abortedGraphUpdate = 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); + if (dataChanged === true) { + this.body.emitter.emit("_dataChanged"); + } else { + this.body.emitter.emit("_dataUpdated"); } + }, + writable: true, + configurable: true + }, + remove: { - // draw the groups - for (i = 0; i < groupIds.length; i++) { - group = this.groups[groupIds[i]]; - if (group.options.style != "bar") { - // bar needs to be drawn enmasse - group.draw(processedGroupData[groupIds[i]], group, this.framework); - } + /** + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var nodes = this.body.nodes; + + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + delete nodes[id]; } - BarGraphFunctions.draw(groupIds, processedGroupData, this.framework); - } + + this.body.emitter.emit("_dataChanged"); + }, + writable: true, + configurable: true + }, + create: { + value: function create(properties) { + var constructorClass = arguments[1] === undefined ? Node : arguments[1]; + return new constructorClass(properties, this.body, this.images, this.groups, this.options); + }, + writable: true, + configurable: true } - } + }); - // cleanup unused svg elements - DOMutil.cleanupElements(this.svgElements); - return false; - }; + return NodesHandler; + })(); + module.exports = NodesHandler; - /** - * first select and preprocess the data from the datasets. - * the groups have their preselection of data, we now loop over this data to see - * what data we need to draw. Sorted data is much faster. - * more optimization is possible by doing the sampling before and using the binary search - * to find the end date to determine the increment. - * - * @param {array} groupIds - * @param {object} groupsData - * @param {date} minDate - * @param {date} maxDate - * @private - */ - LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { - var group, i, j, item; - if (groupIds.length > 0) { - for (i = 0; i < groupIds.length; i++) { - group = this.groups[groupIds[i]]; - groupsData[groupIds[i]] = []; - var dataContainer = groupsData[groupIds[i]]; - // optimization for sorted data - if (group.options.sort == true) { - var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, "x", "before")); - for (j = guess; j < group.itemsData.length; j++) { - item = group.itemsData[j]; - if (item !== undefined) { - if (item.x > maxDate) { - dataContainer.push(item); - break; - } else { - dataContainer.push(item); - } - } - } - } else { - for (j = 0; j < group.itemsData.length; j++) { - item = group.itemsData[j]; - if (item !== undefined) { - if (item.x > minDate && item.x < maxDate) { - dataContainer.push(item); - } - } - } - } - } - } - }; +/***/ }, +/* 52 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * - * @param groupIds - * @param groupsData - * @private + * Created by Alex on 3/4/2015. */ - LineGraph.prototype._applySampling = function (groupIds, groupsData) { - var group; - if (groupIds.length > 0) { - for (var i = 0; i < groupIds.length; i++) { - group = this.groups[groupIds[i]]; - if (group.options.sampling == true) { - var dataContainer = groupsData[groupIds[i]]; - if (dataContainer.length > 0) { - var increment = 1; - var amountOfPoints = dataContainer.length; - // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop - // of width changing of the yAxis. - var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x); - var pointsPerPixel = amountOfPoints / xDistance; - increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel))); - var sampledData = []; - for (var j = 0; j < amountOfPoints; j += increment) { - sampledData.push(dataContainer[j]); - } - groupsData[groupIds[i]] = sampledData; - } - } - } - } - }; + var util = __webpack_require__(1); + var DataSet = __webpack_require__(3); + var DataView = __webpack_require__(4); + var Edge = _interopRequire(__webpack_require__(67)); - /** - * - * - * @param {array} groupIds - * @param {object} groupsData - * @param {object} groupRanges | this is being filled here - * @private - */ - LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { - var groupData, group, i; - var barCombinedDataLeft = []; - var barCombinedDataRight = []; - var options; - if (groupIds.length > 0) { - for (i = 0; i < groupIds.length; i++) { - groupData = groupsData[groupIds[i]]; - options = this.groups[groupIds[i]].options; - if (groupData.length > 0) { - group = this.groups[groupIds[i]]; - // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. - if (options.barChart.handleOverlap == "stack" && options.style == "bar") { - if (options.yAxisOrientation == "left") { - barCombinedDataLeft = barCombinedDataLeft.concat(group.getYRange(groupData)); - } else { - barCombinedDataRight = barCombinedDataRight.concat(group.getYRange(groupData)); - } - } else { - groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); - } - } - } + var EdgesHandler = (function () { + function EdgesHandler(body, images, groups) { + var _this = this; + _classCallCheck(this, EdgesHandler); - // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. - BarGraphFunctions.getStackedBarYRange(barCombinedDataLeft, groupRanges, groupIds, "__barchartLeft", "left"); - BarGraphFunctions.getStackedBarYRange(barCombinedDataRight, groupRanges, groupIds, "__barchartRight", "right"); - } - }; + this.body = body; + this.images = images; + this.groups = groups; + // create the edge API in the body container + this.body.functions.createEdge = this.create.bind(this); - /** - * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden. - * @param {Array} groupIds - * @param {Object} groupRanges - * @private - */ - LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) { - var resized = false; - var yAxisLeftUsed = false; - var yAxisRightUsed = false; - var minLeft = 1000000000, - minRight = 1000000000, - maxLeft = -1000000000, - maxRight = -1000000000, - minVal, - maxVal; - // if groups are present - if (groupIds.length > 0) { - // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop. - for (var i = 0; i < groupIds.length; i++) { - var group = this.groups[groupIds[i]]; - if (group && group.options.yAxisOrientation != "right") { - yAxisLeftUsed = true; - minLeft = 0; - maxLeft = 0; - } else if (group && group.options.yAxisOrientation) { - yAxisRightUsed = true; - minRight = 0; - maxRight = 0; + this.edgesListeners = { + add: function (event, params) { + _this.add(params.items); + }, + update: function (event, params) { + _this.update(params.items); + }, + remove: function (event, params) { + _this.remove(params.items); } - } - - // if there are items: - for (var i = 0; i < groupIds.length; i++) { - if (groupRanges.hasOwnProperty(groupIds[i])) { - if (groupRanges[groupIds[i]].ignore !== true) { - minVal = groupRanges[groupIds[i]].min; - maxVal = groupRanges[groupIds[i]].max; + }; - if (groupRanges[groupIds[i]].yAxisOrientation != "right") { - yAxisLeftUsed = true; - minLeft = minLeft > minVal ? minVal : minLeft; - maxLeft = maxLeft < maxVal ? maxVal : maxLeft; + this.options = {}; + this.defaultOptions = { + arrows: { + to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: { enabled: false, scaleFactor: 1 }, + from: { enabled: false, scaleFactor: 1 } + }, + color: { + color: "#848484", + highlight: "#848484", + hover: "#848484", + inherit: { + enabled: true, + source: "from", // from / true + useGradients: false // release in 4.0 + }, + opacity: 1 + }, + dashes: { + enabled: false, + preset: "dotted", + length: 10, + gap: 5, + altLength: undefined + }, + font: { + color: "#343434", + size: 14, // px + face: "arial", + background: "none", + stroke: 1, // px + strokeColor: "white", + align: "horizontal" + }, + hidden: false, + hoverWidth: 1.5, + label: undefined, + length: undefined, + physics: true, + scaling: { + min: 1, + max: 15, + label: { + enabled: true, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 3 + }, + customScalingFunction: function (min, max, total, value) { + if (max == min) { + return 0.5; } else { - yAxisRightUsed = true; - minRight = minRight > minVal ? minVal : minRight; - maxRight = maxRight < maxVal ? maxVal : maxRight; + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); } } - } - } + }, + selfReferenceSize: 20, + smooth: { + enabled: true, + dynamic: true, + type: "continuous", + roundness: 0.5 + }, + title: undefined, + width: 1, + widthSelectionMultiplier: 2, + value: 1 + }; - if (yAxisLeftUsed == true) { - this.yAxisLeft.setRange(minLeft, maxLeft); - } - if (yAxisRightUsed == true) { - this.yAxisRight.setRange(minRight, maxRight); - } - } - resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized; - resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized; + util.extend(this.options, this.defaultOptions); - if (yAxisRightUsed == true && yAxisLeftUsed == true) { - this.yAxisLeft.drawIcons = true; - this.yAxisRight.drawIcons = true; - } else { - this.yAxisLeft.drawIcons = false; - this.yAxisRight.drawIcons = false; - } - this.yAxisRight.master = !yAxisLeftUsed; - if (this.yAxisRight.master == false) { - if (yAxisRightUsed == true) { - this.yAxisLeft.lineOffset = this.yAxisRight.width; - } else { - this.yAxisLeft.lineOffset = 0; - } - resized = this.yAxisLeft.redraw() || resized; - this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels; - this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing; - resized = this.yAxisRight.redraw() || resized; - } else { - resized = this.yAxisRight.redraw() || resized; - } + // this allows external modules to force all dynamic curves to turn static. + this.body.emitter.on("_forceDisableDynamicCurves", function (type) { + var emitChange = false; + for (var edgeId in _this.body.edges) { + if (_this.body.edges.hasOwnProperty(edgeId)) { + var edgeOptions = _this.body.edges[edgeId].options.smooth; + if (edgeOptions.enabled === true && edgeOptions.dynamic === true) { + if (type === undefined) { + edge.setOptions({ smooth: false }); + } else { + edge.setOptions({ smooth: { dynamic: false, type: type } }); + } + emitChange = true; + } + } + } + if (emitChange === true) { + _this.body.emitter.emit("_dataChanged"); + } + }); - // 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); + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on("_dataUpdated", function () { + _this.reconnectEdges(); + _this.markAllEdgesAsDirty(); + }); } - return resized; - }; - + _prototypeProperties(EdgesHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.mergeOptions(this.options, options, "smooth"); + util.mergeOptions(this.options, options, "dashes"); - /** - * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function - * - * @param {boolean} axisUsed - * @returns {boolean} - * @private - * @param axis - */ - LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { - var changed = false; - if (axisUsed == false) { - if (axis.dom.frame.parentNode && axis.hidden == false) { - axis.hide(); - changed = true; - } - } else { - if (!axis.dom.frame.parentNode && axis.hidden == true) { - axis.show(); - changed = true; - } - } - return changed; - }; + // hanlde multiple input cases for arrows + if (options.arrows !== undefined) { + if (typeof options.arrows === "string") { + var arrows = options.arrows.toLowerCase(); + if (arrows.indexOf("to") != -1) { + this.options.arrows.to.enabled = true; + } + if (arrows.indexOf("middle") != -1) { + this.options.arrows.middle.enabled = true; + } + if (arrows.indexOf("from") != -1) { + this.options.arrows.from.enabled = true; + } + } else if (typeof options.arrows === "object") { + util.mergeOptions(this.options.arrows, options.arrows, "to"); + util.mergeOptions(this.options.arrows, options.arrows, "middle"); + util.mergeOptions(this.options.arrows, options.arrows, "from"); + } else { + throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); + } + } + // hanlde multiple input cases for color + if (options.color !== undefined) { + if (util.isString(options.color)) { + util.assignAllKeys(this.options.color, options.color); + this.options.color.inherit.enabled = false; + } else { + util.extend(this.options.color, options.color); + if (options.color.inherit === undefined) { + this.options.color.inherit.enabled = false; + } + } + util.mergeOptions(this.options.color, options.color, "inherit"); + } - /** - * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the - * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for - * the yAxis. - * - * @param datapoints - * @returns {Array} - * @private - */ - LineGraph.prototype._convertXcoordinates = function (datapoints) { - var extractedData = []; - var xValue, yValue; - var toScreen = this.body.util.toScreen; + // font cases are handled by the Label class + } + }, + writable: true, + configurable: true + }, + setData: { - for (var i = 0; i < datapoints.length; i++) { - xValue = toScreen(datapoints[i].x) + this.props.width; - yValue = datapoints[i].y; - extractedData.push({ x: xValue, y: yValue }); - } - return extractedData; - }; + /** + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private + * @private + */ + value: function setData(edges) { + var _this = this; + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var oldEdgesData = this.body.data.edges; + if (edges instanceof DataSet || edges instanceof DataView) { + this.body.data.edges = edges; + } else if (Array.isArray(edges)) { + this.body.data.edges = new DataSet(); + this.body.data.edges.add(edges); + } else if (!edges) { + this.body.data.edges = new DataSet(); + } else { + throw new TypeError("Array or DataSet expected"); + } - /** - * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the - * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for - * the yAxis. - * - * @param datapoints - * @param group - * @returns {Array} - * @private - */ - LineGraph.prototype._convertYcoordinates = function (datapoints, group) { - var extractedData = []; - var xValue, yValue; - var toScreen = this.body.util.toScreen; - var axis = this.yAxisLeft; - var svgHeight = Number(this.svg.style.height.replace("px", "")); - if (group.options.yAxisOrientation == "right") { - axis = this.yAxisRight; - } + // TODO: is this null or undefined or false? + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.off(event, callback); + }); + } - for (var i = 0; i < datapoints.length; i++) { - var labelValue; - //if (datapoints[i].label) { - // labelValue = datapoints[i].label; - //} - //else { - // labelValue = null; - //} - labelValue = datapoints[i].label ? datapoints[i].label : null; - xValue = toScreen(datapoints[i].x) + this.props.width; - yValue = Math.round(axis.convertValue(datapoints[i].y)); - extractedData.push({ x: xValue, y: yValue, label: labelValue }); - } + // remove drawn edges + this.body.edges = {}; - group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); + // TODO: is this null or undefined or false? + if (this.body.data.edges) { + // subscribe to new dataset + util.forEach(this.edgesListeners, function (callback, event) { + _this.body.data.edges.on(event, callback); + }); - return extractedData; - }; + // draw all new nodes + var ids = this.body.data.edges.getIds(); + this.add(ids, true); + } + if (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + add: { - module.exports = LineGraph; -/***/ }, -/* 46 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var edges = this.body.edges; + var edgesData = this.body.data.edges; - "use strict"; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(6); - var Component = __webpack_require__(25); - var DataStep = __webpack_require__(47); + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } - /** - * A horizontal time axis - * @param {Object} [options] See DataAxis.setOptions for the available - * options. - * @constructor DataAxis - * @extends Component - * @param body - */ - function DataAxis(body, options, svg, linegraphOptions) { - this.id = util.randomUUID(); - this.body = body; + var data = edgesData.get(id, { showInternalIds: true }); + edges[id] = this.create(data); + } - this.defaultOptions = { - orientation: "left", // supported: 'left', 'right' - showMinorLabels: true, - showMajorLabels: true, - icons: true, - majorLinesOffset: 7, - minorLinesOffset: 4, - labelOffsetX: 10, - labelOffsetY: 2, - iconWidth: 20, - width: "40px", - visible: true, - alignZeros: true, - customRange: { - left: { min: undefined, max: undefined }, - right: { min: undefined, max: undefined } - }, - title: { - left: { text: undefined }, - right: { text: undefined } + if (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true }, - format: { - left: { decimals: undefined }, - right: { decimals: undefined } - } - }; - - this.linegraphOptions = linegraphOptions; - this.linegraphSVG = svg; - this.props = {}; - this.DOMelements = { // dynamic elements - lines: {}, - labels: {}, - title: {} - }; + update: { - this.dom = {}; - this.range = { start: 0, end: 0 }; - this.options = util.extend({}, this.defaultOptions); - this.conversionFactor = 1; + /** + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + value: function update(ids) { + var edges = this.body.edges; + var edgesData = this.body.data.edges; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var data = edgesData.get(id); + var edge = edges[id]; + if (edge === null) { + // update edge + edge.disconnect(); + dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. + edge.connect(); + } else { + // create edge + this.body.edges[id] = this.create(data); + dataChanged = true; + } + } - this.setOptions(options); - this.width = Number(("" + this.options.width).replace("px", "")); - this.minWidth = this.width; - this.height = this.linegraphSVG.offsetHeight; - this.hidden = false; + if (dataChanged === true) { + this.body.emitter.emit("_dataChanged"); + } else { + this.body.emitter.emit("_dataUpdated"); + } + }, + writable: true, + configurable: true + }, + remove: { - this.stepPixels = 25; - this.stepPixelsForced = 25; - this.zeroCrossing = -1; - this.lineOffset = 0; - this.master = true; - this.svgElements = {}; - this.iconsRemoved = false; + /** + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var edges = this.body.edges; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge !== undefined) { + if (edge.via != null) { + delete this.body.supportNodes[edge.via.id]; + } + edge.disconnect(); + delete edges[id]; + } + } - this.groups = {}; - this.amountOfGroups = 0; + this.body.emitter.emit("_dataChanged"); + }, + writable: true, + configurable: true + }, + create: { + value: function create(properties) { + return new Edge(properties, this.body, this.options); + }, + writable: true, + configurable: true + }, + markAllEdgesAsDirty: { + value: function markAllEdgesAsDirty() { + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].colorDirty = true; + } + }, + writable: true, + configurable: true + }, + reconnectEdges: { - // create the HTML DOM - this._create(); - var me = this; - this.body.emitter.on("verticalDrag", function () { - me.dom.lineContainer.style.top = me.body.domProps.scrollTop + "px"; - }); - } - DataAxis.prototype = new Component(); + /** + * Reconnect all edges + * @private + */ + value: function reconnectEdges() { + var id; + var nodes = this.body.nodes; + var edges = this.body.edges; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } + } - DataAxis.prototype.addGroup = function (label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; - } - this.amountOfGroups += 1; - }; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } + } + }, + writable: true, + configurable: true + } + }); - DataAxis.prototype.updateGroup = function (label, graphOptions) { - this.groups[label] = graphOptions; - }; + return EdgesHandler; + })(); - DataAxis.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; - } - }; + module.exports = EdgesHandler; +/***/ }, +/* 53 */ +/***/ function(module, exports, __webpack_require__) { - DataAxis.prototype.setOptions = function (options) { - if (options) { - var redraw = false; - if (this.options.orientation != options.orientation && options.orientation !== undefined) { - redraw = true; - } - var fields = ["orientation", "showMinorLabels", "showMajorLabels", "icons", "majorLinesOffset", "minorLinesOffset", "labelOffsetX", "labelOffsetY", "iconWidth", "width", "visible", "customRange", "title", "format", "alignZeros"]; - util.selectiveExtend(fields, this.options, options); + "use strict"; - this.minWidth = Number(("" + this.options.width).replace("px", "")); + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - if (redraw == true && this.dom.frame) { - this.hide(); - this.show(); - } - } - }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Create the HTML DOM for the DataAxis + * Created by Alex on 2/23/2015. */ - 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; - - this.dom.lineContainer = document.createElement("div"); - this.dom.lineContainer.style.width = "100%"; - this.dom.lineContainer.style.height = this.height; - this.dom.lineContainer.style.position = "relative"; - - // create svg element for graph drawing. - this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - this.svg.style.position = "absolute"; - this.svg.style.top = "0px"; - this.svg.style.height = "100%"; - this.svg.style.width = "100%"; - this.svg.style.display = "block"; - this.dom.frame.appendChild(this.svg); - }; - DataAxis.prototype._redrawGroupIcons = function () { - DOMutil.prepareElements(this.svgElements); + var BarnesHutSolver = _interopRequire(__webpack_require__(68)); - var x; - var iconWidth = this.options.iconWidth; - var iconHeight = 15; - var iconOffset = 4; - var y = iconOffset + 0.5 * iconHeight; + var Repulsion = _interopRequire(__webpack_require__(69)); - if (this.options.orientation == "left") { - x = iconOffset; - } else { - x = this.width - iconWidth - iconOffset; - } + var HierarchicalRepulsion = _interopRequire(__webpack_require__(70)); - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); - y += iconHeight + iconOffset; - } - } - } + var SpringSolver = _interopRequire(__webpack_require__(71)); - DOMutil.cleanupElements(this.svgElements); - this.iconsRemoved = false; - }; + var HierarchicalSpringSolver = _interopRequire(__webpack_require__(72)); - DataAxis.prototype._cleanupIcons = function () { - if (this.iconsRemoved == false) { - DOMutil.prepareElements(this.svgElements); - DOMutil.cleanupElements(this.svgElements); - this.iconsRemoved = true; - } - }; + var CentralGravitySolver = _interopRequire(__webpack_require__(73)); - /** - * Create the HTML DOM for the DataAxis - */ - DataAxis.prototype.show = function () { - this.hidden = false; - if (!this.dom.frame.parentNode) { - if (this.options.orientation == "left") { - this.body.dom.left.appendChild(this.dom.frame); - } else { - this.body.dom.right.appendChild(this.dom.frame); - } - } + var util = __webpack_require__(1); - if (!this.dom.lineContainer.parentNode) { - this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); - } - }; - /** - * Create the HTML DOM for the DataAxis - */ - DataAxis.prototype.hide = function () { - this.hidden = true; - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + var PhysicsEngine = (function () { + function PhysicsEngine(body) { + var _this = this; + _classCallCheck(this, PhysicsEngine); - if (this.dom.lineContainer.parentNode) { - this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); - } - }; + this.body = body; + this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; - /** - * Set a range (start and end) - * @param end - * @param start - * @param end - */ - DataAxis.prototype.setRange = function (start, end) { - if (this.master == false && this.options.alignZeros == true && this.zeroCrossing != -1) { - if (start > 0) { - start = 0; - } - } - this.range.start = start; - this.range.end = end; - }; + this.physicsEnabled = true; + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.freezeCache = {}; + this.renderTimer == undefined; - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - DataAxis.prototype.redraw = function () { - var resized = false; - var activeGroups = 0; + this.stabilized = false; + this.stabilizationIterations = 0; + this.ready = false; // will be set to true if the stabilize - // Make sure the line container adheres to the vertical scrolling. - this.dom.lineContainer.style.top = this.body.domProps.scrollTop + "px"; + // default options + this.options = {}; + this.defaultOptions = { + barnesHut: { + thetaInverted: 1 / 0.5, // inverted to save time during calculation + gravitationalConstant: -2000, + centralGravity: 0.3, + springLength: 95, + springConstant: 0.04, + damping: 0.09 + }, + repulsion: { + centralGravity: 0, + springLength: 200, + springConstant: 0.05, + nodeDistance: 100, + damping: 0.09 + }, + hierarchicalRepulsion: { + centralGravity: 0, + springLength: 100, + springConstant: 0.01, + nodeDistance: 120, + damping: 0.09 + }, + solver: "BarnesHut", + timestep: 0.5, + maxVelocity: 50, + minVelocity: 0.1, // px/s + stabilization: { + enabled: true, + iterations: 1000, // maximum number of iteration to stabilize + updateInterval: 100, + onlyDynamicEdges: false, + zoomExtent: true + } + }; + util.extend(this.options, this.defaultOptions); - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - activeGroups++; + this.body.emitter.on("initPhysics", function () { + _this.initPhysics(); + }); + this.body.emitter.on("resetPhysics", function () { + _this.stopSimulation();_this.ready = false; + }); + this.body.emitter.on("disablePhysics", function () { + _this.physicsEnabled = false;_this.stopSimulation(); + }); + this.body.emitter.on("restorePhysics", function () { + _this.setOptions(_this.options); + if (_this.ready === true) { + _this.stabilized = false; + _this.runSimulation(); } - } + }); + this.body.emitter.on("startSimulation", function () { + if (_this.ready === true) { + _this.stabilized = false; + _this.runSimulation(); + } + }); + this.body.emitter.on("stopSimulation", function () { + _this.stopSimulation(); + }); } - if (this.amountOfGroups == 0 || activeGroups == 0) { - this.hide(); - } else { - this.show(); - this.height = Number(this.linegraphSVG.style.height.replace("px", "")); - - // svg offsetheight did not work in firefox and explorer... - this.dom.lineContainer.style.height = this.height + "px"; - this.width = this.options.visible == true ? Number(("" + this.options.width).replace("px", "")) : 0; - - var props = this.props; - var frame = this.dom.frame; - // update classname - frame.className = "dataaxis"; + _prototypeProperties(PhysicsEngine, null, { + setOptions: { + value: function setOptions(options) { + if (options === false) { + this.physicsEnabled = false; + this.stopSimulation(); + } else { + this.physicsEnabled = true; + if (options !== undefined) { + util.selectiveNotDeepExtend(["stabilization"], this.options, options); + util.mergeOptions(this.options, options, "stabilization"); + } + this.init(); + } + }, + writable: true, + configurable: true + }, + init: { + value: function init() { + var options; + if (this.options.solver == "repulsion") { + options = this.options.repulsion; + this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } else if (this.options.solver == "hierarchicalRepulsion") { + options = this.options.hierarchicalRepulsion; + this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); + this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); + } else { + // barnesHut + options = this.options.barnesHut; + this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } - // calculate character width and height - this._calculateCharSize(); + this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); + this.modelOptions = options; + }, + writable: true, + configurable: true + }, + initPhysics: { + value: function initPhysics() { + if (this.physicsEnabled === true) { + this.stabilized = false; + if (this.options.stabilization.enabled === true) { + this.stabilize(); + } else { + this.ready = true; + this.body.emitter.emit("zoomExtent", { duration: 0 }, true); + this.runSimulation(); + } + } else { + this.ready = true; + this.body.emitter.emit("_redraw"); + } + }, + writable: true, + configurable: true + }, + stopSimulation: { + value: function stopSimulation() { + this.stabilized = true; + if (this.viewFunction !== undefined) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.viewFunction = undefined; + this.body.emitter.emit("_stopRendering"); + } + }, + writable: true, + configurable: true + }, + runSimulation: { + value: function runSimulation() { + if (this.physicsEnabled === true) { + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + } else { + this.body.emitter.emit("_redraw"); + } + }, + writable: true, + configurable: true + }, + simulationStep: { + value: function simulationStep() { + // check if the physics have settled + var startTime = Date.now(); + this.physicsTick(); + var physicsTime = Date.now() - startTime; - var orientation = this.options.orientation; - var showMinorLabels = this.options.showMinorLabels; - var showMajorLabels = this.options.showMajorLabels; + // run double speed if it is a little graph + if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { + this.physicsTick(); - // determine the width and height of the elements for the axis - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + // this makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } - props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset; - props.minorLineHeight = 1; - props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset; - props.majorLineHeight = 1; + if (this.stabilized === true) { + if (this.stabilizationIterations > 1) { + // trigger the "stabilized" event. + // The event is triggered on the next tick, to prevent the case that + // it is fired while initializing the Network, in which case you would not + // be able to catch it + var me = this; + var params = { + iterations: this.stabilizationIterations + }; + this.stabilizationIterations = 0; + this.startedStabilization = false; + setTimeout(function () { + me.body.emitter.emit("stabilized", params); + }, 0); + } else { + this.stabilizationIterations = 0; + } + this.stopSimulation(); + } + }, + writable: true, + configurable: true + }, + physicsTick: { - // take frame offline while updating (is almost twice as fast) - if (orientation == "left") { - frame.style.top = "0"; - frame.style.left = "0"; - frame.style.bottom = ""; - frame.style.width = this.width + "px"; - frame.style.height = this.height + "px"; - this.props.width = this.body.domProps.left.width; - this.props.height = this.body.domProps.left.height; - } else { - // right - frame.style.top = ""; - frame.style.bottom = "0"; - frame.style.left = "0"; - frame.style.width = this.width + "px"; - frame.style.height = this.height + "px"; - this.props.width = this.body.domProps.right.width; - this.props.height = this.body.domProps.right.height; - } + /** + * A single simulation step (or "tick") in the physics simulation + * + * @private + */ + value: function physicsTick() { + if (this.stabilized === false) { + this.calculateForces(); + this.stabilized = this.moveNodes(); - resized = this._redrawLabels(); - resized = this._isResized() || resized; + // determine if the network has stabilzied + if (this.stabilized === true) { + this.revert(); + } else { + // this is here to ensure that there is no start event when the network is already stable. + if (this.startedStabilization == false) { + this.body.emitter.emit("startStabilizing"); + this.startedStabilization = true; + } + } - if (this.options.icons == true) { - this._redrawGroupIcons(); - } else { - this._cleanupIcons(); - } + this.stabilizationIterations++; + } + }, + writable: true, + configurable: true + }, + updatePhysicsIndices: { - this._redrawTitle(orientation); - } - return resized; - }; + /** + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.body.nodes with the support nodes. + * + * @private + */ + value: function updatePhysicsIndices() { + this.physicsBody.forces = {}; + this.physicsBody.physicsNodeIndices = []; + this.physicsBody.physicsEdgeIndices = []; + var nodes = this.body.nodes; + var edges = this.body.edges; - /** - * Repaint major and minor text labels and vertical grid lines - * @private - */ - DataAxis.prototype._redrawLabels = function () { - var resized = false; - DOMutil.prepareElements(this.DOMelements.lines); - DOMutil.prepareElements(this.DOMelements.labels); + // get node indices for physics + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].options.physics === true) { + this.physicsBody.physicsNodeIndices.push(nodeId); + } + } + } - var orientation = this.options.orientation; + // get edge indices for physics + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].options.physics === true) { + this.physicsBody.physicsEdgeIndices.push(edgeId); + } + } + } - // 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; + // get the velocity and the forces vector + for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { + var nodeId = this.physicsBody.physicsNodeIndices[i]; + this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; - var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation], this.master == false && this.options.alignZeros // doess the step have to align zeros? only if not master and the options is on - ); + // forces can be reset because they are recalculated. Velocities have to persist. + if (this.physicsBody.velocities[nodeId] === undefined) { + this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; + } + } - 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); + // clean deleted nodes from the velocity vector + for (var nodeId in this.physicsBody.velocities) { + if (nodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } + } + }, + writable: true, + configurable: true + }, + revert: { + value: function revert() { + var nodeIds = Object.keys(this.previousStates); + var nodes = this.body.nodes; + var velocities = this.physicsBody.velocities; - this.stepPixels = stepPixels; + for (var i = 0; i < nodeIds.length; i++) { + var nodeId = nodeIds[i]; + if (nodes[nodeId] !== undefined) { + velocities[nodeId].x = this.previousStates[nodeId].vx; + velocities[nodeId].y = this.previousStates[nodeId].vy; + nodes[nodeId].x = this.previousStates[nodeId].x; + nodes[nodeId].y = this.previousStates[nodeId].y; + } else { + delete this.previousStates[nodeId]; + } + } + }, + writable: true, + configurable: true + }, + moveNodes: { + value: function moveNodes() { + var nodesPresent = false; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; + var stabilized = true; + var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); - var amountOfSteps = this.height / stepPixels; - var stepDifference = 0; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + var nodeVelocity = this._performStep(nodeId, maxVelocity); + // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized + stabilized = nodeVelocity < vminCorrected && stabilized === true; + nodesPresent = true; + } - // the slave axis needs to use the same horizontal lines as the master axis. - 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; - if (this.zeroCrossing != -1 && this.options.alignZeros == true) { - var zeroStepDifference = step.marginEnd / step.step - this.zeroCrossing; - if (zeroStepDifference > 0) { - for (var i = 0; i < zeroStepDifference; i++) { - step.next(); + if (nodesPresent == true) { + if (vminCorrected > 0.5 * this.options.maxVelocity) { + return false; + } else { + return stabilized; + } } - } else if (zeroStepDifference < 0) { - for (var i = 0; i < -zeroStepDifference; i++) { - step.previous(); + return true; + }, + writable: true, + configurable: true + }, + _performStep: { + value: function _performStep(nodeId, maxVelocity) { + var node = this.body.nodes[nodeId]; + var timestep = this.options.timestep; + var forces = this.physicsBody.forces; + var velocities = this.physicsBody.velocities; + + // store the state so we can revert + this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; + + if (node.options.fixed.x === false) { + var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force + var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration + velocities[nodeId].x += ax * timestep; // velocity + velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; + node.x += velocities[nodeId].x * timestep; // position + } else { + forces[nodeId].x = 0; + velocities[nodeId].x = 0; } - } - } - } else { - amountOfSteps += 0.25; - } + if (node.options.fixed.y === false) { + var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force + var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration + velocities[nodeId].y += ay * timestep; // velocity + velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; + node.y += velocities[nodeId].y * timestep; // position + } else { + forces[nodeId].y = 0; + velocities[nodeId].y = 0; + } - this.valueAtZero = step.marginEnd; - var marginStartPos = 0; + var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); + return totalVelocity; + }, + writable: true, + configurable: true + }, + calculateForces: { + value: function calculateForces() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); + }, + writable: true, + configurable: true + }, + _freezeNodes: { - // do not draw the first label - var max = 1; - // Get the number of decimal places - var decimals; - if (this.options.format[orientation] !== undefined) { - decimals = this.options.format[orientation].decimals; - } - this.maxLabelSize = 0; - var y = 0; - while (max < Math.round(amountOfSteps)) { - step.next(); - y = Math.round(max * stepPixels); - marginStartPos = max * stepPixels; - var isMajor = step.isMajor(); - if (this.options.showMinorLabels && isMajor == false || this.master == false && this.options.showMinorLabels == true) { - this._redrawLabel(y - 2, step.getCurrent(decimals), orientation, "yAxis minor", this.props.minorCharHeight); - } - 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(decimals), orientation, "yAxis major", this.props.majorCharHeight); - } - 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); - } - if (this.master == true && step.current == 0) { - this.zeroCrossing = max; - } - max++; - } - if (this.master == false) { - this.conversionFactor = y / (this.valueAtZero - step.current); - } else { - this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; - } - // Note that title is rotated, so we're using the height, not width! - var titleWidth = 0; - if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) { - titleWidth = this.props.titleCharHeight; - } - var offset = this.options.icons == true ? Math.max(this.options.iconWidth, titleWidth) + this.options.labelOffsetX + 15 : titleWidth + this.options.labelOffsetX + 15; + /** + * 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 + */ + value: function _freezeNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].x && nodes[id].y) { + this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; + nodes[id].options.fixed.x = true; + nodes[id].options.fixed.y = true; + } + } + } + }, + writable: true, + configurable: true + }, + _restoreFrozenNodes: { - // this will resize the yAxis to accommodate the labels. - if (this.maxLabelSize > this.width - offset && this.options.visible == true) { - this.width = this.maxLabelSize + offset; - this.options.width = this.width + "px"; - DOMutil.cleanupElements(this.DOMelements.lines); - DOMutil.cleanupElements(this.DOMelements.labels); - this.redraw(); - resized = true; - } - // this will resize the yAxis if it is too big for the labels. - else if (this.maxLabelSize < this.width - offset && this.options.visible == true && this.width > this.minWidth) { - this.width = Math.max(this.minWidth, this.maxLabelSize + offset); - this.options.width = this.width + "px"; - DOMutil.cleanupElements(this.DOMelements.lines); - DOMutil.cleanupElements(this.DOMelements.labels); - this.redraw(); - resized = true; - } else { - DOMutil.cleanupElements(this.DOMelements.lines); - DOMutil.cleanupElements(this.DOMelements.labels); - resized = false; - } + /** + * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. + * + * @private + */ + value: function _restoreFrozenNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (this.freezeCache[id] !== undefined) { + nodes[id].options.fixed.x = this.freezeCache[id].x; + nodes[id].options.fixed.y = this.freezeCache[id].y; + } + } + } + this.freezeCache = {}; + }, + writable: true, + configurable: true + }, + stabilize: { - return resized; - }; + /** + * Find a stable position for all nodes + * @private + */ + value: function stabilize() { + if (this.options.stabilization.onlyDynamicEdges == true) { + this._freezeNodes(); + } + this.stabilizationSteps = 0; - DataAxis.prototype.convertValue = function (value) { - var invertedValue = this.valueAtZero - value; - var convertedValue = invertedValue * this.conversionFactor; - return convertedValue; - }; + setTimeout(this._stabilizationBatch.bind(this), 0); + }, + writable: true, + configurable: true + }, + _stabilizationBatch: { + value: function _stabilizationBatch() { + var count = 0; + while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { + this.physicsTick(); + this.stabilizationSteps++; + count++; + } - /** - * Create a label for the axis at position x - * @private - * @param y - * @param text - * @param orientation - * @param className - * @param characterHeight - */ - DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) { - // reuse redundant label - var label = DOMutil.getDOMElement("div", this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift(); - label.className = className; - label.innerHTML = text; - if (orientation == "left") { - label.style.left = "-" + this.options.labelOffsetX + "px"; - label.style.textAlign = "right"; - } else { - label.style.right = "-" + this.options.labelOffsetX + "px"; - label.style.textAlign = "left"; - } + if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) { + this.body.emitter.emit("stabilizationProgress", { steps: this.stabilizationSteps, total: this.options.stabilization.iterations }); + setTimeout(this._stabilizationBatch.bind(this), 0); + } else { + this._finalizeStabilization(); + } + }, + writable: true, + configurable: true + }, + _finalizeStabilization: { + value: function _finalizeStabilization() { + if (this.options.stabilization.zoomExtent == true) { + this.body.emitter.emit("zoomExtent", { duration: 0 }); + } - label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + "px"; + if (this.options.stabilization.onlyDynamicEdges == true) { + this._restoreFrozenNodes(); + } - text += ""; + this.body.emitter.emit("stabilizationIterationsDone"); + this.body.emitter.emit("_requestRedraw"); + this.ready = true; + }, + writable: true, + configurable: true + } + }); - var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); - if (this.maxLabelSize < text.length * largestWidth) { - this.maxLabelSize = text.length * largestWidth; - } - }; + return PhysicsEngine; + })(); - /** - * 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 = ""; + module.exports = PhysicsEngine; - if (orientation == "left") { - line.style.left = this.width - offset + "px"; - } else { - line.style.right = this.width - offset + "px"; - } +/***/ }, +/* 54 */ +/***/ function(module, exports, __webpack_require__) { - line.style.width = width + "px"; - line.style.top = y + "px"; - } - }; + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Create a title for the axis - * @private - * @param orientation + * Created by Alex on 24-Feb-15. */ - DataAxis.prototype._redrawTitle = function (orientation) { - DOMutil.prepareElements(this.DOMelements.title); - - // Check if the title is defined for this axes - if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) { - var title = DOMutil.getDOMElement("div", this.DOMelements.title, this.dom.frame); - title.className = "yAxis title " + orientation; - title.innerHTML = this.options.title[orientation].text; - // Add style - if provided - if (this.options.title[orientation].style !== undefined) { - util.addCssText(title, this.options.title[orientation].style); - } + var util = __webpack_require__(1); + var Cluster = _interopRequire(__webpack_require__(74)); - if (orientation == "left") { - title.style.left = this.props.titleCharHeight + "px"; - } else { - title.style.right = this.props.titleCharHeight + "px"; - } + var ClusterEngine = (function () { + function ClusterEngine(body) { + _classCallCheck(this, ClusterEngine); - title.style.width = this.height + "px"; + this.body = body; + this.clusteredNodes = {}; } - // we need to clean up in case we did not use all elements. - DOMutil.cleanupElements(this.DOMelements.title); - }; + _prototypeProperties(ClusterEngine, null, { + setOptions: { + value: function setOptions(options) {}, + writable: true, + configurable: true + }, + clusterByConnectionCount: { + /** + * + * @param hubsize + * @param options + */ + value: function clusterByConnectionCount(hubsize, options) { + if (hubsize === undefined) { + hubsize = this._getHubSize(); + } else if (tyepof(hubsize) == "object") { + options = this._checkOptions(hubsize); + hubsize = this._getHubSize(); + } + var nodesToCluster = []; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length >= hubsize) { + nodesToCluster.push(node.id); + } + } + for (var i = 0; i < nodesToCluster.length; i++) { + var node = this.body.nodes[nodesToCluster[i]]; + this.clusterByConnection(node, options, {}, {}, false); + } + this.body.emitter.emit("_dataChanged"); + }, + writable: true, + configurable: true + }, + clusterByNodeData: { - /** - * Determine the size of text on the axis (both major and minor axis). - * The size is calculated only once and then cached in this.props. - * @private - */ - DataAxis.prototype._calculateCharSize = function () { - // determine the char width and height on the minor axis - if (!("minorCharHeight" in this.props)) { - var textMinor = document.createTextNode("0"); - var measureCharMinor = document.createElement("div"); - measureCharMinor.className = "yAxis minor measure"; - measureCharMinor.appendChild(textMinor); - this.dom.frame.appendChild(measureCharMinor); - this.props.minorCharHeight = measureCharMinor.clientHeight; - this.props.minorCharWidth = measureCharMinor.clientWidth; + /** + * loop over all nodes, check if they adhere to the condition and cluster if needed. + * @param options + * @param refreshData + */ + value: function clusterByNodeData() { + var options = arguments[0] === undefined ? {} : arguments[0]; + var refreshData = arguments[1] === undefined ? true : arguments[1]; + if (options.joinCondition === undefined) { + throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); + } - this.dom.frame.removeChild(measureCharMinor); - } + // check if the options object is fine, append if needed + options = this._checkOptions(options); - 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); + var childNodesObj = {}; + var childEdgesObj = {}; - this.props.majorCharHeight = measureCharMajor.clientHeight; - this.props.majorCharWidth = measureCharMajor.clientWidth; + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + } - this.dom.frame.removeChild(measureCharMajor); - } + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + }, + writable: true, + configurable: true + }, + clusterOutliers: { - if (!("titleCharHeight" in this.props)) { - var textTitle = document.createTextNode("0"); - var measureCharTitle = document.createElement("div"); - measureCharTitle.className = "yAxis title measure"; - measureCharTitle.appendChild(textTitle); - this.dom.frame.appendChild(measureCharTitle); - this.props.titleCharHeight = measureCharTitle.clientHeight; - this.props.titleCharWidth = measureCharTitle.clientWidth; + /** + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param refreshData + */ + value: function clusterOutliers(options) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + options = this._checkOptions(options); + var clusters = []; - this.dom.frame.removeChild(measureCharTitle); - } - }; + // collect the nodes that will be in the cluster + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var childNodesObj = {}; + var childEdgesObj = {}; + var nodeId = this.body.nodeIndices[i]; + if (this.body.nodes[nodeId].edges.length == 1) { + var edge = this.body.nodes[nodeId].edges[0]; + var childNodeId = this._getConnectedId(edge, nodeId); + if (childNodeId != nodeId) { + if (options.joinCondition === undefined) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + var clonedOptions = this._cloneOptions(nodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[nodeId] = this.body.nodes[nodeId]; + } + clonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(clonedOptions) == true) { + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); + } + } + } - module.exports = DataAxis; + for (var i = 0; i < clusters.length; i++) { + this._cluster(clusters[i].nodes, clusters[i].edges, options, false); + } -/***/ }, -/* 47 */ -/***/ function(module, exports, __webpack_require__) { + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + clusterByConnection: { - "use strict"; + /** + * + * @param nodeId + * @param options + * @param refreshData + */ + value: function clusterByConnection(nodeId, options) { + var refreshData = arguments[2] === undefined ? true : arguments[2]; + // kill conditions + if (nodeId === undefined) { + throw new Error("No nodeId supplied to clusterByConnection!"); + } + if (this.body.nodes[nodeId] === undefined) { + throw new Error("The nodeId given to clusterByConnection does not exist!"); + } - /** - * @constructor DataStep - * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an - * end data point. The class itself determines the best scale (step size) based on the - * provided start Date, end Date, and minimumStep. - * - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * - * Alternatively, you can set a scale by hand. - * After creation, you can initialize the class by executing first(). Then you - * can iterate from the start date to the end date via next(). You can check if - * the end date is reached with the function hasNext(). After each step, you can - * retrieve the current date via getCurrent(). - * The DataStep has scales ranging from milliseconds, seconds, minutes, hours, - * days, to years. - * - * Version: 1.2 - * - * @param {Date} [start] The start date, for example new Date(2010, 9, 21) - * or new Date(2010, 9, 21, 23, 45, 00) - * @param {Date} [end] The end date - * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds - */ - function DataStep(start, end, minimumStep, containerHeight, customRange, alignZeros) { - // variables - this.current = 0; + var node = this.body.nodes[nodeId]; + options = this._checkOptions(options, node); + if (options.clusterNodeProperties.x === undefined) { + options.clusterNodeProperties.x = node.x; + } + if (options.clusterNodeProperties.y === undefined) { + options.clusterNodeProperties.y = node.y; + } + if (options.clusterNodeProperties.fixed === undefined) { + options.clusterNodeProperties.fixed = {}; + options.clusterNodeProperties.fixed.x = node.options.fixed.x; + options.clusterNodeProperties.fixed.y = node.options.fixed.y; + } - this.autoScale = true; - this.stepIndex = 0; - this.step = 1; - this.scale = 1; - this.marginStart; - this.marginEnd; - this.deadSpace = 0; + var childNodesObj = {}; + var childEdgesObj = {}; + var parentNodeId = node.id; + var parentClonedOptions = this._cloneOptions(parentNodeId); + childNodesObj[parentNodeId] = node; - this.majorSteps = [1, 2, 5, 10]; - this.minorSteps = [0.25, 0.5, 1, 2]; + // collect the nodes that will be in the cluster + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + var childNodeId = this._getConnectedId(edge, parentNodeId); - this.alignZeros = alignZeros; + if (childNodeId !== parentNodeId) { + if (options.joinCondition === undefined) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } else { + // clone the options and insert some additional parameters that could be interesting. + var childClonedOptions = this._cloneOptions(childNodeId); + if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { + childEdgesObj[edge.id] = edge; + childNodesObj[childNodeId] = this.body.nodes[childNodeId]; + } + } + } else { + childEdgesObj[edge.id] = edge; + } + } - this.setRange(start, end, minimumStep, containerHeight, customRange); - } + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + }, + writable: true, + configurable: true + }, + _cloneOptions: { + /** + * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. + * @param objId + * @param type + * @returns {{}} + * @private + */ + value: function _cloneOptions(objId, type) { + var clonedOptions = {}; + if (type === undefined || type == "node") { + util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); + util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); + clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; + } else { + util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); + } + return clonedOptions; + }, + writable: true, + configurable: true + }, + _createClusterEdges: { - /** - * Set a new range - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * @param {Number} [start] The start date and time. - * @param {Number} [end] The end date and time. - * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds - */ - DataStep.prototype.setRange = function (start, end, minimumStep, containerHeight, customRange) { - this._start = customRange.min === undefined ? start : customRange.min; - this._end = customRange.max === undefined ? end : customRange.max; - if (this._start == this._end) { - this._start -= 0.75; - this._end += 1; - } + /** + * This function creates the edges that will be attached to the cluster. + * + * @param childNodesObj + * @param childEdgesObj + * @param newEdges + * @param options + * @private + */ + value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { + var edge, childNodeId, childNode; - if (this.autoScale == true) { - this.setMinimumStep(minimumStep, containerHeight); - } + var childKeys = Object.keys(childNodesObj); + for (var i = 0; i < childKeys.length; i++) { + childNodeId = childKeys[i]; + childNode = childNodesObj[childNodeId]; - this.setFirst(customRange); - }; + // mark all edges for removal from global and construct new edges from the cluster to others + for (var j = 0; j < childNode.edges.length; j++) { + edge = childNode.edges[j]; + childEdgesObj[edge.id] = edge; - /** - * 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 otherNodeId = edge.toId; + var otherOnTo = true; + if (edge.toId != childNodeId) { + otherNodeId = edge.toId; + otherOnTo = true; + } else if (edge.fromId != childNodeId) { + otherNodeId = edge.fromId; + otherOnTo = false; + } - var minorStepIdx = -1; - var magnitudefactor = Math.pow(10, orderOfMagnitude); + if (childNodesObj[otherNodeId] === undefined) { + var clonedOptions = this._cloneOptions(edge.id, "edge"); + util.deepExtend(clonedOptions, options.clusterEdgeProperties); + if (otherOnTo === true) { + clonedOptions.from = options.clusterNodeProperties.id; + clonedOptions.to = otherNodeId; + } else { + clonedOptions.from = otherNodeId; + clonedOptions.to = options.clusterNodeProperties.id; + } + clonedOptions.id = "clusterEdge:" + util.randomUUID(); + newEdges.push(this.body.functions.createEdge(clonedOptions)); + } + } + } + }, + writable: true, + configurable: true + }, + _checkOptions: { - var start = 0; - if (orderOfMagnitude < 0) { - start = orderOfMagnitude; - } - var solutionFound = false; - for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { - magnitudefactor = Math.pow(10, i); - for (var j = 0; j < this.minorSteps.length; j++) { - var stepSize = magnitudefactor * this.minorSteps[j]; - if (stepSize >= minimumStepValue) { - solutionFound = true; - minorStepIdx = j; - break; - } - } - if (solutionFound == true) { - break; - } - } - this.stepIndex = minorStepIdx; - this.scale = magnitudefactor; - this.step = magnitudefactor * this.minorSteps[minorStepIdx]; - }; + /** + * This function checks the options that can be supplied to the different cluster functions + * for certain fields and inserts defaults if needed + * @param options + * @returns {*} + * @private + */ + value: function _checkOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + if (options.clusterEdgeProperties === undefined) { + options.clusterEdgeProperties = {}; + } + if (options.clusterNodeProperties === undefined) { + options.clusterNodeProperties = {}; + } + return options; + }, + writable: true, + configurable: true + }, + _cluster: { + /** + * + * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node + * @param {Object} childEdgesObj | object with edge objects, id as keys + * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} + * @param {Boolean} refreshData | when true, do not wrap up + * @private + */ + value: function _cluster(childNodesObj, childEdgesObj, options) { + var refreshData = arguments[3] === undefined ? true : arguments[3]; + // kill condition: no children so cant cluster + if (Object.keys(childNodesObj).length == 0) { + return; + } - /** - * 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 = {}; - } + // check if we have an unique id; + if (options.clusterNodeProperties.id === undefined) { + options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); + } + var clusterId = options.clusterNodeProperties.id; - 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; + // create the new edges that will connect to the cluster + var newEdges = []; + this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); - this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; - this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; + // construct the clusterNodeProperties + var clusterNodeProperties = options.clusterNodeProperties; + if (options.processProperties !== undefined) { + // get the childNode options + var childNodesOptions = []; + for (var nodeId in childNodesObj) { + var clonedOptions = this._cloneOptions(nodeId); + childNodesOptions.push(clonedOptions); + } - // if we need to align the zero's we need to make sure that there is a zero to use. - if (this.alignZeros == true && (this.marginEnd - this.marginStart) % this.step != 0) { - this.marginEnd += this.marginEnd % this.step; - } + // get clusterproperties based on childNodes + var childEdgesOptions = []; + for (var edgeId in childEdgesObj) { + var clonedOptions = this._cloneOptions(edgeId, "edge"); + childEdgesOptions.push(clonedOptions); + } - this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; - this.marginRange = this.marginEnd - this.marginStart; + clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); + if (!clusterNodeProperties) { + throw new Error("The processClusterProperties function does not return properties!"); + } + } + if (clusterNodeProperties.label === undefined) { + clusterNodeProperties.label = "cluster"; + } - this.current = this.marginEnd; - }; + // give the clusterNode a postion if it does not have one. + var pos = undefined; + if (clusterNodeProperties.x === undefined) { + pos = this._getClusterPosition(childNodesObj); + clusterNodeProperties.x = pos.x; + clusterNodeProperties.allowedToMoveX = true; + } + if (clusterNodeProperties.x === undefined) { + if (pos === undefined) { + pos = this._getClusterPosition(childNodesObj); + } + clusterNodeProperties.y = pos.y; + clusterNodeProperties.allowedToMoveY = true; + } - DataStep.prototype.roundToMinor = function (value) { - var rounded = value - value % (this.scale * this.minorSteps[this.stepIndex]); - if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) { - return rounded + this.scale * this.minorSteps[this.stepIndex]; - } else { - return rounded; - } - }; + // force the ID to remain the same + clusterNodeProperties.id = clusterId; - /** - * 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; + // create the clusterNode + var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); + clusterNode.isCluster = true; + clusterNode.containedNodes = childNodesObj; + clusterNode.containedEdges = childEdgesObj; - // safety mechanism: if current time is still unchanged, move to the end - if (this.current == prev) { - this.current = this._end; - } - }; - /** - * Do the next step - */ - DataStep.prototype.previous = function () { - this.current += this.step; - this.marginEnd += this.step; - this.marginRange = this.marginEnd - this.marginStart; - }; + // disable the childEdges + for (var edgeId in childEdgesObj) { + if (childEdgesObj.hasOwnProperty(edgeId)) { + if (this.body.edges[edgeId] !== undefined) { + var edge = this.body.edges[edgeId]; + edge.togglePhysics(false); + edge.options.hidden = true; + } + } + } + // disable the childNodes + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; + this.body.nodes[nodeId].togglePhysics(false); + this.body.nodes[nodeId].options.hidden = true; + } + } - /** - * Get the current datetime - * @return {String} current The current date - */ - DataStep.prototype.getCurrent = function (decimals) { - // prevent round-off errors when close to zero - var current = Math.abs(this.current) < this.step / 2 ? 0 : this.current; - var toPrecision = "" + Number(current).toPrecision(5); - // If decimals is specified, then limit or extend the string as required - if (decimals !== undefined && !isNaN(Number(decimals))) { - // If string includes exponent, then we need to add it to the end - var exp = ""; - var index = toPrecision.indexOf("e"); - if (index != -1) { - // Get the exponent - exp = toPrecision.slice(index); - // Remove the exponent in case we need to zero-extend - toPrecision = toPrecision.slice(0, index); - } - index = Math.max(toPrecision.indexOf(","), toPrecision.indexOf(".")); - if (index === -1) { - // No decimal found - if we want decimals, then we need to add it - if (decimals !== 0) { - toPrecision += "."; - } - // Calculate how long the string should be - index = toPrecision.length + decimals; - } else if (decimals !== 0) { - // Calculate how long the string should be - accounting for the decimal place - index += decimals + 1; - } - if (index > toPrecision.length) { - // We need to add zeros! - for (var cnt = index - toPrecision.length; cnt > 0; cnt--) { - toPrecision += "0"; - } - } else { - // we need to remove characters - toPrecision = toPrecision.slice(0, index); - } - // Add the exponent if there is one - toPrecision += exp; - } else { - if (toPrecision.indexOf(",") != -1 || toPrecision.indexOf(".") != -1) { - // If no decimal is specified, and there are decimal places, remove trailing zeros - for (var i = toPrecision.length - 1; i > 0; i--) { - if (toPrecision[i] == "0") { - toPrecision = toPrecision.slice(0, i); - } else if (toPrecision[i] == "." || toPrecision[i] == ",") { - toPrecision = toPrecision.slice(0, i); - break; - } else { - break; + // finally put the cluster node into global + this.body.nodes[clusterNodeProperties.id] = clusterNode; + + + // push new edges to global + for (var i = 0; i < newEdges.length; i++) { + this.body.edges[newEdges[i].id] = newEdges[i]; + this.body.edges[newEdges[i].id].connect(); } - } - } - } - return toPrecision; - }; + // set ID to undefined so no duplicates arise + clusterNodeProperties.id = undefined; - /** - * Check if the current value is a major value (for example when the step - * is DAY, a major value is each first day of the MONTH) - * @return {boolean} true if current date is major, else false. - */ - DataStep.prototype.isMajor = function () { - return this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0; - }; - module.exports = DataStep; + // wrap up + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + isCluster: { -/***/ }, -/* 48 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + /** + * Check if a node is a cluster. + * @param nodeId + * @returns {*} + */ + value: function isCluster(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster === true; + } else { + console.log("Node does not exist."); + return false; + } + }, + writable: true, + configurable: true + }, + _getClusterPosition: { - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(6); - var Line = __webpack_require__(49); - var Bar = __webpack_require__(51); - var Points = __webpack_require__(50); + /** + * get the position of the cluster node based on what's inside + * @param {object} childNodesObj | object with node objects, id as keys + * @returns {{x: number, y: number}} + * @private + */ + value: function _getClusterPosition(childNodesObj) { + var childKeys = Object.keys(childNodesObj); + var minX = childNodesObj[childKeys[0]].x; + var maxX = childNodesObj[childKeys[0]].x; + var minY = childNodesObj[childKeys[0]].y; + var maxY = childNodesObj[childKeys[0]].y; + var node; + for (var i = 0; i < childKeys.lenght; i++) { + node = childNodesObj[childKeys[0]]; + minX = node.x < minX ? node.x : minX; + maxX = node.x > maxX ? node.x : maxX; + minY = node.y < minY ? node.y : minY; + maxY = node.y > maxY ? node.y : maxY; + } + return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; + }, + writable: true, + configurable: true + }, + openCluster: { - /** - * /** - * @param {object} group | the object of the group from the dataset - * @param {string} groupId | ID of the group - * @param {object} options | the default options - * @param {array} groupsUsingDefaultStyles | this array has one entree. - * It is passed as an array so it is passed by reference. - * It enumerates through the default styles - * @constructor - */ - function GraphGroup(group, groupId, options, groupsUsingDefaultStyles) { - this.id = groupId; - var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart", "drawPoints", "shaded", "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; - } + /** + * Open a cluster by calling this function. + * @param {String} clusterNodeId | the ID of the cluster node + * @param {Boolean} refreshData | wrap up afterwards if not true + */ + value: function openCluster(clusterNodeId) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + // kill conditions + if (clusterNodeId === undefined) { + throw new Error("No clusterNodeId supplied to openCluster."); + } + if (this.body.nodes[clusterNodeId] === undefined) { + throw new Error("The clusterNodeId supplied to openCluster does not exist."); + } + if (this.body.nodes[clusterNodeId].containedNodes === undefined) { + console.log("The node:" + clusterNodeId + " is not a cluster.");return; + }; - /** - * this loads a reference to all items in this group into this group. - * @param {array} items - */ - GraphGroup.prototype.setItems = function (items) { - if (items != null) { - this.itemsData = items; - if (this.options.sort == true) { - this.itemsData.sort(function (a, b) { - return a.x - b.x; - }); - } - } else { - this.itemsData = []; - } - }; + var clusterNode = this.body.nodes[clusterNodeId]; + var containedNodes = clusterNode.containedNodes; + var containedEdges = clusterNode.containedEdges; + // release nodes + for (var nodeId in containedNodes) { + if (containedNodes.hasOwnProperty(nodeId)) { + var containedNode = this.body.nodes[nodeId]; + containedNode = containedNodes[nodeId]; + // inherit position + containedNode.x = clusterNode.x; + containedNode.y = clusterNode.y; - /** - * this is used for plotting barcharts, this way, we only have to calculate it once. - * @param pos - */ - GraphGroup.prototype.setZeroPosition = function (pos) { - this.zeroPosition = pos; - }; + // inherit speed + containedNode.vx = clusterNode.vx; + containedNode.vy = clusterNode.vy; + containedNode.options.hidden = false; + containedNode.togglePhysics(true); - /** - * set the options of the graph group over the default options. - * @param options - */ - GraphGroup.prototype.setOptions = function (options) { - if (options !== undefined) { - var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart"]; - util.selectiveDeepExtend(fields, this.options, options); + delete this.clusteredNodes[nodeId]; + } + } - util.mergeOptions(this.options, options, "catmullRom"); - util.mergeOptions(this.options, options, "drawPoints"); - util.mergeOptions(this.options, options, "shaded"); + // release edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + var edge = this.body.edges[edgeId]; + edge.options.hidden = false; + edge.togglePhysics(true); + } + } - 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; - } else { - this.options.catmullRom.parametrization = "centripetal"; - this.options.catmullRom.alpha = 0.5; + // remove all temporary edges + for (var i = 0; i < clusterNode.edges.length; i++) { + var edgeId = clusterNode.edges[i].id; + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = undefined; + delete this.body.nodes[viaId]; } + // this removes the edge from node.edges, which is why edgeIds is formed + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; } - } - } - } - if (this.options.style == "line") { - this.type = new Line(this.id, this.options); - } else if (this.options.style == "bar") { - this.type = new Bar(this.id, this.options); - } else if (this.options.style == "points") { - this.type = new Points(this.id, this.options); - } - }; + // remove clusterNode + delete this.body.nodes[clusterNodeId]; + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + _connectEdge: { - /** - * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph - * @param group - */ - GraphGroup.prototype.update = function (group) { - this.group = group; - this.content = group.content || "graph"; - this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10; - this.visible = group.visible === undefined ? true : group.visible; - this.style = group.style; - this.setOptions(group.options); - }; - /** - * draw the icon for the legend. - * - * @param x - * @param y - * @param JSONcontainer - * @param SVGcontainer - * @param iconWidth - * @param iconHeight - */ - GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { - var fillHeight = iconHeight * 0.5; - var path, fillPath; + /** + * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to + * is currently residing in cluster B + * @param edge + * @param nodeId + * @param from + * @private + */ + value: function _connectEdge(edge, nodeId, from) { + var clusterStack = this._getClusterStack(nodeId); + if (from == true) { + edge.from = clusterStack[clusterStack.length - 1]; + edge.fromId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.fromArray = clusterStack; + } else { + edge.to = clusterStack[clusterStack.length - 1]; + edge.toId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.toArray = clusterStack; + } + edge.connect(); + }, + writable: true, + configurable: true + }, + _getClusterStack: { - 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"); + /** + * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node + * @param nodeId + * @returns {Array} + * @private + */ + value: function _getClusterStack(nodeId) { + var stack = []; + var max = 100; + var counter = 0; - if (this.options.style == "line") { - path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer); - path.setAttributeNS(null, "class", this.className); - if (this.style !== undefined) { - path.setAttributeNS(null, "style", this.style); - } + while (this.clusteredNodes[nodeId] !== undefined && counter < max) { + stack.push(this.clusteredNodes[nodeId].node); + nodeId = this.clusteredNodes[nodeId].clusterId; + counter++; + } + stack.push(this.body.nodes[nodeId]); + return stack; + }, + writable: true, + configurable: true + }, + _getConnectedId: { - 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"); - } - if (this.options.drawPoints.enabled == true) { - DOMutil.drawPoint(x + 0.5 * iconWidth, y, this, JSONcontainer, SVGcontainer); - } - } else { - var barWidth = Math.round(0.3 * iconWidth); - var bar1Height = Math.round(0.4 * iconHeight); - var bar2Height = Math.round(0.75 * iconHeight); + /** + * Get the Id the node is connected to + * @param edge + * @param nodeId + * @returns {*} + * @private + */ + value: function _getConnectedId(edge, nodeId) { + if (edge.toId != nodeId) { + return edge.toId; + } else if (edge.fromId != nodeId) { + return edge.fromId; + } else { + return edge.fromId; + } + }, + writable: true, + configurable: true + }, + _getHubSize: { - var offset = Math.round((iconWidth - 2 * barWidth) / 3); + /** + * We determine how many connections denote an important hub. + * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) + * + * @private + */ + value: function _getHubSize() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; - 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); - } - }; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var node = this.body.nodes[this.body.nodeIndices[i]]; + if (node.edges.length > largestHub) { + largestHub = node.edges.length; + } + average += node.edges.length; + averageSquared += Math.pow(node.edges.length, 2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; + var variance = averageSquared - Math.pow(average, 2); + var standardDeviation = Math.sqrt(variance); - /** - * return the legend entree for this group. - * - * @param iconWidth - * @param iconHeight - * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} - */ - GraphGroup.prototype.getLegend = function (iconWidth, iconHeight) { - var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - this.drawIcon(0, 0.5 * iconHeight, [], svg, iconWidth, iconHeight); - return { icon: svg, label: this.content, orientation: this.options.yAxisOrientation }; - }; + var hubThreshold = Math.floor(average + 2 * standardDeviation); - GraphGroup.prototype.getYRange = function (groupData) { - return this.type.getYRange(groupData); - }; + // always have at least one to cluster + if (hubThreshold > largestHub) { + hubThreshold = largestHub; + } - GraphGroup.prototype.draw = function (dataset, group, framework) { - this.type.draw(dataset, group, framework); - }; + return hubThreshold; + }, + writable: true, + configurable: true + } + }); + return ClusterEngine; + })(); - module.exports = GraphGroup; + module.exports = ClusterEngine; /***/ }, -/* 49 */ +/* 55 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** - * Created by Alex on 11/11/2014. + * Created by Alex on 26-Feb-15. */ - var DOMutil = __webpack_require__(6); - var Points = __webpack_require__(50); - function Line(groupId, options) { - this.groupId = groupId; - this.options = options; + if (typeof window !== "undefined") { + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; } - Line.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + var util = __webpack_require__(1); - /** - * draw a line graph - * - * @param dataset - * @param group - */ - Line.prototype.draw = function (dataset, group, framework) { - if (dataset != null) { - if (dataset.length > 0) { - var path, d; - var svgHeight = Number(framework.svg.style.height.replace("px", "")); - path = DOMutil.getSVGElement("path", framework.svgElements, framework.svg); - path.setAttributeNS(null, "class", group.className); - if (group.style !== undefined) { - path.setAttributeNS(null, "style", group.style); - } + var CanvasRenderer = (function () { + function CanvasRenderer(body, canvas) { + var _this = this; + _classCallCheck(this, CanvasRenderer); - // construct path from dataset - if (group.options.catmullRom.enabled == true) { - d = Line._catmullRom(dataset, group); - } else { - d = Line._linear(dataset); + this.body = body; + this.canvas = canvas; + + this.redrawRequested = false; + this.renderTimer = false; + this.requiresTimeout = true; + this.renderingActive = false; + this.renderRequests = 0; + this.pixelRatio = undefined; + + // redefined in this._redraw + this.canvasTopLeft = { x: 0, y: 0 }; + this.canvasBottomRight = { x: 0, y: 0 }; + + this.dragging = false; + + this.body.emitter.on("dragStart", function () { + _this.dragging = true; + }); + this.body.emitter.on("dragEnd", function () { + return _this.dragging = false; + }); + this.body.emitter.on("_redraw", function () { + if (_this.renderingActive === false) { + _this._redraw(); } + }); + this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); + this.body.emitter.on("_startRendering", function () { + _this.renderRequests += 1;_this.renderingActive = true;_this.startRendering(); + }); + this.body.emitter.on("_stopRendering", function () { + _this.renderRequests -= 1;_this.renderingActive = _this.renderRequests > 0; + }); - // append with points for fill and finalize the path - if (group.options.shaded.enabled == true) { - var fillPath = DOMutil.getSVGElement("path", framework.svgElements, framework.svg); - var dFill; - if (group.options.shaded.orientation == "top") { - dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0; - } else { - dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight; + this.options = {}; + this.defaultOptions = { + hideEdgesOnDrag: false, + hideNodesOnDrag: false + }; + util.extend(this.options, this.defaultOptions); + + this._determineBrowserMethod(); + } + + _prototypeProperties(CanvasRenderer, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); } - fillPath.setAttributeNS(null, "class", group.className + " fill"); - if (group.options.shaded.style !== undefined) { - fillPath.setAttributeNS(null, "style", group.options.shaded.style); + }, + writable: true, + configurable: true + }, + startRendering: { + value: function startRendering() { + if (this.renderingActive === true) { + if (!this.renderTimer) { + if (this.requiresTimeout == true) { + this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function + } else { + this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function + } + } + } else {} + }, + writable: true, + configurable: true + }, + renderStep: { + value: function renderStep() { + // reset the renderTimer so a new scheduled animation step can be set + this.renderTimer = undefined; + + if (this.requiresTimeout == true) { + // this schedules a new simulation step + this.startRendering(); } - 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) { - Points.draw(dataset, group, framework); - } - } - } - }; + this._redraw(); + if (this.requiresTimeout == false) { + // this schedules a new simulation step + this.startRendering(); + } + }, + writable: true, + configurable: true + }, + redraw: { + /** + * Redraw the network with the current data + * chart will be resized too. + */ + value: function redraw() { + this.setSize(this.constants.width, this.constants.height); + this._redraw(); + }, + writable: true, + configurable: true + }, + _requestRedraw: { - /** - * 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 - */ - Line._catmullRomUniform = function (data) { - // catmull rom - var p0, p1, p2, p3, bp1, bp2; - var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " "; - var normalization = 1 / 6; - var length = data.length; - for (var i = 0; i < length - 1; i++) { - p0 = i == 0 ? data[0] : data[i - 1]; - p1 = data[i]; - p2 = data[i + 1]; - p3 = i + 2 < length ? data[i + 2] : p2; + /** + * Redraw the network with the current data + * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. + * @private + */ + value: function _requestRedraw() { + if (this.redrawRequested !== true && this.renderingActive === false) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, false), 0); + } else { + window.requestAnimationFrame(this._redraw.bind(this, false)); + } + } + }, + writable: true, + configurable: true + }, + _redraw: { + value: function _redraw() { + var hidden = arguments[0] === undefined ? false : arguments[0]; + this.body.emitter.emit("initRedraw"); + this.redrawRequested = false; + var ctx = this.canvas.frame.canvas.getContext("2d"); - // 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 + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + } - // 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 }; + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; - } + // clear the canvas + var w = this.canvas.frame.canvas.clientWidth; + var h = this.canvas.frame.canvas.clientHeight; + ctx.clearRect(0, 0, w, h); - return d; - }; + this.body.emitter.emit("beforeDrawing", ctx); - /** - * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm. - * By default, the centripetal parameterization is used because this gives the nicest results. - * These parameterizations are relatively heavy because the distance between 4 points have to be calculated. - * - * One optimization can be used to reuse distances since this is a sliding window approach. - * @param data - * @param group - * @returns {string} - * @private - */ - Line._catmullRom = function (data, group) { - var alpha = group.options.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; + // set scaling and translation + ctx.save(); + ctx.translate(this.body.view.translation.x, this.body.view.translation.y); + ctx.scale(this.body.view.scale, this.body.view.scale); - d1 = Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2)); - d2 = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); - d3 = Math.sqrt(Math.pow(p2.x - p3.x, 2) + Math.pow(p2.y - p3.y, 2)); + this.canvasTopLeft = this.canvas.DOMtoCanvas({ x: 0, y: 0 }); + this.canvasBottomRight = this.canvas.DOMtoCanvas({ x: this.canvas.frame.canvas.clientWidth, y: this.canvas.frame.canvas.clientHeight }); - // Catmull-Rom to Cubic Bezier conversion matrix + if (hidden === false) { + if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { + this._drawEdges(ctx); + } + } - // A = 2d1^2a + 3d1^a * d2^a + d3^2a - // B = 2d3^2a + 3d3^a * d2^a + d2^2a + if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { + this._drawNodes(ctx, hidden); + } - // [ 0 1 0 0 ] - // [ -d2^2a /N A/N d1^2a /N 0 ] - // [ 0 d3^2a /M B/M -d2^2a /M ] - // [ 0 0 1 0 ] + if (this.controlNodesActive === true) { + this._drawControlNodes(ctx); + } - d3powA = Math.pow(d3, alpha); - d3pow2A = Math.pow(d3, 2 * alpha); - d2powA = Math.pow(d2, alpha); - d2pow2A = Math.pow(d2, 2 * alpha); - d1powA = Math.pow(d1, alpha); - d1pow2A = Math.pow(d1, 2 * alpha); + //this.physics.nodesSolver._debug(ctx,"#F00F0F"); - A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A; - B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A; - N = 3 * d1powA * (d1powA + d2powA); - if (N > 0) { - N = 1 / N; - } - M = 3 * d3powA * (d3powA + d2powA); - if (M > 0) { - M = 1 / M; - } + this.body.emitter.emit("afterDrawing", ctx); - bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, - y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + // restore original scaling and translation + ctx.restore(); - bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, - y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; + if (hidden === true) { + ctx.clearRect(0, 0, w, h); + } - 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 + " "; - } + }, + writable: true, + configurable: true + }, + _drawNodes: { - return d; - } - }; - /** - * this generates the SVG path for a linear drawing between datapoints. - * @param data - * @returns {string} - * @private - */ - Line._linear = function (data) { - // linear - var d = ""; - for (var i = 0; i < data.length; i++) { - if (i == 0) { - d += data[i].x + "," + data[i].y; - } else { - d += " " + data[i].x + "," + data[i].y; + /** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] + * @private + */ + value: function _drawNodes(ctx) { + var alwaysShow = arguments[1] === undefined ? false : arguments[1]; + var nodes = this.body.nodes; + var nodeIndices = this.body.nodeIndices; + var node; + var selected = []; + + // draw unselected nodes; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + // set selected nodes aside + if (node.isSelected()) { + selected.push(nodeIndices[i]); + } else { + if (alwaysShow === true) { + node.draw(ctx); + } + // todo: replace check + //else if (node.inArea() === true) { + node.draw(ctx); + //} + } + } + + // draw the selected nodes on top + for (var i = 0; i < selected.length; i++) { + node = nodes[selected[i]]; + node.draw(ctx); + } + }, + writable: true, + configurable: true + }, + _drawEdges: { + + + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawEdges(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; + + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edge.draw(ctx); + } + } + }, + writable: true, + configurable: true + }, + _drawControlNodes: { + + /** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawControlNodes(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; + + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + edge._drawControlNodes(ctx); + } + }, + writable: true, + configurable: true + }, + _determineBrowserMethod: { + + /** + * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because + * some implementations (safari and IE9) did not support requestAnimationFrame + * @private + */ + value: function _determineBrowserMethod() { + if (typeof window !== "undefined") { + var browserType = navigator.userAgent.toLowerCase(); + this.requiresTimeout = false; + if (browserType.indexOf("msie 9.0") != -1) { + // IE 9 + this.requiresTimeout = true; + } else if (browserType.indexOf("safari") != -1) { + // safari + if (browserType.indexOf("chrome") <= -1) { + this.requiresTimeout = true; + } + } + } else { + this.requiresTimeout = true; + } + }, + writable: true, + configurable: true } - } - return d; - }; + }); - module.exports = Line; + return CanvasRenderer; + })(); + + module.exports = CanvasRenderer; /***/ }, -/* 50 */ +/* 56 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - /** - * Created by Alex on 11/11/2014. - */ - var DOMutil = __webpack_require__(6); - - function Points(groupId, options) { - this.groupId = groupId; - this.options = options; - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - Points.prototype.getYRange = function (groupData) { - var yMin = groupData[0].y; - var yMax = groupData[0].y; - for (var j = 0; j < groupData.length; j++) { - yMin = yMin > groupData[j].y ? groupData[j].y : yMin; - yMax = yMax < groupData[j].y ? groupData[j].y : yMax; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(43); - Points.prototype.draw = function (dataset, group, framework, offset) { - Points.draw(dataset, group, framework, offset); - }; + var util = __webpack_require__(1); /** - * draw the data points - * - * @param {Array} dataset - * @param {Object} JSONcontainer - * @param {Object} svg | SVG DOM element - * @param {GraphGroup} group - * @param {Number} [offset] + * 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 */ - Points.draw = function (dataset, group, framework, offset) { - if (offset === undefined) { - offset = 0; - } - for (var i = 0; i < dataset.length; i++) { - DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label); + var Canvas = (function () { + function Canvas(body) { + var _this = this; + _classCallCheck(this, Canvas); + + this.body = body; + + this.options = {}; + this.defaultOptions = { + width: "100%", + height: "100%" + }; + util.extend(this.options, this.defaultOptions); + + this.body.emitter.once("resize", function (obj) { + _this.body.view.translation.x = obj.width * 0.5;_this.body.view.translation.y = obj.height * 0.5; + }); + this.body.emitter.on("destroy", function () { + return _this.hammer.destroy(); + }); + + this.pixelRatio = 1; } - }; + _prototypeProperties(Canvas, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + }, + writable: true, + configurable: true + }, + create: { + value: function create() { + // remove all elements from the container element. + while (this.body.container.hasChildNodes()) { + this.body.container.removeChild(this.body.container.firstChild); + } - module.exports = Points; + this.frame = document.createElement("div"); + this.frame.className = "vis network-frame"; + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; + this.frame.tabIndex = 900; -/***/ }, -/* 51 */ -/***/ function(module, exports, __webpack_require__) { + ////////////////////////////////////////////////////////////////// - "use strict"; + this.frame.canvas = document.createElement("canvas"); + this.frame.canvas.style.position = "relative"; + this.frame.appendChild(this.frame.canvas); - /** - * Created by Alex on 11/11/2014. - */ - var DOMutil = __webpack_require__(6); - var Points = __webpack_require__(50); + 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); + } else { + var ctx = this.frame.canvas.getContext("2d"); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - function Bargraph(groupId, options) { - this.groupId = groupId; - this.options = options; - } + this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + } - Bargraph.prototype.getYRange = function (groupData) { - if (this.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; - } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - } else { - var barCombinedData = []; - for (var j = 0; j < groupData.length; j++) { - barCombinedData.push({ - x: groupData[j].x, - y: groupData[j].y, - groupId: this.groupId - }); - } - return barCombinedData; - } - }; + // add the frame to the container element + this.body.container.appendChild(this.frame); + this.body.view.scale = 1; + this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; + this._bindHammer(); + }, + writable: true, + configurable: true + }, + _bindHammer: { - /** - * draw a bar graph - * - * @param groupIds - * @param processedGroupData - */ - Bargraph.draw = function (groupIds, processedGroupData, framework) { - var combinedData = []; - var intersections = {}; - var coreDistance; - var key, drawData; - var group; - var i, j; - var barPoints = 0; - // combine all barchart data - for (i = 0; i < groupIds.length; i++) { - group = framework.groups[groupIds[i]]; - if (group.options.style == "bar") { - if (group.visible == true && (framework.options.groups.visibility[groupIds[i]] === undefined || framework.options.groups.visibility[groupIds[i]] == true)) { - for (j = 0; j < processedGroupData[groupIds[i]].length; j++) { - combinedData.push({ - x: processedGroupData[groupIds[i]][j].x, - y: processedGroupData[groupIds[i]][j].y, - groupId: groupIds[i], - label: processedGroupData[groupIds[i]][j].label - }); - barPoints += 1; + /** + * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * @private + */ + value: function _bindHammer() { + var _this = this; + if (this.hammer !== undefined) { + this.hammer.destroy(); } - } - } - } + this.drag = {}; + this.pinch = {}; - if (barPoints == 0) { - return; - } + // init hammer + this.hammer = new Hammer(this.frame.canvas); + this.hammer.get("pinch").set({ enable: true }); - // 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; - } - }); + hammerUtil.onTouch(this.hammer, function (event) { + _this.body.eventListeners.onTouch(event); + }); + this.hammer.on("tap", function (event) { + _this.body.eventListeners.onTap(event); + }); + this.hammer.on("doubletap", function (event) { + _this.body.eventListeners.onDoubleTap(event); + }); + this.hammer.on("press", function (event) { + _this.body.eventListeners.onHold(event); + }); + this.hammer.on("panstart", function (event) { + _this.body.eventListeners.onDragStart(event); + }); + this.hammer.on("panmove", function (event) { + _this.body.eventListeners.onDrag(event); + }); + this.hammer.on("panend", function (event) { + _this.body.eventListeners.onDragEnd(event); + }); + this.hammer.on("pinch", function (event) { + _this.body.eventListeners.onPinch(event); + }); - // get intersections - Bargraph._getDataIntersections(intersections, combinedData); + // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? + this.frame.canvas.addEventListener("mousewheel", function (event) { + _this.body.eventListeners.onMouseWheel(event); + }); + this.frame.canvas.addEventListener("DOMMouseScroll", function (event) { + _this.body.eventListeners.onMouseWheel(event); + }); - // plot barchart - for (i = 0; i < combinedData.length; i++) { - group = framework.groups[combinedData[i].groupId]; - var minWidth = 0.1 * group.options.barChart.width; + this.frame.canvas.addEventListener("mousemove", function (event) { + _this.body.eventListeners.onMouseMove(event); + }); - key = combinedData[i].x; - var heightOffset = 0; - if (intersections[key] === undefined) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - key); - } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); - } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - } else { - var nextKey = i + (intersections[key].amount - intersections[key].resolved); - var prevKey = i - (intersections[key].resolved + 1); - if (nextKey < combinedData.length) { - coreDistance = Math.abs(combinedData[nextKey].x - key); - } - if (prevKey > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); - } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - intersections[key].resolved += 1; + this.hammerFrame = new Hammer(this.frame); + hammerUtil.onRelease(this.hammerFrame, function (event) { + _this.body.eventListeners.onRelease(event); + }); + }, + writable: true, + configurable: true + }, + setSize: { - if (group.options.barChart.handleOverlap == "stack") { - heightOffset = intersections[key].accumulated; - intersections[key].accumulated += group.zeroPosition - combinedData[i].y; - } else if (group.options.barChart.handleOverlap == "sideBySide") { - drawData.width = drawData.width / intersections[key].amount; - drawData.offset += intersections[key].resolved * drawData.width - 0.5 * drawData.width * (intersections[key].amount + 1); - if (group.options.barChart.align == "left") { - drawData.offset -= 0.5 * drawData.width; - } else if (group.options.barChart.align == "right") { - drawData.offset += 0.5 * drawData.width; + + /** + * 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%') + */ + value: function setSize() { + var width = arguments[0] === undefined ? this.options.width : arguments[0]; + var height = arguments[1] === undefined ? this.options.height : arguments[1]; + var emitEvent = false; + var oldWidth = this.frame.canvas.width; + var oldHeight = this.frame.canvas.height; + if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; + + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + + this.options.width = width; + this.options.height = height; + + emitEvent = true; + } else { + // this would adapt the width of the canvas to the width from 100% if and only if + // there is a change. + + if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + emitEvent = true; + } + if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + emitEvent = true; + } } - } - } - DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + " bar", framework.svgElements, framework.svg); - // draw points - if (group.options.drawPoints.enabled == true) { - Points.draw([combinedData[i]], group, framework, drawData.offset); - //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg); - } - } - }; + if (emitEvent === true) { + this.body.emitter.emit("resize", { width: this.frame.canvas.width / this.pixelRatio, height: this.frame.canvas.height / this.pixelRatio, oldWidth: oldWidth / this.pixelRatio, oldHeight: oldHeight / this.pixelRatio }); + } + }, + writable: true, + configurable: true + }, + _XconvertDOMtoCanvas: { - /** - * Fill the intersections object with counters of how many datapoints share the same x coordinates - * @param intersections - * @param combinedData - * @private - */ - Bargraph._getDataIntersections = function (intersections, combinedData) { - // get intersections - var coreDistance; - for (var i = 0; i < combinedData.length; i++) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x); - } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x)); - } - if (coreDistance == 0) { - if (intersections[combinedData[i].x] === undefined) { - intersections[combinedData[i].x] = { amount: 0, resolved: 0, accumulated: 0 }; - } - intersections[combinedData[i].x].amount += 1; - } - } - }; + /** + * 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 + */ + value: function _XconvertDOMtoCanvas(x) { + return (x - this.body.view.translation.x) / this.body.view.scale; + }, + writable: true, + configurable: true + }, + _XconvertCanvasToDOM: { - /** - * Get the width and offset for bargraphs based on the coredistance between datapoints - * - * @param coreDistance - * @param group - * @param minWidth - * @returns {{width: Number, offset: Number}} - * @private - */ - Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { - var width, offset; - if (coreDistance < group.options.barChart.width && coreDistance > 0) { - width = coreDistance < minWidth ? minWidth : coreDistance; + /** + * 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 + */ + value: function _XconvertCanvasToDOM(x) { + return x * this.body.view.scale + this.body.view.translation.x; + }, + writable: true, + configurable: true + }, + _YconvertDOMtoCanvas: { - offset = 0; // recalculate offset with the new width; - if (group.options.barChart.align == "left") { - offset -= 0.5 * coreDistance; - } else if (group.options.barChart.align == "right") { - offset += 0.5 * coreDistance; - } - } else { - // default settings - width = group.options.barChart.width; - offset = 0; - if (group.options.barChart.align == "left") { - offset -= 0.5 * group.options.barChart.width; - } else if (group.options.barChart.align == "right") { - offset += 0.5 * group.options.barChart.width; - } - } + /** + * 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 + */ + value: function _YconvertDOMtoCanvas(y) { + return (y - this.body.view.translation.y) / this.body.view.scale; + }, + writable: true, + configurable: true + }, + _YconvertCanvasToDOM: { - return { width: width, offset: offset }; - }; + /** + * 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 + */ + value: function _YconvertCanvasToDOM(y) { + return y * this.body.view.scale + this.body.view.translation.y; + }, + writable: true, + configurable: true + }, + canvasToDOM: { - Bargraph.getStackedBarYRange = function (barCombinedData, groupRanges, groupIds, groupLabel, orientation) { - if (barCombinedData.length > 0) { - // sort by time and by group - barCombinedData.sort(function (a, b) { - if (a.x == b.x) { - return a.groupId - b.groupId; - } else { - return a.x - b.x; - } - }); - var intersections = {}; - Bargraph._getDataIntersections(intersections, barCombinedData); - groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData); - groupRanges[groupLabel].yAxisOrientation = orientation; - groupIds.push(groupLabel); - } - }; + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + value: function canvasToDOM(pos) { + return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; + }, + writable: true, + configurable: true + }, + DOMtoCanvas: { - Bargraph._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; + /** + * + * @param {object} pos = {x: number, y: number} + * @returns {{x: number, y: number}} + * @constructor + */ + value: function DOMtoCanvas(pos) { + return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; + }, + writable: true, + configurable: true } - } + }); - return { min: yMin, max: yMax }; - }; + return Canvas; + })(); - module.exports = Bargraph; + module.exports = Canvas; /***/ }, -/* 52 */ +/* 57 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(6); - var Component = __webpack_require__(25); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Legend for Graph2d + * Created by Alex on 26-Feb-15. */ - function Legend(body, options, side, linegraphOptions) { - this.body = body; - this.defaultOptions = { - enabled: true, - icons: true, - iconSize: 20, - iconSpacing: 6, - left: { - visible: true, - position: "top-left" // top/bottom - left,center,right - }, - right: { - visible: true, - position: "top-left" // top/bottom - left,center,right - } - }; - this.side = side; - this.options = util.extend({}, this.defaultOptions); - this.linegraphOptions = linegraphOptions; - this.svgElements = {}; - this.dom = {}; - this.groups = {}; - this.amountOfGroups = 0; - this._create(); + var util = __webpack_require__(1); - this.setOptions(options); - } + var View = (function () { + function View(body, canvas) { + var _this = this; + _classCallCheck(this, View); - Legend.prototype = new Component(); + this.body = body; + this.canvas = canvas; - Legend.prototype.clear = function () { - this.groups = {}; - this.amountOfGroups = 0; - }; + this.animationSpeed = 1 / this.renderRefreshRate; + this.animationEasingFunction = "easeInOutQuint"; + this.easingTime = 0; + this.sourceScale = 0; + this.targetScale = 0; + this.sourceTranslation = 0; + this.targetTranslation = 0; + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + this.touchTime = 0; - Legend.prototype.addGroup = function (label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; + this.viewFunction = undefined; + + this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); + this.body.emitter.on("animationFinished", function () { + _this.body.emitter.emit("_stopRendering"); + }); + this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); } - this.amountOfGroups += 1; - }; - Legend.prototype.updateGroup = function (label, graphOptions) { - this.groups[label] = graphOptions; - }; + _prototypeProperties(View, null, { + setOptions: { + value: function setOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + this.options = options; + }, + writable: true, + configurable: true + }, + _getRange: { - Legend.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; - } - }; - 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"; + // zoomExtent + /** + * Find the center position of the network + * @private + */ + value: function _getRange() { + var specificNodes = arguments[0] === undefined ? [] : arguments[0]; + var minY = 1000000000, + maxY = -1000000000, + minX = 1000000000, + maxX = -1000000000, + node; + if (specificNodes.length > 0) { + for (var i = 0; i < specificNodes.length; i++) { + node = this.body.nodes[specificNodes[i]]; + if (minX > node.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; + } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.boundingBox.bottom; + } // top is negative, bottom is positive + } + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (minX > node.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; + } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.boundingBox.bottom; + } // top is negative, bottom is positive + } + } + } - this.dom.textArea = document.createElement("div"); - this.dom.textArea.className = "legendText"; - this.dom.textArea.style.position = "relative"; - this.dom.textArea.style.top = "0px"; + if (minX == 1000000000 && maxX == -1000000000 && minY == 1000000000 && maxY == -1000000000) { + minY = 0, maxY = 0, minX = 0, maxX = 0; + } + return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; + }, + writable: true, + configurable: true + }, + _findCenter: { - this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - this.svg.style.position = "absolute"; - this.svg.style.top = 0 + "px"; - this.svg.style.width = this.options.iconSize + 5 + "px"; - this.svg.style.height = "100%"; - this.dom.frame.appendChild(this.svg); - this.dom.frame.appendChild(this.dom.textArea); - }; + /** + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} + * @private + */ + value: function _findCenter(range) { + return { x: 0.5 * (range.maxX + range.minX), + y: 0.5 * (range.maxY + range.minY) }; + }, + writable: true, + configurable: true + }, + zoomExtent: { - /** - * Hide the component from the DOM - */ - Legend.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } - }; - /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed - */ - Legend.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } - }; + /** + * This function zooms out to fit all data on screen based on amount of nodes + * @param {Object} + * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; + * @param {Boolean} [disableStart] | If true, start is not called. + */ + value: function zoomExtent() { + var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; + var initialZoom = arguments[1] === undefined ? false : arguments[1]; + var range; + var zoomLevel; - Legend.prototype.setOptions = function (options) { - var fields = ["enabled", "orientation", "icons", "left", "right"]; - util.selectiveDeepExtend(fields, this.options, options); - }; + if (initialZoom === true) { + // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. + var positionDefined = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.predefinedPosition == true) { + positionDefined += 1; + } + } + } + if (positionDefined > 0.5 * this.body.nodeIndices.length) { + this.zoomExtent(options, false); + return; + } - Legend.prototype.redraw = function () { - var activeGroups = 0; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - activeGroups++; - } - } - } + range = this._getRange(options.nodes); - 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 = ""; - } + var numberOfNodes = this.body.nodeIndices.length; + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - if (this.options[this.side].position == "top-left" || this.options[this.side].position == "top-right") { - this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px", "")) + "px"; - this.dom.frame.style.bottom = ""; - } else { - var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height; - this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace("px", "")) + "px"; - this.dom.frame.style.top = ""; - } + // correct for larger canvasses. + var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); + zoomLevel *= factor; + } else { + this.body.emitter.emit("_redraw", true); + range = this._getRange(options.nodes); + var xDistance = Math.abs(range.maxX - range.minX) * 1.1; + var yDistance = Math.abs(range.maxY - range.minY) * 1.1; - if (this.options.icons == false) { - this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + "px"; - this.dom.textArea.style.right = ""; - this.dom.textArea.style.left = ""; - this.svg.style.width = "0px"; - } else { - this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + "px"; - this.drawLegendIcons(); - } + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + } - var content = ""; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - content += this.groups[groupId].content + "
"; + if (zoomLevel > 1) { + zoomLevel = 1; } - } - } - this.dom.textArea.innerHTML = content; - this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + "px"; - } - }; - Legend.prototype.drawLegendIcons = function () { - if (this.dom.frame.parentNode) { - 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; + var center = this._findCenter(range); + var animationOptions = { position: center, scale: zoomLevel, animation: options }; + this.moveTo(animationOptions); + }, + writable: true, + configurable: true + }, + focusOnNode: { + + // animation + + /** + * Center a node in view. + * + * @param {Number} nodeId + * @param {Number} [options] + */ + value: function focusOnNode(nodeId) { + var options = arguments[1] === undefined ? {} : arguments[1]; + if (this.body.nodes[nodeId] !== undefined) { + var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; + options.position = nodePosition; + options.lockedOnNode = nodeId; + + this.moveTo(options); + } else { + console.log("Node: " + nodeId + " cannot be found."); + } + }, + writable: true, + configurable: true + }, + moveTo: { + + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.scale = Number // scale to move to + * | options.position = {x:Number, y:Number} // position to move to + * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to + */ + value: function moveTo(options) { + if (options === undefined) { + options = {}; + return; + } + if (options.offset === undefined) { + options.offset = { x: 0, y: 0 }; + } + if (options.offset.x === undefined) { + options.offset.x = 0; + } + if (options.offset.y === undefined) { + options.offset.y = 0; + } + if (options.scale === undefined) { + options.scale = this.body.view.scale; + } + if (options.position === undefined) { + options.position = this.body.view.translation; + } + if (options.animation === undefined) { + options.animation = { duration: 0 }; + } + if (options.animation === false) { + options.animation = { duration: 0 }; + } + if (options.animation === true) { + options.animation = {}; + } + if (options.animation.duration === undefined) { + options.animation.duration = 1000; + } // default duration + if (options.animation.easingFunction === undefined) { + options.animation.easingFunction = "easeInOutQuad"; + } // default easing function + + this.animateView(options); + }, + writable: true, + configurable: true + }, + animateView: { + + /** + * + * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels + * | options.time = Number // animation time in milliseconds + * | options.scale = Number // scale to animate to + * | options.position = {x:Number, y:Number} // position to animate to + * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, + * // easeInCubic, easeOutCubic, easeInOutCubic, + * // easeInQuart, easeOutQuart, easeInOutQuart, + * // easeInQuint, easeOutQuint, easeInOutQuint + */ + value: function animateView(options) { + if (options === undefined) { + return; + } + this.animationEasingFunction = options.animation.easingFunction; + // release if something focussed on the node + this.releaseNode(); + if (options.locked == true) { + this.lockedOnNodeId = options.lockedOnNode; + this.lockedOnNodeOffset = options.offset; + } + + // forcefully complete the old animation if it was still running + if (this.easingTime != 0) { + this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. + } + + this.sourceScale = this.body.view.scale; + this.sourceTranslation = this.body.view.translation; + this.targetScale = options.scale; + + // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw + // but at least then we'll have the target transition + this.body.view.scale = this.targetScale; + var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - options.position.x, + y: viewCenter.y - options.position.y + }; + this.targetTranslation = { + x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, + y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y + }; + + // if the time is set to 0, don't do an animation + if (options.animation.duration == 0) { + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + } else { + this.body.view.scale = this.targetScale; + this.body.view.translation = this.targetTranslation; + this.body.emitter.emit("_requestRedraw"); + } + } else { + this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's + this.animationEasingFunction = options.animation.easingFunction; + + + this.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + }, + writable: true, + configurable: true + }, + _lockedRedraw: { + + /** + * used to animate smoothly by hijacking the redraw function. + * @private + */ + value: function _lockedRedraw() { + var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; + var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); + var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node + x: viewCenter.x - nodePosition.x, + y: viewCenter.y - nodePosition.y + }; + var sourceTranslation = this.body.view.translation; + var targetTranslation = { + x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, + y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y + }; + + this.body.view.translation = targetTranslation; + }, + writable: true, + configurable: true + }, + releaseNode: { + value: function releaseNode() { + if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + } + }, + writable: true, + configurable: true + }, + _transitionRedraw: { + + /** + * + * @param easingTime + * @private + */ + value: function _transitionRedraw() { + var finished = arguments[0] === undefined ? false : arguments[0]; + this.easingTime += this.animationSpeed; + this.easingTime = finished === true ? 1 : this.easingTime; - this.svg.style.width = iconWidth + 5 + iconOffset + "px"; + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); - y += iconHeight + this.options.iconSpacing; + this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress; + this.body.view.translation = { + x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, + y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress + }; + + // cleanup + if (this.easingTime >= 1) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.easingTime = 0; + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + } + this.body.emitter.emit("animationFinished"); } - } + }, + writable: true, + configurable: true } + }); - DOMutil.cleanupElements(this.svgElements); - } - }; + return View; + })(); - module.exports = Legend; + module.exports = View; /***/ }, -/* 53 */ +/* 58 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // Load custom shapes into CanvasRenderingContext2D - __webpack_require__(54); - - var Emitter = __webpack_require__(11); - var Hammer = __webpack_require__(19); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); - var dotparser = __webpack_require__(55); - var gephiParser = __webpack_require__(56); - var Images = __webpack_require__(57); - var Activator = __webpack_require__(38); - - var Groups = _interopRequire(__webpack_require__(58)); - - var NodesHandler = _interopRequire(__webpack_require__(59)); - - var EdgesHandler = _interopRequire(__webpack_require__(79)); - - var PhysicsEngine = _interopRequire(__webpack_require__(86)); - - var ClusterEngine = _interopRequire(__webpack_require__(93)); - - var CanvasRenderer = _interopRequire(__webpack_require__(95)); - - var Canvas = _interopRequire(__webpack_require__(96)); - - var View = _interopRequire(__webpack_require__(97)); - - var InteractionHandler = _interopRequire(__webpack_require__(98)); - - var SelectionHandler = _interopRequire(__webpack_require__(101)); - - var LayoutEngine = _interopRequire(__webpack_require__(102)); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var ManipulationSystem = _interopRequire(__webpack_require__(103)); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * @constructor Network - * Create a network visualization, displaying nodes and edges. + * Created by Alex on 2/27/2015. * - * @param {Element} container The DOM element in which the Network will - * be created. Normally a div element. - * @param {Object} data An object containing parameters - * {Array} nodes - * {Array} edges - * @param {Object} options Options */ - function Network(container, data, options) { - var _this = this; - if (!(this instanceof Network)) { - throw new SyntaxError("Constructor must be called with the new operator"); - } - - // set constant values - this.options = {}; - this.defaultOptions = { - clickToUse: false - }; - util.extend(this.options, this.defaultOptions); - - // containers for nodes and edges - this.body = { - nodes: {}, - nodeIndices: [], - edges: {}, - edgeIndices: [], - data: { - nodes: null, // A DataSet or DataView - edges: null // A DataSet or DataView - }, - functions: { - createNode: function () {}, - createEdge: function () {}, - getPointer: function () {} - }, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this), - once: this.once.bind(this) - }, - eventListeners: { - onTap: function () {}, - onTouch: function () {}, - onDoubleTap: function () {}, - onHold: function () {}, - onDragStart: function () {}, - onDrag: function () {}, - onDragEnd: function () {}, - onMouseWheel: function () {}, - onPinch: function () {}, - onMouseMove: function () {}, - onRelease: function () {} - }, - container: container, - view: { - scale: 1, - translation: { x: 0, y: 0 } - } - }; - - // bind the event listeners - this.bindEventListeners(); - - // setting up all modules - var images = new Images(function () { - return _this.body.emitter.emit("_requestRedraw"); - }); // object with images - - this.groups = new Groups(); // object with groups - this.canvas = new Canvas(this.body); // DOM handler - this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler - this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key - this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms - this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into - this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations - this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout - this.clustering = new ClusterEngine(this.body); // clustering api - this.manipulation = new ManipulationSystem(this.body, this.canvas, this.selectionHandler); // data manipulation system - this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options - this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options - - // create the DOM elements - this.canvas.create(); + var util = __webpack_require__(1); - // apply options - this.setOptions(options); + var NavigationHandler = _interopRequire(__webpack_require__(75)); - // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data); - } + var Popup = _interopRequire(__webpack_require__(76)); - // Extend Network with an Emitter mixin - Emitter(Network.prototype); + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; + this.navigationHandler = new NavigationHandler(body, canvas); + // bind the events from hammer to functions in this object + this.body.eventListeners.onTap = this.onTap.bind(this); + this.body.eventListeners.onTouch = this.onTouch.bind(this); + this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); + this.body.eventListeners.onHold = this.onHold.bind(this); + this.body.eventListeners.onDragStart = this.onDragStart.bind(this); + this.body.eventListeners.onDrag = this.onDrag.bind(this); + this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); + this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); + this.body.eventListeners.onPinch = this.onPinch.bind(this); + this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); + this.body.eventListeners.onRelease = this.onRelease.bind(this); - /** - * Set options - * @param {Object} options - */ - Network.prototype.setOptions = function (options) { - if (options !== undefined) { - // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. - options = this.layoutEngine.setOptions(options.layout, options); + this.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.hoverObj = { nodes: {}, edges: {} }; + this.popup = undefined; + this.popupObj = undefined; + this.popupTimer = undefined; - this.groups.setOptions(options.groups); - this.nodesHandler.setOptions(options.nodes); - this.edgesHandler.setOptions(options.edges); - this.physics.setOptions(options.physics); - this.canvas.setOptions(options.canvas); - this.renderer.setOptions(options.rendering); - this.view.setOptions(options.view); - this.interactionHandler.setOptions(options.interaction); - this.selectionHandler.setOptions(options.selection); - this.clustering.setOptions(options.clustering); - this.manipulation.setOptions(options.manipulation); + this.body.functions.getPointer = this.getPointer.bind(this); - if (options.clickToUse !== undefined) { - if (options.clickToUse === true) { - if (this.activator === undefined) { - this.activator = new Activator(this.frame); - this.activator.on("change", this._createKeyBinds.bind(this)); - } - } else { - if (this.activator !== undefined) { - this.activator.destroy(); - delete this.activator; + this.options = {}; + this.defaultOptions = { + dragNodes: true, + dragView: true, + zoomView: true, + hoverEnabled: false, + showNavigationIcons: false, + tooltip: { + delay: 300, + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" } - this.body.emitter.emit("activate"); - } - } else { - this.body.emitter.emit("activate"); - } - - this.canvas.setSize(); - } - }; - - - /** - * Update the this.body.nodeIndices with the most recent node index list - * @private - */ - Network.prototype._updateVisibleIndices = function () { - var nodes = this.body.nodes; - var edges = this.body.edges; - this.body.nodeIndices = []; - this.body.edgeIndices = []; - - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].options.hidden === false) { - this.body.nodeIndices.push(nodeId); + }, + keyboard: { + enabled: false, + speed: { x: 10, y: 10, zoom: 0.02 }, + bindToWindow: true } - } + }; + util.extend(this.options, this.defaultOptions); } - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].options.hidden === false) { - this.body.edgeIndices.push(edgeId); - } - } - } - }; + _prototypeProperties(InteractionHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + // extend all but the values in fields + var fields = ["keyboard", "tooltip"]; + util.selectiveNotDeepExtend(fields, this.options, options); - Network.prototype.bindEventListeners = function () { - var _this = this; - // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. - this.body.emitter.on("_dataChanged", function (params) { - var t0 = new Date().valueOf(); - // update shortcut lists - _this._updateVisibleIndices(); - _this.physics.updatePhysicsIndices(); + // merge the keyboard options in. + util.mergeOptions(this.options, options, "keyboard"); - // call the dataUpdated event because the only difference between the two is the updating of the indices - _this.body.emitter.emit("_dataUpdated"); + if (options.tooltip) { + util.extend(this.options.tooltip, options.tooltip); + if (options.tooltip.color) { + this.options.tooltip.color = util.parseColor(options.tooltip.color); + } + } + } - console.log("_dataChanged took:", new Date().valueOf() - t0); - }); + this.navigationHandler.setOptions(this.options); + }, + writable: true, + configurable: true + }, + getPointer: { - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on("_dataUpdated", function () { - var t0 = new Date().valueOf(); - // update values - _this._updateValueRange(_this.body.nodes); - _this._updateValueRange(_this.body.edges); - // start simulation (can be called safely, even if already running) - _this.body.emitter.emit("startSimulation"); - console.log("_dataUpdated took:", new Date().valueOf() - t0); - }); - }; + /** + * Get the pointer location from a touch location + * @param {{x: Number, y: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private + */ + value: function getPointer(touch) { + return { + x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), + y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) + }; + }, + writable: true, + configurable: true + }, + onTouch: { - /** - * 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. - */ - Network.prototype.setData = function (data) { - // reset the physics engine. - this.body.emitter.emit("resetPhysics"); - this.body.emitter.emit("_resetData"); - // unselect all to ensure no selections from old data are carried over. - this.selectionHandler.unselectAll(); + /** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ + value: function onTouch(event) { + if (new Date().valueOf() - this.touchTime > 100) { + this.drag.pointer = this.getPointer(event.center); + this.drag.pinched = false; + this.pinch.scale = this.body.view.scale; + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); + } + }, + writable: true, + configurable: true + }, + onTap: { - 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."); - } + /** + * handle tap/click event: select/unselect a node + * @private + */ + value: function onTap(event) { + var pointer = this.getPointer(event.center); - // 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; - } - } else { - this.nodesHandler.setData(data && data.nodes, true); - this.edgesHandler.setData(data && data.edges, true); - } + var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; + var selected = this.selectionHandler.selectOnPoint(pointer); - // emit change in data - this.body.emitter.emit("_dataChanged"); + if (selected === true || previouslySelected == true && selected === false) { + // select or unselect + this.body.emitter.emit("select", this.selectionHandler.getSelection()); + } - // find a stable position or start animating to a stable position - this.body.emitter.emit("initPhysics"); - }; + this.selectionHandler._generateClickEvent("click", pointer); + }, + writable: true, + configurable: true + }, + onDoubleTap: { - /** - * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. - * var network = new vis.Network(..); - * network.destroy(); - * network = null; - */ - Network.prototype.destroy = function () { - this.body.emitter.emit("destroy"); + /** + * handle doubletap event + * @private + */ + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent("doubleClick", pointer); + }, + writable: true, + configurable: true + }, + onHold: { - // clear events - this.body.emitter.off(); - // remove the container and everything inside it recursively - util.recursiveDOMDelete(this.body.container); - }; + /** + * handle long tap event: multi select nodes + * @private + */ + value: function onHold(event) { + var pointer = this.getPointer(event.center); + var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); - /** - * Update the values of all object in the given array according to the current - * value range of the objects in the array. - * @param {Object} obj An object containing a set of Edges or Nodes - * The objects must have a method getValue() and - * setValueRange(min, max). - * @private - */ - Network.prototype._updateValueRange = function (obj) { - var id; + if (selectionChanged === true) { + // select or longpress + this.body.emitter.emit("select", this.selectionHandler.getSelection()); + } - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; - var valueTotal = 0; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = valueMin === undefined ? value : Math.min(value, valueMin); - valueMax = valueMax === undefined ? value : Math.max(value, valueMax); - valueTotal += value; - } - } - } + this.selectionHandler._generateClickEvent("click", pointer); + }, + writable: true, + configurable: true + }, + onRelease: { - // adjust the range of all objects - if (valueMin !== undefined && valueMax !== undefined) { - for (id in obj) { - if (obj.hasOwnProperty(id)) { - obj[id].setValueRange(valueMin, valueMax, valueTotal); - } - } - } - }; + /** + * handle the release of the screen + * + * @private + */ + value: function onRelease(event) { + this.body.emitter.emit("release", event); + }, + writable: true, + configurable: true + }, + onDragStart: { - /** - * Scale the network - * @param {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._setScale = function (scale) { - this.body.view.scale = scale; - }; - /** - * Get the current scale of the network - * @return {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._getScale = function () { - return this.body.view.scale; - }; + /** + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ + value: function onDragStart(event) { + //in case the touch event was triggered on an external div, do the initial touch now. + if (this.drag.pointer === undefined) { + this.onTouch(event); + } + // note: drag.pointer is set in onTouch to get the initial touch location + var node = this.selectionHandler.getNodeAt(this.drag.pointer); - /** - * Load the XY positions of the nodes into the dataset. - */ - Network.prototype.storePositions = function () { - // todo: incorporate fixed instead of allowedtomove, add support for clusters and hierarchical. - var dataArray = []; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - var allowedToMoveX = !this.body.nodes.xFixed; - var allowedToMoveY = !this.body.nodes.yFixed; - if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { - dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y), allowedToMoveX: allowedToMoveX, allowedToMoveY: allowedToMoveY }); - } - } - } - this.body.data.nodes.update(dataArray); - }; + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({}, this.body.view.translation); // copy the object + this.drag.nodeId = undefined; - /** - * Return the positions of the nodes. - */ - Network.prototype.getPositions = function (ids) { - var dataArray = {}; - if (ids !== undefined) { - if (Array.isArray(ids) == true) { - for (var i = 0; i < ids.length; i++) { - if (this.body.nodes[ids[i]] !== undefined) { - var node = this.body.nodes[ids[i]]; - dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } else { - if (this.body.nodes[ids] !== undefined) { - var node = this.body.nodes[ids]; - dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) }; - } - } - } - return dataArray; - }; + this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); + if (node !== undefined && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + this.selectionHandler.unselectAll(); + this.selectionHandler.selectObject(node); + } - /** - * Returns true when the Network is active. - * @returns {boolean} - */ - Network.prototype.isActive = function () { - return !this.activator || this.activator.active; - }; + var selection = this.selectionHandler.selectionObj.nodes; + // create an array with the selected nodes and their original location and status + for (var nodeId in selection) { + if (selection.hasOwnProperty(nodeId)) { + var object = selection[nodeId]; + var s = { + id: object.id, + node: object, + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: object.x, + y: object.y, + xFixed: object.options.fixed.x, + yFixed: object.options.fixed.y + }; - /** - * Sets the scale - * @returns {Number} - */ - Network.prototype.setScale = function () { - return this._setScale(); - }; + object.options.fixed.x = true; + object.options.fixed.y = true; + this.drag.selection.push(s); + } + } + } + }, + writable: true, + configurable: true + }, + onDrag: { - /** - * Returns the scale - * @returns {Number} - */ - Network.prototype.getScale = function () { - return this._getScale(); - }; + /** + * handle drag event + * @private + */ + value: function onDrag(event) { + var _this = this; + if (this.drag.pinched === true) { + return; + } - /** - * Check if a node is a cluster. - * @param nodeId - * @returns {*} - */ - Network.prototype.isCluster = function (nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].isCluster; - } else { - console.log("Node does not exist."); - return false; - } - }; + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit("unlockNode"); - /** - * Returns the scale - * @returns {Number} - */ - Network.prototype.getCenterCoordinates = function () { - return this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); - }; + var pointer = this.getPointer(event.center); + var selection = this.drag.selection; + if (selection && selection.length && this.options.dragNodes === true) { + (function () { + // calculate delta's and new location + var deltaX = pointer.x - _this.drag.pointer.x; + var deltaY = pointer.y - _this.drag.pointer.y; + // update position of all selected nodes + selection.forEach(function (selection) { + var node = selection.node; + // only move the node if it was not fixed initially + if (selection.xFixed === false) { + node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } + // only move the node if it was not fixed initially + if (selection.yFixed === false) { + node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); - Network.prototype.getBoundingBox = function (nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].boundingBox; - } - }; + // start the simulation of the physics + _this.body.emitter.emit("startSimulation"); + })(); + } else { + // move the network + if (this.options.dragView === true) { + // if the drag was not started properly because the click started outside the network div, start it now. + if (this.drag.pointer === undefined) { + this._handleDragStart(event); + return; + } + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; - Network.prototype.getConnectedNodes = function (nodeId) { - var nodeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - var nodeObj = { nodeId: true }; // used to quickly check if node already exists - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - if (edge.toId == nodeId) { - if (nodeObj[edge.fromId] === undefined) { - nodeList.push(edge.fromId); - nodeObj[edge.fromId] = true; - } - } else if (edge.fromId == nodeId) { - if (nodeObj[edge.toId] === undefined) { - nodeList.push(edge.toId); - nodeObj[edge.toId] = true; + this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; + this.body.emitter.emit("_redraw"); + } } - } - } - } - return nodeList; - }; + }, + writable: true, + configurable: true + }, + onDragEnd: { - Network.prototype.getEdgesFromNode = function (nodeId) { - var edgesList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - for (var i = 0; i < node.edges.length; i++) { - edgesList.push(node.edges[i].id); - } - } - return edgesList; - }; + /** + * handle drag start event + * @private + */ + value: function onDragEnd(event) { + this.drag.dragging = false; + var selection = this.drag.selection; + if (selection && selection.length) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.options.fixed.x = s.xFixed; + s.node.options.fixed.y = s.yFixed; + }); + this.body.emitter.emit("startSimulation"); + } else { + this.body.emitter.emit("_requestRedraw"); + } - Network.prototype.generateColorObject = function (color) { - return util.parseColor(color); - }; + this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); + }, + writable: true, + configurable: true + }, + onPinch: { - module.exports = Network; -/***/ }, -/* 54 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + /** + * Handle pinch event + * @param event + * @private + */ + value: function onPinch(event) { + var pointer = this.getPointer(event.center); - /** - * Canvas shapes used by Network - */ - if (typeof CanvasRenderingContext2D !== "undefined") { - /** - * Draw a circle shape - */ - CanvasRenderingContext2D.prototype.circle = function (x, y, r) { - this.beginPath(); - this.arc(x, y, r, 0, 2 * Math.PI, false); - }; + this.drag.pinched = true; + if (this.pinch.scale === undefined) { + this.pinch.scale = 1; + } - /** - * Draw a square shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r size, width and height of the square - */ - CanvasRenderingContext2D.prototype.square = function (x, y, r) { - this.beginPath(); - this.rect(x - r, y - r, r * 2, r * 2); - }; + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.scale; + this.zoom(scale, pointer); + }, + writable: true, + configurable: true + }, + zoom: { - /** - * Draw a triangle shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.triangle = function (x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - // the change in radius and the offset is here to center the shape - r *= 1.15; - y += 0.275 * r; + /** + * 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 + */ + value: function zoom(scale, pointer) { + if (this.options.zoomView === true) { + var scaleOld = this.body.view.scale; + if (scale < 0.00001) { + scale = 0.00001; + } + if (scale > 10) { + scale = 10; + } - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height + var preScaleDragPointer = undefined; + if (this.drag !== undefined) { + if (this.drag.dragging === true) { + preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); + } + } + // + this.canvas.frame.canvas.clientHeight / 2 + var translation = this.body.view.translation; + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - this.moveTo(x, y - (h - ir)); - this.lineTo(x + s2, y + ir); - this.lineTo(x - s2, y + ir); - this.lineTo(x, y - (h - ir)); - this.closePath(); + this.body.view.scale = scale; + this.body.view.translation = { x: tx, y: ty }; - }; + if (preScaleDragPointer != undefined) { + var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + this.drag.pointer.x = postScaleDragPointer.x; + this.drag.pointer.y = postScaleDragPointer.y; + } - /** - * Draw a triangle shape in downward orientation - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius - */ - CanvasRenderingContext2D.prototype.triangleDown = function (x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); + this.body.emitter.emit("_requestRedraw"); - // the change in radius and the offset is here to center the shape - r *= 1.15; - y -= 0.275 * r; + if (scaleOld < scale) { + this.body.emitter.emit("zoom", { direction: "+" }); + } else { + this.body.emitter.emit("zoom", { direction: "-" }); + } + } + }, + writable: true, + configurable: true + }, + onMouseWheel: { - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - this.moveTo(x, y + (h - ir)); - this.lineTo(x + s2, y - ir); - this.lineTo(x - s2, y - ir); - this.lineTo(x, y + (h - ir)); - this.closePath(); - }; + /** + * 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 + */ + value: function onMouseWheel(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; + } - /** - * Draw a star shape, a star with 5 points - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.star = function (x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); + // 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 !== 0) { + // calculate the new scale + var scale = this.body.view.scale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= 1 + zoom; - // the change in radius and the offset is here to center the shape - r *= 0.82; - y += 0.1 * r; + // calculate the pointer location + var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); - for (var n = 0; n < 10; n++) { - var radius = n % 2 === 0 ? r * 1.3 : r * 0.5; - this.lineTo(x + radius * Math.sin(n * 2 * Math.PI / 10), y - radius * Math.cos(n * 2 * Math.PI / 10)); - } + // apply the new scale + this.zoom(scale, pointer); + } - this.closePath(); - }; + // Prevent default actions caused by mouse wheel. + event.preventDefault(); + }, + writable: true, + configurable: true + }, + onMouseMove: { - /** - * Draw a Diamond shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.diamond = function (x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); - this.lineTo(x, y + r); - this.lineTo(x + r, y); - this.lineTo(x, y - r); - this.lineTo(x - r, y); + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + value: function onMouseMove(event) { + var _this = this; + var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + var popupVisible = false; + // check if the previously selected node is still selected + if (this.popup !== undefined) { + if (this.popup.hidden === false) { + this._checkHidePopup(pointer); + } - this.closePath(); - }; + // if the popup was not hidden above + if (this.popup.hidden === false) { + popupVisible = true; + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.show(); + } + } - /** - * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas - */ - CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { - var r2d = Math.PI / 180; - if (w - 2 * r < 0) { - r = w / 2; - } //ensure that the radius isn't too large for x - if (h - 2 * r < 0) { - r = h / 2; - } //ensure that the radius isn't too large for y - this.beginPath(); - this.moveTo(x + r, y); - this.lineTo(x + w - r, y); - this.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); - this.lineTo(x + w, y + h - r); - this.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); - this.lineTo(x + r, y + h); - this.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false); - this.lineTo(x, y + r); - this.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); - }; + // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over. + if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) { + this.canvas.frame.focus(); + } + + // start a timeout that will check if the mouse is positioned above an element + if (popupVisible === false) { + if (this.popupTimer !== undefined) { + clearInterval(this.popupTimer); // stop any running calculationTimer + this.popupTimer = undefined; + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(function () { + return _this._checkShowPopup(pointer); + }, this.options.tooltip.delay); + } + } - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.ellipse = function (x, y, w, h) { - var kappa = 0.5522848, - ox = w / 2 * kappa, - // control point offset horizontal - oy = h / 2 * kappa, - // control point offset vertical - xe = x + w, - // x-end - ye = y + h, - // y-end - xm = x + w / 2, - // x-middle - ym = y + h / 2; // y-middle + /** + * Adding hover highlights + */ + if (this.options.hoverEnabled === 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]; + } + } - this.beginPath(); - this.moveTo(x, ym); - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - }; + // adding hover highlights + var obj = this.selectionHandler.getNodeAt(pointer); + if (obj == undefined) { + obj = this.selectionHandler.getEdgeAt(pointer); + } + if (obj != undefined) { + this.selectionHandler.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 == undefined) { + this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); + delete this.hoverObj.nodes[nodeId]; + } + } + } + this.body.emitter.emit("_requestRedraw"); + } + }, + writable: true, + configurable: true + }, + _checkShowPopup: { - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.database = function (x, y, w, h) { - var f = 1 / 3; - var wEllipse = w; - var hEllipse = h * f; - var kappa = 0.5522848, - ox = wEllipse / 2 * kappa, - // control point offset horizontal - oy = hEllipse / 2 * kappa, - // control point offset vertical - xe = x + wEllipse, - // x-end - ye = y + hEllipse, - // y-end - xm = x + wEllipse / 2, - // x-middle - ym = y + hEllipse / 2, - // y-middle - ymb = y + (h - hEllipse / 2), - // y-midlle, bottom ellipse - yeb = y + h; // y-end, bottom ellipse - this.beginPath(); - this.moveTo(xe, ym); + /** + * 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 + */ + value: function _checkShowPopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: y + }; - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = "node"; - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + // check if a node is under the cursor. + if (this.popupObj === undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodeIndices = this.body.nodeIndices; + var nodes = this.body.nodes; + var node = undefined; + var overlappingNodes = []; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + if (node.isOverlappingWith(pointerObj) === true) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(nodeIndices[i]); + } + } + } - this.lineTo(xe, ymb); + if (overlappingNodes.length > 0) { + // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others + this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; + } + } - this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); - this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + if (this.popupObj === undefined && nodeUnderCursor == false) { + // search the edges for overlap + var edgeIndices = this.body.edgeIndices; + var edges = this.body.edges; + var edge = undefined; + var overlappingEdges = []; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.isOverlappingWith(pointerObj) === true) { + if (edge.connected === true && edge.getTitle() !== undefined) { + overlappingEdges.push(edgeIndices[i]); + } + } + } - this.lineTo(x, ym); - }; + if (overlappingEdges.length > 0) { + this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = "edge"; + } + } + if (this.popupObj !== undefined) { + // show popup message window + if (this.popupObj.id != previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new Popup(this.frame, this.options.tooltip); + } - /** - * Draw an arrow point (no line) - */ - CanvasRenderingContext2D.prototype.arrow = function (x, y, angle, length) { - // tail - var xt = x - length * Math.cos(angle); - var yt = y - length * Math.sin(angle); + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; - // inner tail - // TODO: allow to customize different shapes - var xi = x - length * 0.9 * Math.cos(angle); - var yi = y - length * 0.9 * Math.sin(angle); + // 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 + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.setText(this.popupObj.getTitle()); + this.popup.show(); + } + } else { + if (this.popup) { + this.popup.hide(); + } + } + }, + writable: true, + configurable: true + }, + _checkHidePopup: { - // left - var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI); - var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI); - // right - var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); - var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + /** + * Check if the popup must be hidden, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer + * @private + */ + value: function _checkHidePopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: y + }; - this.beginPath(); - this.moveTo(x, y); - this.lineTo(xl, yl); - this.lineTo(xi, yi); - this.lineTo(xr, yr); - this.closePath(); - }; + var stillOnObj = false; + if (this.popup.popupTargetType == "node") { + if (this.body.nodes[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); - /** - * Sets up the dashedLine functionality for drawing - * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas - * @author David Jordan - * @date 2012-08-08 - */ - CanvasRenderingContext2D.prototype.dashedLine = function (x, y, x2, y2, dashArray) { - if (!dashArray) dashArray = [10, 5]; - if (dashLength == 0) dashLength = 0.001; // Hack for Safari - var dashCount = dashArray.length; - this.moveTo(x, y); - var dx = x2 - x, - dy = y2 - y; - var slope = dy / dx; - var distRemaining = Math.sqrt(dx * dx + dy * dy); - var dashIndex = 0, - draw = true; - while (distRemaining >= 0.1) { - var dashLength = dashArray[dashIndex++ % dashCount]; - if (dashLength > distRemaining) dashLength = distRemaining; - var xStep = Math.sqrt(dashLength * dashLength / (1 + slope * slope)); - if (dx < 0) xStep = -xStep; - x += xStep; - y += slope * xStep; - this[draw ? "lineTo" : "moveTo"](x, y); - distRemaining -= dashLength; - draw = !draw; + // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. + // we initially only check stillOnObj because this is much faster. + if (stillOnObj === true) { + var overNode = this.selectionHandler.getNodeAt(pointer); + stillOnObj = overNode.id == this.popup.popupTargetId; + } + } + } else { + if (this.selectionHandler.getNodeAt(pointer) === undefined) { + if (this.body.edges[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + } + } + } + + + if (stillOnObj === false) { + this.popupObj = undefined; + this.popup.hide(); + } + }, + writable: true, + configurable: true } - }; + }); - // TODO: add diamond shape - } + return InteractionHandler; + })(); + + module.exports = InteractionHandler; /***/ }, -/* 55 */ +/* 59 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** - * Parse a text source containing data in DOT language into a JSON object. - * The object contains two lists: one with nodes and one with edges. - * - * DOT language reference: http://www.graphviz.org/doc/info/lang.html - * - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graph An object containing two parameters: - * {Object[]} nodes - * {Object[]} edges + * Created by Alex on 2/27/2015. */ - function parseDOT(data) { - dot = data; - return parseGraph(); - } - // token types enumeration - var TOKENTYPE = { - NULL: 0, - DELIMITER: 1, - IDENTIFIER: 2, - UNKNOWN: 3 - }; + var Node = __webpack_require__(66); + var util = __webpack_require__(1); - // map with all delimiters - var DELIMITERS = { - "{": true, - "}": true, - "[": true, - "]": true, - ";": true, - "=": true, - ",": true, + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { + var _this = this; + _classCallCheck(this, SelectionHandler); - "->": true, - "--": true - }; + this.body = body; + this.canvas = canvas; + this.selectionObj = { nodes: [], edges: [] }; + this.forceSelectEdges = false; - var dot = ""; // current dot file - var index = 0; // current index in dot file - var c = ""; // current token character in expr - var token = ""; // current token - var tokenType = TOKENTYPE.NULL; // type of the token + this.options = {}; + this.defaultOptions = { + select: true, + selectConnectedEdges: true + }; + util.extend(this.options, this.defaultOptions); - /** - * Get the first character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function first() { - index = 0; - c = dot.charAt(0); - } + this.body.emitter.on("_dataChanged", function () { + _this.updateSelection(); + }); + } - /** - * Get the next character from the dot file. - * The character is stored into the char c. If the end of the dot file is - * reached, the function puts an empty string in c. - */ - function next() { - index++; - c = dot.charAt(index); - } + _prototypeProperties(SelectionHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + }, + writable: true, + configurable: true + }, + selectOnPoint: { - /** - * Preview the next character from the dot file. - * @return {String} cNext - */ - function nextPreview() { - return dot.charAt(index + 1); - } - /** - * Test whether given character is alphabetic or numeric - * @param {String} c - * @return {Boolean} isAlphaNumeric - */ - var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; - function isAlphaNumeric(c) { - return regexAlphaNumeric.test(c); - } - /** - * Merge all options of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - function merge(a, b) { - if (!a) { - a = {}; - } + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + var selected = false; + if (this.options.select === true) { + this.unselectAll(); + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; + if (obj !== undefined) { + selected = this.selectObject(obj); + } + this.body.emitter.emit("_requestRedraw"); + } + return selected; + }, + writable: true, + configurable: true + }, + selectAdditionalOnPoint: { + value: function selectAdditionalOnPoint(pointer) { + var selectionChanged = false; + if (this.options.select === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; - } - } - } - return a; - } + if (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); + } else { + this.selectObject(obj); + } - /** - * Set a value in an object, where the provided parameter name can be a - * path with nested parameters. For example: - * - * var obj = {a: 2}; - * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} - * - * @param {Object} obj - * @param {String} path A parameter name or dot-separated parameter path, - * like "color.highlight.border". - * @param {*} value - */ - function setValue(obj, path, value) { - var keys = path.split("."); - var o = obj; - while (keys.length) { - var key = keys.shift(); - if (keys.length) { - // this isn't the end point - if (!o[key]) { - o[key] = {}; - } - o = o[key]; - } else { - // this is the end point - o[key] = value; - } - } - } + this.body.emitter.emit("_requestRedraw"); + } + } + return selectionChanged; + }, + writable: true, + configurable: true + }, + _generateClickEvent: { + value: function _generateClickEvent(eventType, pointer) { + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: this.canvas.DOMtoCanvas(pointer) + }; + this.body.emitter.emit(eventType, properties); + }, + writable: true, + configurable: true + }, + selectObject: { + value: function selectObject(obj) { + if (obj !== undefined) { + if (obj instanceof Node) { + if (this.options.selectConnectedEdges === true || this.forceSelectEdges === true) { + this._selectConnectedEdges(obj); + } + } + obj.select(); + this._addToSelection(obj); + return true; + } + return false; + }, + writable: true, + configurable: true + }, + deselectObject: { + value: function deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); + } + }, + writable: true, + configurable: true + }, + _getAllNodesOverlappingWith: { - /** - * Add a node to a graph object. If there is already a node with - * the same id, their attributes will be merged. - * @param {Object} graph - * @param {Object} node - */ - function addNode(graph, node) { - var i, len; - var current = null; - // find root graph (in case of subgraph) - var graphs = [graph]; // list with all graphs from current graph to root graph - var root = graph; - while (root.parent) { - graphs.push(root.parent); - root = root.parent; - } - // find existing node (at root level) by its id - if (root.nodes) { - for (i = 0, len = root.nodes.length; i < len; i++) { - if (node.id === root.nodes[i].id) { - current = root.nodes[i]; - break; - } - } - } + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; + var nodes = this.body.nodes; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + return overlappingNodes; + }, + writable: true, + configurable: true + }, + _pointerToPositionObject: { - if (!current) { - // this is a new node - current = { - id: node.id - }; - if (graph.node) { - // clone default attributes - current.attr = merge(current.attr, graph.node); - } - } - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + value: function _pointerToPositionObject(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x - 1, + top: canvasPos.y + 1, + right: canvasPos.x + 1, + bottom: canvasPos.y - 1 + }; + }, + writable: true, + configurable: true + }, + getNodeAt: { - if (!g.nodes) { - g.nodes = []; - } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); - } - } - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); - } - } + /** + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | undefined} node + * @private + */ + value: function getNodeAt(pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge - */ - function addEdge(graph, edge) { - if (!graph.edges) { - graph.edges = []; - } - graph.edges.push(edge); - if (graph.edge) { - var attr = merge({}, graph.edge); // clone default attributes - edge.attr = merge(attr, edge.attr); // merge attributes - } - } + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } else { + return undefined; + } + }, + writable: true, + configurable: true + }, + _getEdgesOverlappingWith: { - /** - * Create an edge to a graph object - * @param {Object} graph - * @param {String | Number | Object} from - * @param {String | Number | Object} to - * @param {String} type - * @param {Object | null} attr - * @return {Object} edge - */ - function createEdge(graph, from, to, type, attr) { - var edge = { - from: from, - to: to, - type: type - }; - if (graph.edge) { - edge.attr = merge({}, graph.edge); // clone default attributes - } - edge.attr = merge(edge.attr || {}, attr); // merge attributes + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var i = 0; i < this.body.edgeIndices.length; i++) { + var edgeId = this.body.edgeIndices[i]; + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + }, + writable: true, + configurable: true + }, + _getAllEdgesOverlappingWith: { - return edge; - } - /** - * Get next token in the current dot file. - * The token and token type are available as token and tokenType - */ - function getToken() { - tokenType = TOKENTYPE.NULL; - token = ""; + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllEdgesOverlappingWith(object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; + }, + writable: true, + configurable: true + }, + getEdgeAt: { - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); - } - do { - var isComment = false; + /** + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {undefined} + * @private + */ + value: function getEdgeAt(pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - // skip comment - if (c == "#") { - // find the previous non-space character - var i = index - 1; - while (dot.charAt(i) == " " || dot.charAt(i) == "\t") { - i--; - } - if (dot.charAt(i) == "\n" || dot.charAt(i) == "") { - // the # is at the start of a line, this is indeed a line comment - while (c != "" && c != "\n") { - next(); - } - isComment = true; - } - } - if (c == "/" && nextPreview() == "/") { - // skip line comment - while (c != "" && c != "\n") { - next(); - } - isComment = true; - } - if (c == "/" && nextPreview() == "*") { - // skip block comment - while (c != "") { - if (c == "*" && nextPreview() == "/") { - // end of block comment found. skip these last two characters - next(); - next(); - break; + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; } else { - next(); + return undefined; } - } - isComment = true; - } - - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); - } - } while (isComment); + }, + writable: true, + configurable: true + }, + _addToSelection: { - // check for end of dot file - if (c == "") { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; - } - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; - next(); - next(); - return; - } + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + value: function _addToSelection(obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; + } + }, + writable: true, + configurable: true + }, + _addToHover: { - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; - next(); - return; - } + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + value: function _addToHover(obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; + } + }, + writable: true, + configurable: true + }, + _removeFromSelection: { - // check for an identifier (number or string) - // TODO: more precise parsing of numbers/strings (and the port separator ':') - if (isAlphaNumeric(c) || c == "-") { - token += c; - next(); - while (isAlphaNumeric(c)) { - token += c; - next(); - } - if (token == "false") { - token = false; // convert to boolean - } else if (token == "true") { - token = true; // convert to boolean - } else if (!isNaN(Number(token))) { - token = Number(token); // convert to number - } - tokenType = TOKENTYPE.IDENTIFIER; - return; - } + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + value: function _removeFromSelection(obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } else { + delete this.selectionObj.edges[obj.id]; + } + }, + writable: true, + configurable: true + }, + unselectAll: { - // check for a string enclosed by double quotes - if (c == "\"") { - next(); - while (c != "" && (c != "\"" || c == "\"" && nextPreview() == "\"")) { - token += c; - if (c == "\"") { - // skip the escape character - next(); - } - next(); - } - if (c != "\"") { - throw newSyntaxError("End of string \" expected"); - } - next(); - tokenType = TOKENTYPE.IDENTIFIER; - return; - } + /** + * Unselect all. The selectionObj is useful for this. + * + * @private + */ + value: function unselectAll() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); + } + } - // something unknown is found, wrong characters, a syntax error - tokenType = TOKENTYPE.UNKNOWN; - while (c != "") { - token += c; - next(); - } - throw new SyntaxError("Syntax error in part \"" + chop(token, 30) + "\""); - } + this.selectionObj = { nodes: {}, edges: {} }; + }, + writable: true, + configurable: true + }, + _getSelectedNodeCount: { - /** - * Parse a graph. - * @returns {Object} graph - */ - function parseGraph() { - var graph = {}; - first(); - getToken(); + /** + * return the number of selected nodes + * + * @returns {number} + * @private + */ + value: function _getSelectedNodeCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; + }, + writable: true, + configurable: true + }, + _getSelectedNode: { - // optional strict keyword - if (token == "strict") { - graph.strict = true; - getToken(); - } + /** + * return the selected node + * + * @returns {number} + * @private + */ + value: function _getSelectedNode() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return undefined; + }, + writable: true, + configurable: true + }, + _getSelectedEdge: { - // graph or digraph keyword - if (token == "graph" || token == "digraph") { - graph.type = token; - getToken(); - } + /** + * return the selected edge + * + * @returns {number} + * @private + */ + value: function _getSelectedEdge() { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return undefined; + }, + writable: true, + configurable: true + }, + _getSelectedEdgeCount: { - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); - } - // open angle bracket - if (token != "{") { - throw newSyntaxError("Angle bracket { expected"); - } - getToken(); + /** + * return the number of selected edges + * + * @returns {number} + * @private + */ + value: function _getSelectedEdgeCount() { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + }, + writable: true, + configurable: true + }, + _getSelectedObjectCount: { - // statements - parseStatements(graph); - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); - } - getToken(); + /** + * return the number of selected objects. + * + * @returns {number} + * @private + */ + value: function _getSelectedObjectCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + }, + writable: true, + configurable: true + }, + _selectionIsEmpty: { - // end of file - if (token !== "") { - throw newSyntaxError("End of file expected"); - } - getToken(); + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } + } + return true; + }, + writable: true, + configurable: true + }, + _clusterInSelection: { - // remove temporary default options - delete graph.node; - delete graph.edge; - delete graph.graph; - return graph; - } + /** + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private + */ + value: function _clusterInSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } + } + } + return false; + }, + writable: true, + configurable: true + }, + _selectConnectedEdges: { - /** - * Parse a list with statements. - * @param {Object} graph - */ - function parseStatements(graph) { - while (token !== "" && token != "}") { - parseStatement(graph); - if (token == ";") { - getToken(); - } - } - } + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); + } + }, + writable: true, + configurable: true + }, + _hoverConnectedEdges: { - /** - * Parse a single statement. Can be a an attribute statement, node - * statement, a series of node statements and edge statements, or a - * parameter. - * @param {Object} graph - */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } + }, + writable: true, + configurable: true + }, + _unselectConnectedEdges: { - return; - } - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { - return; - } + /** + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _unselectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); + } + }, + writable: true, + configurable: true + }, + blurObject: { - // parse node - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier expected"); - } - var id = token; // id can be a string or a number - getToken(); - if (token == "=") { - // id statement - getToken(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier expected"); - } - graph[id] = token; - getToken(); - // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " - } else { - parseNodeStatement(graph, id); - } - } - /** - * Parse a subgraph - * @param {Object} graph parent graph object - * @return {Object | null} subgraph - */ - function parseSubgraph(graph) { - var subgraph = null; - // optional subgraph keyword - if (token == "subgraph") { - subgraph = {}; - subgraph.type = "subgraph"; - getToken(); - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); - } - } - // open angle bracket - if (token == "{") { - getToken(); + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function blurObject(object) { + if (object.hover == true) { + object.hover = false; + this.body.emitter.emit("blurNode", { node: object.id }); + } + }, + writable: true, + configurable: true + }, + hoverObject: { - if (!subgraph) { - subgraph = {}; - } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; + /** + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @private + */ + value: function hoverObject(object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.body.emitter.emit("hoverNode", { node: object.id }); + } + } + if (object instanceof Node) { + this._hoverConnectedEdges(object); + } + }, + writable: true, + configurable: true + }, + getSelection: { - // statements - parseStatements(subgraph); - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); - } - getToken(); - // remove temporary default options - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; - } - graph.subgraphs.push(subgraph); - } + /** + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection + */ + value: function getSelection() { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; + }, + writable: true, + configurable: true + }, + getSelectedNodes: { - return subgraph; - } + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedNodes() { + var idArray = []; + if (this.options.select == true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } + } + return idArray; + }, + writable: true, + configurable: true + }, + getSelectedEdges: { - /** - * parse an attribute statement like "node [shape=circle fontSize=16]". - * Available keywords are 'node', 'edge', 'graph'. - * The previous list with default attributes will be replaced - * @param {Object} graph - * @returns {String | null} keyword Returns the name of the parsed attribute - * (node, edge, graph), or null if nothing - * is parsed. - */ - function parseAttributeStatement(graph) { - // attribute statements - if (token == "node") { - getToken(); + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedEdges() { + var idArray = []; + if (this.options.select == true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } + } + } + return idArray; + }, + writable: true, + configurable: true + }, + selectNodes: { - // node attributes - graph.node = parseAttributeList(); - return "node"; - } else if (token == "edge") { - getToken(); - // edge attributes - graph.edge = parseAttributeList(); - return "edge"; - } else if (token == "graph") { - getToken(); + /** + * select zero or more nodes with the option to highlight edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + * @param {boolean} [highlightEdges] + */ + value: function selectNodes(selection, highlightEdges) { + var i, iMax, id; - // graph attributes - graph.graph = parseAttributeList(); - return "graph"; - } + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - return null; - } + // first unselect any selected node + this.unselectAll(true); - /** - * parse a node statement - * @param {Object} graph - * @param {String | Number} id - */ - function parseNodeStatement(graph, id) { - // node statement - var node = { - id: id - }; - var attr = parseAttributeList(); - if (attr) { - node.attr = attr; - } - addNode(graph, node); + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - // edge statements - parseEdge(graph, id); - } + var node = this.body.nodes[id]; + if (!node) { + throw new RangeError("Node with id \"" + id + "\" not found"); + } + this._selectObject(node, true, true, highlightEdges, true); + } + this.redraw(); + }, + writable: true, + configurable: true + }, + selectEdges: { - /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node - */ - function parseEdge(graph, from) { - while (token == "->" || token == "--") { - var to; - var type = token; - getToken(); - var subgraph = parseSubgraph(graph); - if (subgraph) { - to = subgraph; - } else { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier or subgraph expected"); - } - to = token; - addNode(graph, { - id: to - }); - getToken(); - } + /** + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + value: function selectEdges(selection) { + var i, iMax, id; - // parse edge attributes - var attr = parseAttributeList(); + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); + // first unselect any selected node + this.unselectAll(true); - from = to; - } - } + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr - */ - function parseAttributeList() { - var attr = null; + var edge = this.body.edges[id]; + if (!edge) { + throw new RangeError("Edge with id \"" + id + "\" not found"); + } + this._selectObject(edge, true, true, false, true); + } + this.redraw(); + }, + writable: true, + configurable: true + }, + updateSelection: { - while (token == "[") { - getToken(); - attr = {}; - while (token !== "" && token != "]") { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute name expected"); - } - var name = token; + /** + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ + value: function updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } + } + }, + writable: true, + configurable: true + } + }); - getToken(); - if (token != "=") { - throw newSyntaxError("Equal sign = expected"); - } - getToken(); + return SelectionHandler; + })(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute value expected"); - } - var value = token; - setValue(attr, name, value); // name can be a path + module.exports = SelectionHandler; + +/***/ }, +/* 60 */ +/***/ function(module, exports, __webpack_require__) { - getToken(); - if (token == ",") { - getToken(); - } - } + "use strict"; - if (token != "]") { - throw newSyntaxError("Bracket ] expected"); - } - getToken(); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - return attr; - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Create a syntax error with extra information on current token and index. - * @param {String} message - * @returns {SyntaxError} err + * Created by Alex on 3/3/2015. */ - function newSyntaxError(message) { - return new SyntaxError(message + ", got \"" + chop(token, 30) + "\" (char " + index + ")"); - } - /** - * Chop off text after a maximum length - * @param {String} text - * @param {Number} maxLength - * @returns {String} - */ - function chop(text, maxLength) { - return text.length <= maxLength ? text : text.substr(0, 27) + "..."; - } + var util = __webpack_require__(1); - /** - * Execute a function fn for each pair of elements in two arrays - * @param {Array | *} array1 - * @param {Array | *} array2 - * @param {function} fn - */ - function forEach2(array1, array2, fn) { - if (Array.isArray(array1)) { - array1.forEach(function (elem1) { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(elem1, elem2); - }); - } else { - fn(elem1, array2); - } - }); - } else { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } else { - fn(array1, array2); - } - } - } + var LayoutEngine = (function () { + function LayoutEngine(body) { + var _this = this; + _classCallCheck(this, LayoutEngine); - /** - * Convert a string containing a graph in DOT language into a map containing - * with nodes and edges in the format of graph. - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graphData - */ - function DOTToGraph(data) { - // parse the DOT file - var dotData = parseDOT(data); - var graphData = { - nodes: [], - edges: [], - options: {} - }; + this.body = body; - // copy the nodes - if (dotData.nodes) { - dotData.nodes.forEach(function (dotNode) { - var graphNode = { - id: dotNode.id, - label: String(dotNode.label || dotNode.id) - }; - merge(graphNode, dotNode.attr); - if (graphNode.image) { - graphNode.shape = "image"; + this.initialRandomSeed = Math.round(Math.random() * 1000000); + this.randomSeed = this.initialRandomSeed; + this.options = {}; + this.defaultOptions = { + randomSeed: undefined, + hierarchical: { + enabled: false, + levelSeparation: 150, + direction: "UD", // UD, DU, LR, RL + sortMethod: "hubsize" // hubsize, directed } - graphData.nodes.push(graphNode); + }; + util.extend(this.options, this.defaultOptions); + + this.hierarchicalLevels = {}; + + this.body.emitter.on("_dataChanged", function () { + _this.setupHierarchicalLayout(); }); } - // copy the edges - if (dotData.edges) { - /** - * Convert an edge in DOT format to an edge with VisGraph format - * @param {Object} dotEdge - * @returns {Object} graphEdge - */ - var convertEdge = function (dotEdge) { - var graphEdge = { - from: dotEdge.from, - to: dotEdge.to - }; - merge(graphEdge, dotEdge.attr); - graphEdge.style = dotEdge.type == "->" ? "arrow" : "line"; - return graphEdge; - }; + _prototypeProperties(LayoutEngine, null, { + setOptions: { + value: function setOptions(options, allOptions) { + if (options !== undefined) { + util.mergeOptions(this.options, options, "hierarchical"); + if (options.randomSeed !== undefined) { + this.randomSeed = options.randomSeed; + } - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } else { - from = { - id: dotEdge.from - }; - } + if (this.options.hierarchical.enabled === true) { + // make sure the level seperation is the right way up + if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "DU") { + if (this.options.hierarchical.levelSeparation > 0) { + this.options.hierarchical.levelSeparation *= -1; + } + } else { + if (this.options.hierarchical.levelSeparation < 0) { + this.options.hierarchical.levelSeparation *= -1; + } + } - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } else { - to = { - id: dotEdge.to - }; - } + // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. + return this.adaptAllOptions(allOptions); + } + } + return allOptions; + }, + writable: true, + configurable: true + }, + adaptAllOptions: { + value: function adaptAllOptions(allOptions) { + if (this.options.hierarchical.enabled === true) { + // set the physics + if (allOptions.physics === undefined || allOptions.physics === true) { + allOptions.physics = { solver: "hierarchicalRepulsion" }; + } else if (options.physics !== false) { + allOptions.physics.solver = "hierarchicalRepulsion"; + } - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } + // get the type of static smooth curve in case it is required + var type = "horizontal"; + if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "LR") { + type = "vertical"; + } - forEach2(from, to, function (from, to) { - var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); + // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. + if (allOptions.edges === undefined) { + allOptions.edges = { smooth: false }; + } else if (allOptions.edges.smooth === undefined) { + allOptions.edges.smooth = false; + } else { + allOptions.edges.smooth = { enabled: true, dynamic: false, type: type }; + } - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - }); - } + // force all edges into static smooth curves. + this.body.emitter.emit("_forceDisableDynamicCurves", type); + } + return allOptions; + }, + writable: true, + configurable: true + }, + seededRandom: { + value: function seededRandom() { + var x = Math.sin(this.randomSeed++) * 10000; + return x - Math.floor(x); + }, + writable: true, + configurable: true + }, + positionInitially: { + value: function positionInitially(nodesArray) { + if (this.options.hierarchical.enabled !== true) { + for (var i = 0; i < nodesArray.length; i++) { + var node = nodesArray[i]; + if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { + var radius = 10 * 0.1 * nodesArray.length + 10; + var angle = 2 * Math.PI * this.seededRandom(); - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; - } + if (node.options.fixed.x == false) { + node.x = radius * Math.cos(angle); + } + if (node.options.fixed.x == false) { + node.y = radius * Math.sin(angle); + } + } + } + } + }, + writable: true, + configurable: true + }, + getSeed: { + value: function getSeed() { + return this.initialRandomSeed; + }, + writable: true, + configurable: true + }, + setupHierarchicalLayout: { - return graphData; - } + /** + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * + * @private + */ + value: function setupHierarchicalLayout() { + if (this.options.hierarchical.enabled == true && this.body.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var node = undefined, + nodeId = undefined; + var definedLevel = false; + var undefinedLevel = false; + this.hierarchicalLevels = {}; + this.nodeSpacing = 100; - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.options.level !== undefined) { + definedLevel = true; + this.hierarchicalLevels[nodeId] = node.options.level; + } else { + undefinedLevel = true; + } + } + } -/***/ }, -/* 56 */ -/***/ function(module, exports, __webpack_require__) { + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel == true && definedLevel == true) { + throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); + return; + } else { + // setup the system to use hierarchical method. + //this._changeConstants(); - "use strict"; + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + if (this.options.hierarchical.sortMethod == "hubsize") { + this._determineLevelsByHubsize(); + } else if (this.options.hierarchical.sortMethod == "directed" || "direction") { + this._determineLevelsDirected(); + } + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); - function parseGephi(gephiJSON, options) { - var edges = []; - var nodes = []; - this.options = { - edges: { - inheritColor: true + // place the nodes on the canvas. + this._placeNodesByHierarchy(distribution); + } + } + }, + writable: true, + configurable: true }, - nodes: { - allowedToMove: false, - parseColor: false - } - }; + _placeNodesByHierarchy: { - if (options !== undefined) { - this.options.nodes.allowedToMove = options.allowedToMove | false; - this.options.nodes.parseColor = options.parseColor | false; - this.options.edges.inheritColor = options.inheritColor | true; - } + /** + * This function places the nodes on the canvas based on the hierarchial distribution. + * + * @param {Object} distribution | obtained by the function this._getDistribution() + * @private + */ + value: function _placeNodesByHierarchy(distribution) { + var nodeId = undefined, + node = undefined; + this.positionedNodes = {}; - var gEdges = gephiJSON.edges; - var gNodes = gephiJSON.nodes; - for (var i = 0; i < gEdges.length; i++) { - var edge = {}; - var gEdge = gEdges[i]; - edge.id = gEdge.id; - edge.from = gEdge.source; - edge.to = gEdge.target; - edge.attributes = gEdge.attributes; - // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; - // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; - edge.color = gEdge.color; - edge.inheritColor = edge.color !== undefined ? false : this.options.inheritColor; - edges.push(edge); - } + // start placing all the level 0 nodes first. Then recursively position their branches. + for (var level in distribution) { + if (distribution.hasOwnProperty(level)) { + for (nodeId in distribution[level].nodes) { + if (distribution[level].nodes.hasOwnProperty(nodeId)) { + node = distribution[level].nodes[nodeId]; + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + if (node.x === undefined) { + node.x = distribution[level].distance; + } + distribution[level].distance = node.x + this.nodeSpacing; + } else { + if (node.y === undefined) { + node.y = distribution[level].distance; + } + distribution[level].distance = node.y + this.nodeSpacing; + } - for (var i = 0; i < gNodes.length; i++) { - var node = {}; - var gNode = gNodes[i]; - node.id = gNode.id; - node.attributes = gNode.attributes; - node.x = gNode.x; - node.y = gNode.y; - node.label = gNode.label; - if (this.options.nodes.parseColor == true) { - node.color = gNode.color; - } else { - node.color = gNode.color !== undefined ? { background: gNode.color, border: gNode.color } : undefined; - } - node.radius = gNode.size; - node.allowedToMoveX = this.options.nodes.allowedToMove; - node.allowedToMoveY = this.options.nodes.allowedToMove; - nodes.push(node); - } + this.positionedNodes[nodeId] = true; + this._placeBranchNodes(node.edges, node.id, distribution, level); + } + } + } + } + }, + writable: true, + configurable: true + }, + _getDistribution: { + + + /** + * This function get the distribution of levels based on hubsize + * + * @returns {Object} + * @private + */ + value: function _getDistribution() { + var distribution = {}; + var nodeId = undefined, + node = undefined; + + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + node.y = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; + node.options.fixed.y = true; + } else { + node.x = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; + node.options.fixed.x = true; + } + if (distribution[this.hierarchicalLevels[nodeId]] === undefined) { + distribution[this.hierarchicalLevels[nodeId]] = { amount: 0, nodes: {}, distance: 0 }; + } + distribution[this.hierarchicalLevels[nodeId]].amount += 1; + distribution[this.hierarchicalLevels[nodeId]].nodes[nodeId] = node; + } + } + return distribution; + }, + writable: true, + configurable: true + }, + _getHubSize: { + + + /** + * Get the hubsize from all remaining unlevelled nodes. + * + * @returns {number} + * @private + */ + value: function _getHubSize() { + var hubSize = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (this.hierarchicalLevels[nodeId] === undefined) { + hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; + } + } + } + return hubSize; + }, + writable: true, + configurable: true + }, + _determineLevelsByHubsize: { + + + /** + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize + * @private + */ + value: function _determineLevelsByHubsize() { + var nodeId = undefined, + node = undefined; + var hubSize = 1; + + while (hubSize > 0) { + // determine hubs + hubSize = this._getHubSize(); + if (hubSize == 0) break; + + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length == hubSize) { + this._setLevel(0, node); + } + } + } + } + }, + writable: true, + configurable: true + }, + _setLevel: { + + + /** + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * + * @param level + * @param edges + * @param parentId + * @private + */ + value: function _setLevel(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) { + return; + }var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId == node.id) { + childNode = node.edges[i].from; + } else { + childNode = node.edges[i].to; + } + this._setLevel(level + 1, childNode); + } + }, + writable: true, + configurable: true + }, + _determineLevelsDirected: { - return { nodes: nodes, edges: edges }; - } - exports.parseGephi = parseGephi; -/***/ }, -/* 57 */ -/***/ function(module, exports, __webpack_require__) { + /** + * this function allocates nodes in levels based on the direction of the edges + * + * @param hubsize + * @private + */ + value: function _determineLevelsDirected() { + var nodeId = undefined, + node = undefined; + var minLevel = 10000; - "use strict"; + // set first node to source + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + this._setLevelDirected(minLevel, node); + } + } - /** - * @class Images - * This class loads images and keeps them stored. - */ - function Images(callback) { - this.images = {}; - this.imageBroken = {}; - this.callback = callback; - } + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; + } + } - /** - * - * @param {string} url Url of the image - * @param {string} url Url of an image to use if the url image is not found - * @return {Image} img The image object - */ - Images.prototype.load = function (url, brokenUrl) { - var img = this.images[url]; // make a pointer - if (img === undefined) { - // create the image - var me = this; - img = new Image(); - img.onload = function () { - // IE11 fix -- thanks dponch! - if (this.width == 0) { - document.body.appendChild(this); - this.width = this.offsetWidth; - this.height = this.offsetHeight; - document.body.removeChild(this); - } + // subtract the minimum from the set so we have a range starting from 0 + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.hierarchicalLevels[nodeId] -= minLevel; + } + } + }, + writable: true, + configurable: true + }, + _setLevelDirected: { - if (me.callback) { - me.images[url] = img; - me.callback(this); - } - }; - img.onerror = function () { - if (brokenUrl === undefined) { - console.error("Could not load image:", url); - delete this.src; - if (me.callback) { - me.callback(this); - } - } else { - if (me.imageBroken[url] === true) { - console.error("Could not load brokenImage:", brokenUrl); - delete this.src; - if (me.callback) { - me.callback(this); + /** + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction + * + * @param level + * @param edges + * @param parentId + * @private + */ + value: function _setLevelDirected(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) { + return; + }var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId == node.id) { + childNode = node.edges[i].from; + this._setLevelDirected(level - 1, childNode); + } else { + childNode = node.edges[i].to; + this._setLevelDirected(level + 1, childNode); } - } else { - console.error("Could not load image:", url); - this.src = brokenUrl; - me.imageBroken[url] = true; } - } - }; + }, + writable: true, + configurable: true + }, + _placeBranchNodes: { - img.src = url; - } - return img; - }; - module.exports = Images; + /** + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. + * + * @param edges + * @param parentId + * @param distribution + * @param parentLevel + * @private + */ + value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = undefined; + var parentNode = undefined; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + parentNode = edges[i].to; + } else { + childNode = edges[i].to; + parentNode = edges[i].from; + } + var childNodeLevel = this.hierarchicalLevels[childNode.id]; + if (this.positionedNodes[childNode.id] === undefined) { + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + if (childNodeLevel > parentLevel) { + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + if (childNode.x === undefined) { + childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); + } + distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; + this.positionedNodes[childNode.id] = true; + } else { + if (childNode.y === undefined) { + childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); + } + distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; + } + this.positionedNodes[childNode.id] = true; + + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); + } + } + } + } + }, + writable: true, + configurable: true + } + }); + + return LayoutEngine; + })(); + + module.exports = LayoutEngine; /***/ }, -/* 58 */ +/* 61 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -25053,6179 +24010,7075 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var util = __webpack_require__(1); + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(43); + var locales = __webpack_require__(77); /** - * @class Groups - * This class can store groups and options specific for groups. + * clears the toolbar div element of children + * + * @private */ - var Groups = (function () { - function Groups() { - _classCallCheck(this, Groups); - - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; + var ManipulationSystem = (function () { + function ManipulationSystem(body, canvas, selectionHandler) { + _classCallCheck(this, ManipulationSystem); - this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue - { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow - { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red - { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green - { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta - { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple - { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange - { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue - { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink - { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; - { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red + this.editMode = false; + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; - { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange - { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue - { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green - { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta - { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple - { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue - { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink - { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint + this.manipulationHammers = []; + this.temporaryUIFunctions = {}; + this.temporaryEventFunctions = []; - { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; + this.touchTime = 0; + this.temporaryIds = { nodes: [], edges: [] }; + this.guiEnabled = false; + this.selectedControlNode = undefined; this.options = {}; this.defaultOptions = { - useDefaultGroups: true + enabled: false, + initiallyVisible: false, + locale: "en", + locales: locales, + functionality: { + addNode: true, + addEdge: true, + editNode: true, + editEdge: true, + deleteNode: true, + deleteEdge: true + }, + handlerFunctions: { + addNode: undefined, + addEdge: undefined, + editNode: undefined, + editEdge: undefined, + deleteNode: undefined, + deleteEdge: undefined + }, + controlNodeStyle: { + shape: "dot", + size: 6, + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } }, + borderWidth: 2, + borderWidthSelected: 2 + } }; util.extend(this.options, this.defaultOptions); } - _prototypeProperties(Groups, null, { + _prototypeProperties(ManipulationSystem, null, { setOptions: { - value: function setOptions(options) { - var optionFields = ["useDefaultGroups"]; + + /** + * Set the Options + * @param options + */ + value: function setOptions(options) { if (options !== undefined) { - for (var groupName in options) { - if (options.hasOwnProperty(groupName)) { - if (optionFields.indexOf(groupName) == -1) { - var group = options[groupName]; - this.add(groupName, group); + if (typeof options == "boolean") { + this.options.enabled = options; + } else { + this.options.enabled = true; + for (var prop in options) { + if (options.hasOwnProperty(prop)) { + this.options[prop] = options[prop]; } } } + if (this.options.initiallyVisible === true) { + this.editMode = true; + } + this._setup(); } }, writable: true, configurable: true }, - clear: { - + toggleEditMode: { - /** - * Clear all groups - */ - value: function clear() { - this.groups = {}; - this.groupsArray = []; - }, - writable: true, - configurable: true - }, - get: { /** - * get group options of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group options + * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. + * + * @private */ - value: function get(groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; + value: function toggleEditMode() { + this.editMode = !this.editMode; + if (this.guiEnabled === true) { + var toolbar = this.manipulationDiv; + var closeDiv = this.closeDiv; + var editModeDiv = this.editModeDiv; + if (this.editMode === true) { + toolbar.style.display = "block"; + closeDiv.style.display = "block"; + editModeDiv.style.display = "none"; + this._bindHammerToDiv(closeDiv, this.toggleEditMode.bind(this)); + this.showManipulatorToolbar(); } else { - // create new group - var index = this.defaultIndex % this.defaultGroups.length; - this.defaultIndex++; - group = {}; - group.color = this.defaultGroups[index]; - this.groups[groupname] = group; + toolbar.style.display = "none"; + closeDiv.style.display = "none"; + editModeDiv.style.display = "block"; + this._createEditButton(); } } - - return group; }, writable: true, configurable: true }, - add: { + showManipulatorToolbar: { + /** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object + * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * + * @private */ - value: function add(groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; - }, - writable: true, - configurable: true - } - }); - - return Groups; - })(); + value: function showManipulatorToolbar() { + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - module.exports = Groups; - // 20:bright red + // reset global letiables + this.manipulationDOM = {}; -/***/ }, -/* 59 */ -/***/ function(module, exports, __webpack_require__) { + var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); + var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); + var selectedTotalCount = selectedNodeCount + selectedEdgeCount; + var locale = this.options.locales[this.options.locale]; + var needSeperator = false; - "use strict"; + if (this.options.functionality.addNode === true) { + this._createAddNodeButton(locale); + needSeperator = true; + } + if (this.options.functionality.addEdge === true) { + if (needSeperator === true) { + this._createSeperator(1); + } else { + needSeperator = true; + } + this._createAddEdgeButton(locale); + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + if (selectedNodeCount === 1 && typeof this.options.handlerFunctions.editNode === "function" && this.options.functionality.editNode === true) { + if (needSeperator === true) { + this._createSeperator(2); + } else { + needSeperator = true; + } + this._createEditNodeButton(locale); + } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.functionality.editEdge === true) { + if (needSeperator === true) { + this._createSeperator(3); + } else { + needSeperator = true; + } + this._createEditEdgeButton(locale); + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + // remove buttons + if (selectedTotalCount !== 0) { + if (selectedNodeCount === 1 && this.options.functionality.deleteNode === true) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } else if (selectedNodeCount === 0 && this.options.functionality.deleteEdge === true) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - /** - * Created by Alex on 3/4/2015. - */ + // refresh this bar based on what has been selected + this._temporaryBindEvent("select", this.showManipulatorToolbar.bind(this)); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); + // redraw to show any possible changes + this.body.emitter.emit("_redraw"); + }, + writable: true, + configurable: true + }, + addNodeMode: { - var Node = _interopRequire(__webpack_require__(60)); - var NodesHandler = (function () { - function NodesHandler(body, images, groups, layoutEngine) { - var _this = this; - _classCallCheck(this, NodesHandler); + /** + * Create the toolbar for adding Nodes + * + * @private + */ + value: function addNodeMode() { + // clear the toolbar + this._clean(); - this.body = body; - this.images = images; - this.groups = groups; - this.layoutEngine = layoutEngine; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale.addDescription); - // create the node API in the body container - this.body.functions.createNode = this.create.bind(this); + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } - this.nodesListeners = { - add: function (event, params) { - _this.add(params.items); - }, - update: function (event, params) { - _this.update(params.items, params.data); + this._temporaryBindEvent("click", this._performAddNode.bind(this)); }, - remove: function (event, params) { - _this.remove(params.items); - } - }; - + writable: true, + configurable: true + }, + editNode: { - this.options = {}; - this.defaultOptions = { - borderWidth: 1, - borderWidthSelected: undefined, - color: { - border: "#2B7CE9", - background: "#97C2FC", - highlight: { - border: "#2B7CE9", - background: "#D2E5FF" - }, - hover: { - border: "#2B7CE9", - background: "#D2E5FF" - } - }, - fixed: { - x: false, - y: false - }, - font: { - color: "#343434", - size: 14, // px - face: "arial", - background: "none", - stroke: 0, // px - strokeColor: "white", - align: "horizontal" - }, - group: undefined, - hidden: false, - icon: { - face: undefined, //'FontAwesome', - code: undefined, //'\uf007', - size: undefined, //50, - color: undefined //'#aa00ff' - }, - image: undefined, // --> URL - label: undefined, - level: undefined, - mass: 1, - physics: true, - scaling: { - min: 10, - max: 30, - label: { - enabled: true, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 3 - }, - customScalingFunction: function (min, max, total, value) { - if (max == min) { - return 0.5; + /** + * call the bound function to handle the editing of the node. The node has to be selected. + * + * @private + */ + value: function editNode() { + var _this = this; + if (typeof this.options.handlerFunctions.editNode === "function") { + var node = this.selectionHandler._getSelectedNode(); + if (node.isCluster !== true) { + var data = util.deepExtend({}, node.options, true); + data.x = node.x; + data.y = node.y; + + if (this.options.handlerFunctions.editNode.length == 2) { + this.options.handlerFunctions.editNode(data, function (finalizedData) { + _this.body.data.nodes.update(finalizedData); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for edit does not support two arguments (data, callback)"); + } } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); + alert(this.options.locales[this.options.locale].editClusterError); } + } else { + throw new Error("No function has been configured to handle the editing of nodes."); } }, - shape: "ellipse", - size: 25, - value: 1, - x: undefined, - y: undefined - }; - util.extend(this.options, this.defaultOptions); - } + writable: true, + configurable: true + }, + addEdgeMode: { - _prototypeProperties(NodesHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options) { - util.selectiveNotDeepExtend(["color"], this.options, options); - if (options.color) { - this.options.color = util.parseColor(options.color); - } + + /** + * create the toolbar to connect nodes + * + * @private + */ + value: function addEdgeMode() { + // _clean the system + this._clean(); + + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale.edgeDescription); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } + + // temporarily overload functions + this._temporaryBindUI("onTouch", this._handleConnect.bind(this)); + this._temporaryBindUI("onDragEnd", this._finishConnect.bind(this)); + this._temporaryBindUI("onHold", function () {}); }, writable: true, configurable: true }, - setData: { + editEdgeMode: { /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * create the toolbar to edit edges + * * @private */ - value: function setData(nodes) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var oldNodesData = this.body.data.nodes; + value: function editEdgeMode() { + // clear the system + this._clean(); - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.body.data.nodes = nodes; - } else if (Array.isArray(nodes)) { - this.body.data.nodes = new DataSet(); - this.body.data.nodes.add(nodes); - } else if (!nodes) { - this.body.data.nodes = new DataSet(); - } else { - throw new TypeError("Array or DataSet expected"); - } + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale.editEdgeDescription); - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.off(event, callback); - }); + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } - // remove drawn nodes - this.body.nodes = {}; + this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; + var edge = this.body.edges[this.edgeBeingEditedId]; - if (this.body.data.nodes) { - // subscribe to new dataset - var me = this; - util.forEach(this.nodesListeners, function (callback, event) { - me.body.data.nodes.on(event, callback); - }); + // create control nodes + var controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); + var controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); - // draw all new nodes - var ids = this.body.data.nodes.getIds(); - this.add(ids, true); + this.temporaryIds.nodes.push(controlNodeFrom.id); + this.temporaryIds.nodes.push(controlNodeTo.id); + + this.body.nodes[controlNodeFrom.id] = controlNodeFrom; + this.body.nodeIndices.push(controlNodeFrom.id); + this.body.nodes[controlNodeTo.id] = controlNodeTo; + this.body.nodeIndices.push(controlNodeTo.id); + + // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI + this._temporaryBindUI("onTouch", this._controlNodeTouch.bind(this)); // used to get the position + this._temporaryBindUI("onTap", function () {}); // disabled + this._temporaryBindUI("onHold", function () {}); // disabled + this._temporaryBindUI("onDragStart", this._controlNodeDragStart.bind(this)); // used to select control node + this._temporaryBindUI("onDrag", this._controlNodeDrag.bind(this)); // used to drag control node + this._temporaryBindUI("onDragEnd", this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes + this._temporaryBindUI("onMouseMove", function () {}); // disabled + + // create function to position control nodes correctly on movement + // automatically cleaned up because we use the temporary bind + this._temporaryBindEvent("beforeDrawing", function (ctx) { + var positions = edge.edgeType.findBorderPositions(ctx); + if (controlNodeFrom.selected === false) { + controlNodeFrom.x = positions.from.x; + controlNodeFrom.y = positions.from.y; + } + if (controlNodeTo.selected === false) { + controlNodeTo.x = positions.to.x; + controlNodeTo.y = positions.to.y; + } + }); + + this.body.emitter.emit("_redraw"); + }, + writable: true, + configurable: true + }, + deleteSelected: { + + /** + * delete everything in the selection + * + * @private + */ + value: function deleteSelected() { + var _this = this; + var selectedNodes = this.selectionHandler.getSelectedNodes(); + var selectedEdges = this.selectionHandler.getSelectedEdges(); + var deleteFunction = undefined; + if (selectedNodes.length > 0) { + for (var i = 0; i < selectedNodes.length; i++) { + if (this.body.nodes[selectedNodes[i]].isCluster === true) { + alert(this.options.locales[this.options.locale].deleteClusterError); + return; + } + } + + if (typeof this.options.handlerFunctions.deleteNode === "function") { + deleteFunction = this.options.handlerFunctions.deleteNode; + } + } else if (selectedEdges.length > 0) { + if (typeof this.options.handlerFunctions.deleteEdge === "function") { + deleteFunction = this.options.handlerFunctions.deleteEdge; + } } - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); + if (typeof deleteFunction === "function") { + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (deleteFunction.length == 2) { + deleteFunction(data, function (finalizedData) { + _this.body.data.edges.remove(finalizedData.edges); + _this.body.data.nodes.remove(finalizedData.nodes); + _this.body.emitter.emit("startSimulation"); + }); + } else { + throw new Error("The function for delete does not support two arguments (data, callback)"); + } + } else { + this.body.data.edges.remove(selectedEdges); + this.body.data.nodes.remove(selectedNodes); + this.body.emitter.emit("startSimulation"); } }, writable: true, configurable: true }, - add: { + _setup: { + + + //********************************************** PRIVATE ***************************************// + /** - * Add nodes - * @param {Number[] | String[]} ids + * draw or remove the DOM * @private */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var id; - var newNodes = []; - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - var properties = this.body.data.nodes.get(id); - var node = this.create(properties);; - newNodes.push(node); - this.body.nodes[id] = node; // note: this may replace an existing node - } + value: function _setup() { + if (this.options.enabled === true) { + // Enable the GUI + this.guiEnabled = true; - this.layoutEngine.positionInitially(newNodes); + // remove override + this.selectionHandler.forceSelectEdges = true; - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); + this._createWrappers(); + if (this.editMode === false) { + this._createEditButton(); + } else { + this.showManipulatorToolbar(); + } + } else { + this._removeManipulationDOM(); + + // disable the gui + this.guiEnabled = false; } }, writable: true, configurable: true }, - update: { + _createWrappers: { + /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids + * create the div overlays that contain the DOM * @private */ - value: function update(ids, changedData) { - var nodes = this.body.nodes; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = changedData[i]; - if (node !== undefined) { - // update node - node.setOptions(data, this.constants); + value: function _createWrappers() { + // load the manipulator HTML elements. All styling done in css. + if (this.manipulationDiv === undefined) { + this.manipulationDiv = document.createElement("div"); + this.manipulationDiv.className = "network-manipulationDiv"; + if (this.editMode === true) { + this.manipulationDiv.style.display = "block"; } else { - dataChanged = true; - // create node - node = this.create(properties); - nodes[id] = node; + this.manipulationDiv.style.display = "none"; } + this.canvas.frame.appendChild(this.manipulationDiv); } - if (dataChanged === true) { - this.body.emitter.emit("_dataChanged"); - } else { - this.body.emitter.emit("_dataUpdated"); + // container for the edit button. + if (this.editModeDiv === undefined) { + this.editModeDiv = document.createElement("div"); + this.editModeDiv.className = "network-manipulation-editMode"; + if (this.editMode === true) { + this.editModeDiv.style.display = "none"; + } else { + this.editModeDiv.style.display = "block"; + } + this.canvas.frame.appendChild(this.editModeDiv); + } + + + // container for the close div button + if (this.closeDiv === undefined) { + this.closeDiv = document.createElement("div"); + this.closeDiv.className = "network-manipulation-closeDiv"; + this.closeDiv.style.display = this.manipulationDiv.style.display; + this.canvas.frame.appendChild(this.closeDiv); } }, writable: true, configurable: true }, - remove: { + _getNewTargetNode: { + /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids + * generate a new target node. Used for creating new edges and editing edges + * @param x + * @param y + * @returns {*} * @private */ - value: function remove(ids) { - var nodes = this.body.nodes; + value: function _getNewTargetNode(x, y) { + var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - delete nodes[id]; - } + controlNodeStyle.id = "targetNode" + util.randomUUID(); + controlNodeStyle.hidden = false; + controlNodeStyle.physics = false; + controlNodeStyle.x = x; + controlNodeStyle.y = y; - this.body.emitter.emit("_dataChanged"); + return this.body.functions.createNode(controlNodeStyle); }, writable: true, configurable: true }, - create: { - value: function create(properties) { - var constructorClass = arguments[1] === undefined ? Node : arguments[1]; - return new constructorClass(properties, this.body, this.images, this.groups, this.options); + _createEditButton: { + + + /** + * Create the edit button + */ + value: function _createEditButton() { + // restore everything to it's original state (if applicable) + this._clean(); + + // reset the manipulationDOM + this.manipulationDOM = {}; + + // empty the editModeDiv + util.recursiveDOMDelete(this.editModeDiv); + + // create the contents for the editMode button + var locale = this.options.locales[this.options.locale]; + var button = this._createButton("editMode", "network-manipulationUI edit editmode", locale.edit); + this.editModeDiv.appendChild(button); + + // bind a hammer listener to the button, calling the function toggleEditMode. + this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); }, writable: true, configurable: true - } - }); - - return NodesHandler; - })(); + }, + _clean: { - module.exports = NodesHandler; -/***/ }, -/* 60 */ -/***/ function(module, exports, __webpack_require__) { + /** + * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. + * @private + */ + value: function _clean() { + // _clean the divs + if (this.guiEnabled === true) { + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.manipulationDiv); - "use strict"; + // removes all the bindings and overloads + this._cleanManipulatorHammers(); + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + // remove temporary nodes and edges + this._cleanupTemporaryNodesAndEdges(); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + // restore overloaded UI functions + this._unbindTemporaryUIs(); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // remove the temporaryEventFunctions + this._unbindTemporaryEvents(); - var util = __webpack_require__(1); + // restore the physics if required + this.body.emitter.emit("restorePhysics"); + }, + writable: true, + configurable: true + }, + _cleanManipulatorHammers: { - var Label = _interopRequire(__webpack_require__(61)); - var Box = _interopRequire(__webpack_require__(62)); + /** + * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. + * @private + */ + value: function _cleanManipulatorHammers() { + // _clean hammer bindings + if (this.manipulationHammers.length != 0) { + for (var i = 0; i < this.manipulationHammers.length; i++) { + this.manipulationHammers[i].destroy(); + } + this.manipulationHammers = []; + } + }, + writable: true, + configurable: true + }, + _removeManipulationDOM: { - var Circle = _interopRequire(__webpack_require__(64)); - var CircularImage = _interopRequire(__webpack_require__(66)); + /** + * Remove all DOM elements created by this module. + * @private + */ + value: function _removeManipulationDOM() { + // removes all the bindings and overloads + this._clean(); - var Database = _interopRequire(__webpack_require__(67)); + // empty the manipulation divs + util.recursiveDOMDelete(this.manipulationDiv); + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.closeDiv); - var Diamond = _interopRequire(__webpack_require__(68)); + // remove the manipulation divs + this.canvas.frame.removeChild(this.manipulationDiv); + this.canvas.frame.removeChild(this.editModeDiv); + this.canvas.frame.removeChild(this.closeDiv); - var Dot = _interopRequire(__webpack_require__(70)); + // set the references to undefined + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; - var Ellipse = _interopRequire(__webpack_require__(71)); + // remove override + this.selectionHandler.forceSelectEdges = false; + }, + writable: true, + configurable: true + }, + _createSeperator: { - var Icon = _interopRequire(__webpack_require__(72)); - var Image = _interopRequire(__webpack_require__(73)); + /** + * create a seperator line. the index is to differentiate in the manipulation dom + * @param index + * @private + */ + value: function _createSeperator() { + var index = arguments[0] === undefined ? 1 : arguments[0]; + this.manipulationDOM["seperatorLineDiv" + index] = document.createElement("div"); + this.manipulationDOM["seperatorLineDiv" + index].className = "network-seperatorLine"; + this.manipulationDiv.appendChild(this.manipulationDOM["seperatorLineDiv" + index]); + }, + writable: true, + configurable: true + }, + _createAddNodeButton: { - var Square = _interopRequire(__webpack_require__(74)); + // ---------------------- DOM functions for buttons --------------------------// - var Star = _interopRequire(__webpack_require__(75)); + value: function _createAddNodeButton(locale) { + var button = this._createButton("addNode", "network-manipulationUI add", locale.addNode); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addNodeMode.bind(this)); + }, + writable: true, + configurable: true + }, + _createAddEdgeButton: { + value: function _createAddEdgeButton(locale) { + var button = this._createButton("addEdge", "network-manipulationUI connect", locale.addEdge); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); + }, + writable: true, + configurable: true + }, + _createEditNodeButton: { + value: function _createEditNodeButton(locale) { + var button = this._createButton("editNode", "network-manipulationUI edit", locale.editNode); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editNode.bind(this)); + }, + writable: true, + configurable: true + }, + _createEditEdgeButton: { + value: function _createEditEdgeButton(locale) { + var button = this._createButton("editEdge", "network-manipulationUI edit", locale.editEdge); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); + }, + writable: true, + configurable: true + }, + _createDeleteButton: { + value: function _createDeleteButton(locale) { + var button = this._createButton("delete", "network-manipulationUI delete", locale.del); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.deleteSelected.bind(this)); + }, + writable: true, + configurable: true + }, + _createBackButton: { + value: function _createBackButton(locale) { + var button = this._createButton("back", "network-manipulationUI back", locale.back); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); + }, + writable: true, + configurable: true + }, + _createButton: { + value: function _createButton(id, className, label) { + var labelClassName = arguments[3] === undefined ? "network-manipulationLabel" : arguments[3]; + this.manipulationDOM[id + "Div"] = document.createElement("div"); + this.manipulationDOM[id + "Div"].className = className; + this.manipulationDOM[id + "Label"] = document.createElement("div"); + this.manipulationDOM[id + "Label"].className = labelClassName; + this.manipulationDOM[id + "Label"].innerHTML = label; + this.manipulationDOM[id + "Div"].appendChild(this.manipulationDOM[id + "Label"]); + return this.manipulationDOM[id + "Div"]; + }, + writable: true, + configurable: true + }, + _createDescription: { + value: function _createDescription(label) { + this.manipulationDiv.appendChild(this._createButton("description", "network-manipulationUI none", label)); + }, + writable: true, + configurable: true + }, + _temporaryBindEvent: { - var Text = _interopRequire(__webpack_require__(76)); + // -------------------------- End of DOM functions for buttons ------------------------------// - var Triangle = _interopRequire(__webpack_require__(77)); + /** + * this binds an event until cleanup by the clean functions. + * @param event + * @param newFunction + * @private + */ + value: function _temporaryBindEvent(event, newFunction) { + this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); + this.body.emitter.on(event, newFunction); + }, + writable: true, + configurable: true + }, + _temporaryBindUI: { - var TriangleDown = _interopRequire(__webpack_require__(78)); + /** + * this overrides an UI function until cleanup by the clean function + * @param UIfunctionName + * @param newFunction + * @private + */ + value: function _temporaryBindUI(UIfunctionName, newFunction) { + if (this.body.eventListeners[UIfunctionName] !== undefined) { + this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; + this.body.eventListeners[UIfunctionName] = newFunction; + } else { + throw new Error("This UI function does not exist. Typo? You tried: \"" + UIfunctionName + "\" possible are: " + JSON.stringify(Object.keys(this.body.eventListeners))); + } + }, + writable: true, + configurable: true + }, + _unbindTemporaryUIs: { - /** - * @class Node - * A node. A node can be connected to other nodes via one or multiple edges. - * @param {object} options An object containing options for the node. All - * options are optional, except for the id. - * {number} id Id of the node. Required - * {string} label Text label for the node - * {number} x Horizontal position of the node - * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square", "icon" - * {string} image An image url - * {string} title An title text, can be HTML - * {anytype} group A group name or number - * @param {Network.Images} imagelist A list with images. Only needed - * when the node has an image - * @param {Network.Groups} grouplist A list with groups. Needed for - * retrieving group options - * @param {Object} constants An object with default values for - * example for the color - * - */ - var Node = (function () { - function Node(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Node); + /** + * Restore the overridden UI functions to their original state. + * + * @private + */ + value: function _unbindTemporaryUIs() { + for (var functionName in this.temporaryUIFunctions) { + if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { + this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; + delete this.temporaryUIFunctions[functionName]; + } + } + this.temporaryUIFunctions = {}; + }, + writable: true, + configurable: true + }, + _unbindTemporaryEvents: { - this.options = util.bridgeObject(globalOptions); - this.body = body; + /** + * Unbind the events created by _temporaryBindEvent + * @private + */ + value: function _unbindTemporaryEvents() { + for (var i = 0; i < this.temporaryEventFunctions.length; i++) { + var eventName = this.temporaryEventFunctions[i].event; + var boundFunction = this.temporaryEventFunctions[i].boundFunction; + this.body.emitter.off(eventName, boundFunction); + } + this.temporaryEventFunctions = []; + }, + writable: true, + configurable: true + }, + _bindHammerToDiv: { - this.edges = []; // all edges connected to this node + /** + * Bind an hammer instance to a DOM element. + * @param domElement + * @param funct + */ + value: function _bindHammerToDiv(domElement, boundFunction) { + var hammer = new Hammer(domElement, {}); + hammerUtil.onTouch(hammer, boundFunction); + this.manipulationHammers.push(hammer); + }, + writable: true, + configurable: true + }, + _cleanupTemporaryNodesAndEdges: { - // set defaults for the options - this.id = undefined; - this.imagelist = imagelist; - this.grouplist = grouplist; - // state options - this.x = undefined; - this.y = undefined; - this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate - this.selected = false; - this.hover = false; + /** + * Neatly clean up temporary edges and nodes + * @private + */ + value: function _cleanupTemporaryNodesAndEdges() { + // _clean temporary edges + for (var i = 0; i < this.temporaryIds.edges.length; i++) { + this.body.edges[this.temporaryIds.edges[i]].disconnect(); + delete this.body.edges[this.temporaryIds.edges[i]]; + var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); + if (indexTempEdge !== -1) { + this.body.edgeIndices.splice(indexTempEdge, 1); + } + } - this.labelModule = new Label(this.body, this.options); - this.setOptions(options); - } + // _clean temporary nodes + for (var i = 0; i < this.temporaryIds.nodes.length; i++) { + delete this.body.nodes[this.temporaryIds.nodes[i]]; + var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); + if (indexTempNode !== -1) { + this.body.nodeIndices.splice(indexTempNode, 1); + } + } - _prototypeProperties(Node, null, { - attachEdge: { + this.temporaryIds = { nodes: [], edges: [] }; + }, + writable: true, + configurable: true + }, + _controlNodeTouch: { + // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// /** - * Attach a edge to the node - * @param {Edge} edge + * the touch is used to get the position of the initial click + * @param event + * @private */ - value: function attachEdge(edge) { - if (this.edges.indexOf(edge) == -1) { - this.edges.push(edge); - } + value: function _controlNodeTouch(event) { + this.lastTouch = this.body.functions.getPointer(event.center); + this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object }, writable: true, configurable: true }, - detachEdge: { + _controlNodeDragStart: { /** - * Detach a edge from the node - * @param {Edge} edge + * the drag start is used to mark one of the control nodes as selected. + * @param event + * @private */ - value: function detachEdge(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); + value: function _controlNodeDragStart(event) { + var pointer = this.lastTouch; + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + var to = this.body.nodes[this.temporaryIds.nodes[1]]; + var edge = this.body.edges[this.edgeBeingEditedId]; + this.selectedControlNode = undefined; + + var fromSelect = from.isOverlappingWith(pointerObj); + var toSelect = to.isOverlappingWith(pointerObj); + + if (fromSelect === true) { + this.selectedControlNode = from; + edge.edgeType.from = from; + } else if (toSelect === true) { + this.selectedControlNode = to; + edge.edgeType.to = to; } + + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - togglePhysics: { + _controlNodeDrag: { /** - * Enable or disable the physics. - * @param status + * dragging the control nodes or the canvas + * @param event + * @private */ - value: function togglePhysics(status) { - this.options.physics = status; + value: function _controlNodeDrag(event) { + this.body.emitter.emit("disablePhysics"); + var pointer = this.body.functions.getPointer(event.center); + var pos = this.canvas.DOMtoCanvas(pointer); + + if (this.selectedControlNode !== undefined) { + this.selectedControlNode.x = pos.x; + this.selectedControlNode.y = pos.y; + } else { + // if the drag was not started properly because the click started outside the network div, start it now. + var diffX = pointer.x - this.lastTouch.x; + var diffY = pointer.y - this.lastTouch.y; + this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + } + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - setOptions: { + _controlNodeDragEnd: { /** - * Set or overwrite options for the node - * @param {Object} options an object with options - * @param {Object} constants and object with default, global options + * connecting or restoring the control nodes. + * @param event + * @private */ - value: function setOptions(options) { - if (!options) { - return; - } - - var fields = ["borderWidth", "borderWidthSelected", "brokenImage", "customScalingFunction", "font", "hidden", "icon", "id", "image", "label", "level", "physics", "shape", "size", "title", "value", "x", "y"]; - util.selectiveDeepExtend(fields, this.options, options); - - // basic options - if (options.id !== undefined) { - this.id = options.id; - } - - if (this.id === undefined) { - throw "Node must have an id"; - } - - if (options.x !== undefined) { - this.x = options.x;this.predefinedPosition = true; - } - if (options.y !== undefined) { - this.y = options.y;this.predefinedPosition = true; - } - if (options.value !== undefined) { - this.value = options.value; - } - - // copy group options - if (typeof options.group === "number" || typeof options.group === "string" && options.group != "") { - var groupObj = this.grouplist.get(options.group); - util.deepExtend(this.options, groupObj); - // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. - this.options.color = util.parseColor(this.options.color); - } - // individual shape options - if (options.color !== undefined) { - this.options.color = util.parseColor(options.color); - } + value: function _controlNodeDragEnd(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var edge = this.body.edges[this.edgeBeingEditedId]; - if (this.options.image !== undefined && this.options.image != "") { - if (this.imagelist) { - this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage); - } else { - throw "No imagelist provided"; + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + if (overlappingNodeIds[i] !== this.selectedControlNode.id) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; } } - if (options.fixed !== undefined) { - if (typeof options.fixed == "boolean") { - this.options.fixed.x = true; - this.options.fixed.y = true; + // perform the connection + if (node !== undefined && this.selectedControlNode !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale].createEdgeError); } else { - if (options.fixed.x !== undefined && typeof options.fixed.x == "boolean") { - this.options.fixed.x = options.fixed.x; - } - if (options.fixed.y !== undefined && typeof options.fixed.y == "boolean") { - this.options.fixed.y = options.fixed.y; + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + if (this.selectedControlNode.id == from.id) { + this._performEditEdge(node.id, edge.to.id); + } else { + this._performEditEdge(edge.from.id, node.id); } } + } else { + edge.updateEdgeType(); + this.body.emitter.emit("restorePhysics"); } - - // choose draw method depending on the shape - switch (this.options.shape) { - - case "box": - this.shape = new Box(this.options, this.body, this.labelModule); - break; - case "circle": - this.shape = new Circle(this.options, this.body, this.labelModule); - break; - case "circularImage": - this.shape = new CircularImage(this.options, this.body, this.labelModule, this.imageObj); - break; - case "database": - this.shape = new Database(this.options, this.body, this.labelModule); - break; - case "diamond": - this.shape = new Diamond(this.options, this.body, this.labelModule); - break; - case "dot": - this.shape = new Dot(this.options, this.body, this.labelModule); - break; - case "ellipse": - this.shape = new Ellipse(this.options, this.body, this.labelModule); - break; - case "icon": - this.shape = new Icon(this.options, this.body, this.labelModule); - break; - case "image": - this.shape = new Image(this.options, this.body, this.labelModule, this.imageObj); - break; - case "square": - this.shape = new Square(this.options, this.body, this.labelModule); - break; - case "star": - this.shape = new Star(this.options, this.body, this.labelModule); - break; - case "text": - this.shape = new Text(this.options, this.body, this.labelModule); - break; - case "triangle": - this.shape = new Triangle(this.options, this.body, this.labelModule); - break; - case "triangleDown": - this.shape = new TriangleDown(this.options, this.body, this.labelModule); - break; - default: - this.shape = new Ellipse(this.options, this.body, this.labelModule); - break; - } - - this.labelModule.setOptions(this.options, options); - - // reset the size of the node, this can be changed - this._reset(); + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - select: { + _handleConnect: { + // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + + + // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// /** - * select this node + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * + * @private */ - value: function select() { - this.selected = true; - this._reset(); + value: function _handleConnect(event) { + var _this = this; + // check to avoid double fireing of this function. + if (new Date().valueOf() - this.touchTime > 100) { + var pointer = this.body.functions.getPointer(event.center); + var node = this.selectionHandler.getNodeAt(pointer); + + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale].createEdgeError); + } else { + (function () { + // create a node the temporary line can look at + var targetNode = _this._getNewTargetNode(node.x, node.y); + var targetNodeId = targetNode.id; + _this.body.nodes[targetNode.id] = targetNode; + _this.body.nodeIndices.push(targetNode.id); + + // create a temporary edge + var connectionEdge = _this.body.functions.createEdge({ + id: "connectionEdge" + util.randomUUID(), + from: node.id, + to: targetNode.id, + physics: false, + smooth: { + enabled: true, + dynamic: false, + type: "continuous", + roundness: 0.5 + } + }); + _this.body.edges[connectionEdge.id] = connectionEdge; + _this.body.edgeIndices.push(connectionEdge.id); + + _this.temporaryIds.nodes.push(targetNode.id); + _this.temporaryIds.edges.push(connectionEdge.id); + + _this.temporaryUIFunctions.onDrag = _this.body.eventListeners.onDrag; + _this.body.eventListeners.onDrag = function (event) { + var pointer = _this.body.functions.getPointer(event.center); + var targetNode = _this.body.nodes[targetNodeId]; + targetNode.x = _this.canvas._XconvertDOMtoCanvas(pointer.x); + targetNode.y = _this.canvas._YconvertDOMtoCanvas(pointer.y); + _this.body.emitter.emit("_redraw"); + }; + })(); + } + } + this.touchTime = new Date().valueOf(); + + // do the original touch events + this.temporaryUIFunctions.onTouch(event); + } }, writable: true, configurable: true }, - unselect: { - + _finishConnect: { /** - * unselect this node + * Connect the new edge to the target if one exists, otherwise remove temp line + * @param event + * @private */ - value: function unselect() { - this.selected = false; - this._reset(); + value: function _finishConnect(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + + // remember the edge id + var connectFromId = undefined; + if (this.temporaryIds.edges[0] !== undefined) { + connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; + } + + //restore the drag function + if (this.temporaryUIFunctions.onDrag !== undefined) { + this.body.eventListeners.onDrag = this.temporaryUIFunctions.onDrag; + delete this.temporaryUIFunctions.onDrag; + } + + // get the overlapping node but NOT the temporary node; + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) !== -1) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; + } + } + + // clean temporary nodes and edges. + this._cleanupTemporaryNodesAndEdges(); + + // perform the connection + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale].createEdgeError); + } else { + if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { + this._performCreateEdge(connectFromId, node.id); + } + } + } + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - _reset: { + _performAddNode: { + // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// + // ------------------------------ Performing all the actual data manipulation ------------------------// + /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private + * Adds a node on the specified location */ - value: function _reset() { - this.shape.width = undefined; - this.shape.height = undefined; + value: function _performAddNode(clickData) { + var _this = this; + var defaultData = { + id: util.randomUUID(), + x: clickData.pointer.canvas.x, + y: clickData.pointer.canvas.y, + label: "new" + }; + + if (typeof this.options.handlerFunctions.addNode === "function") { + if (this.options.handlerFunctions.addNode.length == 2) { + this.options.handlerFunctions.addNode(defaultData, function (finalizedData) { + _this.body.data.nodes.add(finalizedData); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for add does not support two arguments (data,callback)"); + this.showManipulatorToolbar(); + } + } else { + this.body.data.nodes.add(defaultData); + this.showManipulatorToolbar(); + } }, writable: true, configurable: true }, - getTitle: { + _performCreateEdge: { /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. + * connect two nodes with a new edge. + * + * @private */ - value: function getTitle() { - return typeof this.options.title === "function" ? this.options.title() : this.options.title; + value: function _performCreateEdge(sourceNodeId, targetNodeId) { + var _this = this; + var defaultData = { from: sourceNodeId, to: targetNodeId }; + if (this.options.handlerFunctions.addEdge) { + if (this.options.handlerFunctions.addEdge.length == 2) { + this.options.handlerFunctions.addEdge(defaultData, function (finalizedData) { + _this.body.data.edges.add(finalizedData); + _this.selectionHandler.unselectAll(); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for connect does not support two arguments (data,callback)"); + } + } else { + this.body.data.edges.add(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); + } }, writable: true, configurable: true }, - distanceToBorder: { - + _performEditEdge: { /** - * 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 + * connect two nodes with a new edge. + * + * @private */ - value: function distanceToBorder(ctx, angle) { - return this.shape.distanceToBorder(ctx, angle); + value: function _performEditEdge(sourceNodeId, targetNodeId) { + var _this = this; + var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; + if (this.options.handlerFunctions.editEdge) { + if (this.options.handlerFunctions.editEdge.length == 2) { + this.options.handlerFunctions.editEdge(defaultData, function (finalizedData) { + _this.body.data.edges.update(finalizedData); + _this.selectionHandler.unselectAll(); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for edit does not support two arguments (data, callback)"); + } + } else { + this.body.data.edges.update(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); + } }, writable: true, configurable: true - }, - isFixed: { + } + }); + + return ManipulationSystem; + })(); + + module.exports = ManipulationSystem; + +/***/ }, +/* 62 */ +/***/ function(module, exports, __webpack_require__) { + + + /** + * Expose `Emitter`. + */ + + module.exports = Emitter; + + /** + * Initialize a new `Emitter`. + * + * @api public + */ + + function Emitter(obj) { + if (obj) return mixin(obj); + }; + + /** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + + function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; + } + + /** + * 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; + }; + + /** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + + Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; + + function on() { + self.off(event, on); + fn.apply(this, arguments); + } + + on.fn = fn; + this.on(event, on); + return this; + }; + + /** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + + Emitter.prototype.off = + Emitter.prototype.removeListener = + Emitter.prototype.removeAllListeners = + Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } + + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; + + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } + // 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; + }; - /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not - */ - value: function isFixed() { - return this.options.fixed.x && this.options.fixed.y; - }, - writable: true, - configurable: true - }, - isSelected: { + /** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ + Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - }, - writable: true, - configurable: true - }, - getValue: { + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } + return this; + }; - /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.value; - }, - writable: true, - configurable: true - }, - setValueRange: { + /** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ + Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; + }; - /** - * Adjust the value range of the node. The node will adjust it's size - * based on its value. - * @param {Number} min - * @param {Number} max - */ - value: function setValueRange(min, max, total) { - if (this.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); - var sizeDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled == true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.size = this.options.scaling.min + scale * sizeDiff; - } - }, - writable: true, - configurable: true - }, - draw: { + /** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; + }; - /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); - }, - writable: true, - configurable: true - }, - resize: { +/***/ }, +/* 63 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Recalculate the size of this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function resize(ctx) { - this.shape.resize(ctx); - }, - writable: true, - configurable: true - }, - isOverlappingWith: { + var __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(global, module) {//! moment.js + //! version : 2.9.0 + //! authors : Tim Wood, Iskren Chernev, Moment.js contributors + //! license : MIT + //! momentjs.com + (function (undefined) { + /************************************ + Constants + ************************************/ - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isOverlappingWith(obj) { - return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; - }, - writable: true, - configurable: true - } - }); + var moment, + VERSION = '2.9.0', + // the global-scope this is NOT the global object in Node.js + globalScope = (typeof global !== 'undefined' && (typeof window === 'undefined' || window === global.window)) ? global : this, + oldGlobalMoment, + round = Math.round, + hasOwnProperty = Object.prototype.hasOwnProperty, + i, - return Node; - })(); + YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, - module.exports = Node; + // internal storage for locale config files + locales = {}, -/***/ }, -/* 61 */ -/***/ function(module, exports, __webpack_require__) { + // extra moment internal properties (plugins register props here) + momentProperties = [], - "use strict"; + // check for nodeJS + hasModule = (typeof module !== 'undefined' && module && module.exports), - var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; + // ASP.NET json date format regex + aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, + aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + // 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)$/, - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // format tokens + formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, - var util = __webpack_require__(1); + // parsing token regexes + parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 + parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 + parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999 + parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 + parseTokenDigits = /\d+/, // nonzero number of digits + parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic. + parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z + parseTokenT = /T/i, // T (ISO separator) + parseTokenOffsetMs = /[\+\-]?\d+/, // 1234567890123 + parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 - /** - * Created by Alex on 3/17/2015. - */ + //strict parsing regexes + parseTokenOneDigit = /\d/, // 0 - 9 + parseTokenTwoDigits = /\d\d/, // 00 - 99 + parseTokenThreeDigits = /\d{3}/, // 000 - 999 + parseTokenFourDigits = /\d{4}/, // 0000 - 9999 + parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 + parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf - var Label = (function () { - function Label(body, options) { - _classCallCheck(this, Label); + // iso 8601 regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) + isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/, - this.body = body; + isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', - this.fontOptions = {}; - this.defaultOptions = { - color: "#343434", - size: 14, // px - face: "arial", - background: "none", - stroke: 0, // px - strokeColor: "white", - align: "horizontal" - }; - util.extend(this.fontOptions, this.defaultOptions); + isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], + ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], + ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], + ['GGGG-[W]WW', /\d{4}-W\d{2}/], + ['YYYY-DDD', /\d{4}-\d{3}/] + ], - this.setOptions(options); - this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached - } + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], + ['HH:mm', /(T| )\d\d:\d\d/], + ['HH', /(T| )\d\d/] + ], - _prototypeProperties(Label, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - if (options.label !== undefined) { - this.labelDirty = true; - } - if (options.font) { - if (typeof options.font === "string") { - var optionsArray = options.font.split(" "); - this.fontOptions.size = optionsArray[0].replace("px", ""); - this.fontOptions.face = optionsArray[1]; - this.fontOptions.color = optionsArray[2]; - } else if (typeof options.font == "object") { - util.protoExtend(this.fontOptions, options.font); - } - this.fontOptions.size = Number(this.fontOptions.size); - } - }, - writable: true, - configurable: true - }, - draw: { + // timezone chunker '+10:00' > ['10', '00'] or '-1530' > ['-', '15', '30'] + parseTimezoneChunker = /([\+\-]|\d\d)/gi, + // getter and setter names + proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), + unitMillisecondFactors = { + 'Milliseconds' : 1, + 'Seconds' : 1e3, + 'Minutes' : 6e4, + 'Hours' : 36e5, + 'Days' : 864e5, + 'Months' : 2592e6, + 'Years' : 31536e6 + }, - /** - * Main function. This is called from anything that wants to draw a label. - * @param ctx - * @param x - * @param y - * @param selected - * @param baseline - */ - value: function draw(ctx, x, y, selected) { - var baseline = arguments[4] === undefined ? "middle" : arguments[4]; - // if no label, return - if (this.options.label === undefined) { - return; - } // check if we have to render the label - var viewFontSize = this.fontOptions.size * this.body.view.scale; - if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) { - return; - } // update the size cache if required - this.calculateLabelSize(ctx, selected, x, y, baseline); + 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' + }, - // create the fontfill background - this._drawBackground(ctx); - // draw text - this._drawText(ctx, selected, x, y, baseline); - }, - writable: true, - configurable: true - }, - _drawBackground: { + camelFunctions = { + dayofyear : 'dayOfYear', + isoweekday : 'isoWeekday', + isoweek : 'isoWeek', + weekyear : 'weekYear', + isoweekyear : 'isoWeekYear' + }, - /** - * Draws the label background - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _drawBackground(ctx) { - if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") { - ctx.fillStyle = this.fontOptions.background; + // format function strings + formatFunctions = {}, - var lineMargin = 2; + // default relative time thresholds + relativeTimeThresholds = { + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year + }, - switch (this.fontOptions.align) { - case "middle": - ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); - break; - case "top": - ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); - break; - case "bottom": - ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); - break; - default: - ctx.fillRect(this.size.left, this.size.top, this.size.width, this.size.height); - break; - } - } - }, - writable: true, - configurable: true - }, - _drawText: { + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w W M D d'.split(' '), + paddedTokens = 'M D H h m s w W'.split(' '), + formatTokenFunctions = { + M : function () { + return this.month() + 1; + }, + MMM : function (format) { + return this.localeData().monthsShort(this, format); + }, + MMMM : function (format) { + return this.localeData().months(this, format); + }, + D : function () { + return this.date(); + }, + DDD : function () { + return this.dayOfYear(); + }, + d : function () { + return this.day(); + }, + dd : function (format) { + return this.localeData().weekdaysMin(this, format); + }, + ddd : function (format) { + return this.localeData().weekdaysShort(this, format); + }, + dddd : function (format) { + return this.localeData().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.localeData().meridiem(this.hours(), this.minutes(), true); + }, + A : function () { + return this.localeData().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.utcOffset(), + b = '+'; + if (a < 0) { + a = -a; + b = '-'; + } + return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2); + }, + ZZ : function () { + var a = this.utcOffset(), + 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.valueOf(); + }, + X : function () { + return this.unix(); + }, + Q : function () { + return this.quarter(); + } + }, - /** - * - * @param ctx - * @param x - * @param baseline - * @private - */ - value: function _drawText(ctx, selected, x, y) { - var baseline = arguments[4] === undefined ? "middle" : arguments[4]; - var fontSize = this.fontOptions.size; - var viewFontSize = fontSize * this.body.view.scale; - // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) - if (viewFontSize >= this.options.scaling.label.maxVisible) { - fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; - } + deprecations = {}, - var yLine = this.size.yLine; - var _getColor = this._getColor(viewFontSize); + lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'], - var _getColor2 = _slicedToArray(_getColor, 2); + updateInProgress = false; - var fontColor = _getColor2[0]; - var strokeColor = _getColor2[1]; - var _ref = this._setAlignment(ctx, x, yLine, baseline); + // 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'); + } + } - var _ref2 = _slicedToArray(_ref, 2); + function hasOwnProp(a, b) { + return hasOwnProperty.call(a, b); + } - x = _ref2[0]; - yLine = _ref2[1]; + 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 + }; + } + function printMsg(msg) { + if (moment.suppressDeprecationWarnings === false && + typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); + } + } - // configure context for drawing the text - ctx.font = (selected ? "bold " : "") + fontSize + "px " + this.fontOptions.face; - ctx.fillStyle = fontColor; - ctx.textAlign = "center"; + function deprecate(msg, fn) { + var firstTime = true; + return extend(function () { + if (firstTime) { + printMsg(msg); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } - // set the strokeWidth - if (this.fontOptions.stroke > 0) { - ctx.lineWidth = this.fontOptions.stroke; - ctx.strokeStyle = strokeColor; - ctx.lineJoin = "round"; + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + printMsg(msg); + deprecations[name] = true; } + } - // draw the text - for (var i = 0; i < this.lineCount; i++) { - if (this.fontOptions.stroke > 0) { - ctx.strokeText(this.lines[i], x, yLine); - } - ctx.fillText(this.lines[i], x, yLine); - yLine += fontSize; - } - }, - writable: true, - configurable: true - }, - _setAlignment: { - value: function _setAlignment(ctx, x, yLine, baseline) { - // check for label alignment (for edges) - // TODO: make alignment for nodes - if (this.fontOptions.align !== "horizontal") { - x = 0; - yLine = 0; + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; + } + function ordinalizeToken(func, period) { + return function (a) { + return this.localeData().ordinal(func.call(this, a), period); + }; + } - var lineMargin = 2; - if (this.fontOptions.align === "top") { - ctx.textBaseline = "alphabetic"; - yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } else if (this.fontOptions.align === "bottom") { - ctx.textBaseline = "hanging"; - yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers - } else { - ctx.textBaseline = "middle"; - } + function monthDiff(a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); } else { - ctx.textBaseline = baseline; + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); } - return [x, yLine]; - }, - writable: true, - configurable: true - }, - _getColor: { - - /** - * fade in when relative scale is between threshold and threshold - 1. - * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. - * - * @param viewFontSize - * @returns {*[]} - * @private - */ - value: function _getColor(viewFontSize) { - var fontColor = this.fontOptions.color || "#000000"; - var strokeColor = this.fontOptions.strokeColor || "#ffffff"; - if (viewFontSize <= this.options.scaling.label.drawThreshold) { - var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); - fontColor = util.overrideOpacity(fontColor, opacity); - strokeColor = util.overrideOpacity(strokeColor, opacity); - } - return [fontColor, strokeColor]; - }, - writable: true, - configurable: true - }, - getTextSize: { + return -(wholeMonthDiff + adjust); + } + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); + } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); - /** - * - * @param ctx - * @param selected - * @returns {{width: number, height: number}} - */ - value: function getTextSize(ctx) { - var selected = arguments[1] === undefined ? false : arguments[1]; - var size = { - width: this._processLabel(ctx, selected), - height: this.fontOptions.size * this.lineCount - }; - return size; - }, - writable: true, - configurable: true - }, - calculateLabelSize: { + function meridiemFixWrap(locale, hour, meridiem) { + var isPm; - /** - * - * @param ctx - * @param selected - * @param x - * @param y - * @param baseline - */ - value: function calculateLabelSize(ctx, selected) { - var x = arguments[2] === undefined ? 0 : arguments[2]; - var y = arguments[3] === undefined ? 0 : arguments[3]; - var baseline = arguments[4] === undefined ? "middle" : arguments[4]; - if (this.labelDirty === true) { - this.size.width = this._processLabel(ctx, selected); + if (meridiem == null) { + // nothing to do + return hour; } - this.size.height = this.fontOptions.size * this.lineCount; - this.size.left = x - this.size.width * 0.5; - this.size.top = y - this.size.height * 0.5; - this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; - if (baseline == "hanging") { - this.size.top += 0.5 * this.fontOptions.size; - this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers - this.size.yLine += 4; // distance from node + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // thie is not supposed to happen + return hour; } + } - this.labelDirty = false; - }, - writable: true, - configurable: true - }, - _processLabel: { + /************************************ + Constructors + ************************************/ + function Locale() { + } - /** - * This calculates the width as well as explodes the label string and calculates the amount of lines. - * @param ctx - * @param selected - * @returns {number} - * @private - */ - value: function _processLabel(ctx, selected) { - var width = 0; - var lines = [""]; - var lineCount = 0; - if (this.options.label !== undefined) { - lines = String(this.options.label).split("\n"); - lineCount = lines.length; - ctx.font = (selected ? "bold " : "") + this.fontOptions.size + "px " + this.fontOptions.face; - width = ctx.measureText(lines[0]).width; - for (var i = 1; i < lineCount; i++) { - var lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; - } + // Moment prototype object + function Moment(config, skipOverflow) { + if (skipOverflow !== false) { + checkOverflow(config); + } + copyConfig(this, config); + this._d = new Date(+config._d); + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + moment.updateOffset(this); + updateInProgress = false; } - this.lines = lines; - this.lineCount = lineCount; - - return width; - }, - writable: true, - configurable: true } - }); - - return Label; - })(); - - module.exports = Label; - -/***/ }, -/* 62 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + // 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; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // 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; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + this._data = {}; - var NodeBase = _interopRequire(__webpack_require__(63)); + this._locale = moment.localeData(); - var Box = (function (NodeBase) { - function Box(options, body, labelModule) { - _classCallCheck(this, Box); + this._bubble(); + } - _get(Object.getPrototypeOf(Box.prototype), "constructor", this).call(this, options, body, labelModule); - } + /************************************ + Helpers + ************************************/ - _inherits(Box, NodeBase); - _prototypeProperties(Box, null, { - resize: { - value: function resize(ctx) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, this.selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - - ctx.roundRect(this.left, this.top, this.width, this.height, this.options.size); - ctx.fill(); - ctx.stroke(); + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } - this.labelModule.draw(ctx, x, y, selected); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); - }, - writable: true, - configurable: true + return a; } - }); - - return Box; - })(NodeBase); - - module.exports = Box; - -/***/ }, -/* 63 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + function copyConfig(to, from) { + var i, prop, val; - /** - * Created by Alex on 3/19/2015. - */ + if (typeof from._isAMomentObject !== 'undefined') { + to._isAMomentObject = from._isAMomentObject; + } + if (typeof from._i !== 'undefined') { + to._i = from._i; + } + if (typeof from._f !== 'undefined') { + to._f = from._f; + } + if (typeof from._l !== 'undefined') { + to._l = from._l; + } + if (typeof from._strict !== 'undefined') { + to._strict = from._strict; + } + if (typeof from._tzm !== 'undefined') { + to._tzm = from._tzm; + } + if (typeof from._isUTC !== 'undefined') { + to._isUTC = from._isUTC; + } + if (typeof from._offset !== 'undefined') { + to._offset = from._offset; + } + if (typeof from._pf !== 'undefined') { + to._pf = from._pf; + } + if (typeof from._locale !== 'undefined') { + to._locale = from._locale; + } - var NodeBase = (function () { - function NodeBase(options, body, labelModule) { - _classCallCheck(this, NodeBase); + if (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.top = undefined; - this.left = undefined; - this.height = undefined; - this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; - } + return to; + } - _prototypeProperties(NodeBase, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true - }, - _distanceToBorder: { - value: function _distanceToBorder(angle) { - var borderWidth = 1; - return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - }, - writable: true, - configurable: true + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } } - }); - return NodeBase; - })(); + // 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; + } - module.exports = NodeBase; + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; -/***/ }, -/* 64 */ -/***/ function(module, exports, __webpack_require__) { + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + return res; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + function momentsDifference(base, other) { + var res; + other = makeAs(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; + } - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + return res; + } - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // TODO: remove 'name' arg after deprecation is removed + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); + tmp = val; val = period; period = tmp; + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + val = typeof val === 'string' ? +val : val; + dur = moment.duration(val, period); + addOrSubtractDurationFromMoment(this, dur, direction); + return this; + }; + } - var CircleImageBase = _interopRequire(__webpack_require__(65)); + function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; - var Circle = (function (CircleImageBase) { - function Circle(options, body, labelModule) { - _classCallCheck(this, Circle); + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + if (days) { + rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding); + } + if (months) { + rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding); + } + if (updateOffset) { + moment.updateOffset(mom, days || months); + } + } - _get(Object.getPrototypeOf(Circle.prototype), "constructor", this).call(this, options, body, labelModule); - } + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } - _inherits(Circle, CircleImageBase); + function isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } - _prototypeProperties(Circle, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.options.size = diameter / 2; + // 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; + } - this.width = diameter; - this.height = diameter; + function normalizeUnits(units) { + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + return units; + } - this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } - this.labelModule.draw(ctx, x, y, selected); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); - }, - writable: true, - configurable: true + return normalizedInput; } - }); - return Circle; - })(CircleImageBase); + function makeList(field) { + var count, setter; - module.exports = Circle; + if (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; + } + else if (field.indexOf('month') === 0) { + count = 12; + setter = 'month'; + } + else { + return; + } -/***/ }, -/* 65 */ -/***/ function(module, exports, __webpack_require__) { + moment[field] = function (format, index) { + var i, getter, + method = moment._locale[field], + results = []; - "use strict"; + if (typeof format === 'number') { + index = format; + format = undefined; + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment._locale, m, format || ''); + }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; + } - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + return value; + } - /** - * Created by Alex on 3/19/2015. - */ - var NodeBase = _interopRequire(__webpack_require__(63)); + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } - var CircleImageBase = (function (NodeBase) { - function CircleImageBase(options, body, labelModule) { - _classCallCheck(this, CircleImageBase); + function weeksInYear(year, dow, doy) { + return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; + } - _get(Object.getPrototypeOf(CircleImageBase.prototype), "constructor", this).call(this, options, body, labelModule); - } + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } - _inherits(CircleImageBase, NodeBase); + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } - _prototypeProperties(CircleImageBase, null, { - _drawRawCircle: { - value: function _drawRawCircle(ctx, x, y, selected, hover, size) { - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + 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] > 24 || + (m._a[HOUR] === 24 && (m._a[MINUTE] !== 0 || + m._a[SECOND] !== 0 || + m._a[MILLISECOND] !== 0)) ? 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; - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + m._pf.overflow = overflow; + } + } - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(x, y, size); - ctx.fill(); - ctx.stroke(); - }, - writable: true, - configurable: true - }, - _drawImageAtPosition: { - value: function _drawImageAtPosition(ctx) { - if (this.imageObj.width != 0) { - // draw the image - ctx.globalAlpha = 1; - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + 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 && + m._pf.bigHour === undefined; + } } - }, - writable: true, - configurable: true - }, - _drawImageLabel: { - value: function _drawImageLabel(ctx, x, y, selected) { - var yLabel; - var offset = 0; + return m._isValid; + } - if (this.height !== undefined) { - offset = this.height * 0.5; - var labelDimensions = this.labelModule.getTextSize(ctx); + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } - if (labelDimensions.lineCount >= 1) { - offset += labelDimensions.height / 2; - offset += 3; - } + // pick the locale from the array + // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + function chooseLocale(names) { + var i = 0, j, next, locale, split; + + while (i < names.length) { + split = normalizeLocale(names[i]).split('-'); + j = split.length; + next = normalizeLocale(names[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + locale = loadLocale(split.slice(0, j).join('-')); + if (locale) { + return locale; + } + if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; } + return null; + } - yLabel = y + offset; - this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); - }, - writable: true, - configurable: true + function loadLocale(name) { + var oldLocale = null; + if (!locales[name] && hasModule) { + try { + oldLocale = moment.locale(); + !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()); + // because defineLocale currently also sets the global locale, we want to undo that for lazy loaded locales + moment.locale(oldLocale); + } catch (e) { } + } + return locales[name]; } - }); - return CircleImageBase; - })(NodeBase); + // Return a moment from input, that is local/utc/utcOffset equivalent to + // model. + function makeAs(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = (moment.isMoment(input) || isDate(input) ? + +input : +moment(input)) - (+res); + // Use low-level api, because this fn is low-level api. + res._d.setTime(+res._d + diff); + moment.updateOffset(res, false); + return res; + } else { + return moment(input).local(); + } + } - module.exports = CircleImageBase; + /************************************ + Locale + ************************************/ -/***/ }, -/* 66 */ -/***/ function(module, exports, __webpack_require__) { - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + extend(Locale.prototype, { + set : function (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + /\d{1,2}/.source); + }, - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + months : function (m) { + return this._months[m.month()]; + }, - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + monthsParse : function (monthName, format, strict) { + var i, mom, regex; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = moment.utc([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); + this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); + } + if (!strict && !this._monthsParse[i]) { + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { + return i; + } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; + } + } + }, - var CircleImageBase = _interopRequire(__webpack_require__(65)); + _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, - var CircularImage = (function (CircleImageBase) { - function CircularImage(options, body, labelModule, imageObj) { - _classCallCheck(this, CircularImage); + _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, - _get(Object.getPrototypeOf(CircularImage.prototype), "constructor", this).call(this, options, body, labelModule); - this.imageObj = imageObj; - } + _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, - _inherits(CircularImage, CircleImageBase); + weekdaysParse : function (weekdayName) { + var i, mom, regex; - _prototypeProperties(CircularImage, null, { - resize: { - value: function resize(ctx) { - if (this.imageObj.src !== undefined || this.imageObj.width !== undefined || this.imageObj.height !== undefined) { - if (!this.width) { - var diameter = this.options.size * 2; - this.width = diameter; - this.height = diameter; - this._swapToImageResizeWhenImageLoaded = true; - } - } else { - if (this._swapToImageResizeWhenImageLoaded) { - this.width = 0; - this.height = 0; - delete this._swapToImageResizeWhenImageLoaded; - } - this._resizeImage(ctx); - } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } - this.left = x - this.width / 2; - this.top = y - this.height / 2; + 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; + } + } + }, - var size = Math.abs(this.height / 2); - this._drawRawCircle(ctx, x, y, selected, hover, size); + _longDateFormat : { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY LT', + LLLL : 'dddd, MMMM D, YYYY LT' + }, + 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; + }, - ctx.save(); - ctx.circle(x, y, size); - ctx.stroke(); - ctx.clip(); + 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'); + }, - this._drawImageAtPosition(ctx); + _meridiemParse : /[ap]\.?m?\.?/i, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, - ctx.restore(); - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + _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, now) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.apply(mom, [now]) : output; + }, - this._drawImageLabel(ctx, x, y, selected); + _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' + }, - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true - } - }); + 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); + }, - return CircularImage; - })(CircleImageBase); + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, - module.exports = CircularImage; + ordinal : function (number) { + return this._ordinal.replace('%d', number); + }, + _ordinal : '%d', + _ordinalParse : /\d{1,2}/, -/***/ }, -/* 67 */ -/***/ function(module, exports, __webpack_require__) { + preparse : function (string) { + return string; + }, - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + postformat : function (string) { + return string; + }, - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + }, - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + _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. + }, - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + firstDayOfWeek : function () { + return this._week.dow; + }, - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + firstDayOfYear : function () { + return this._week.doy; + }, - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; + } + }); - var NodeBase = _interopRequire(__webpack_require__(63)); + /************************************ + Formatting + ************************************/ - var Database = (function (NodeBase) { - function Database(options, body, labelModule) { - _classCallCheck(this, Database); - _get(Object.getPrototypeOf(Database.prototype), "constructor", this).call(this, options, body, labelModule); - } + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); + } - _inherits(Database, NodeBase); + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; - _prototypeProperties(Database, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + 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; + }; + } - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); - ctx.fill(); - ctx.stroke(); + format = expandFormat(format, m.localeData()); - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } - this.labelModule.draw(ctx, x, y, selected); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); - }, - writable: true, - configurable: true + return formatFunctions[format](m); } - }); - - return Database; - })(NodeBase); - module.exports = Database; + function expandFormat(format, locale) { + var i = 5; -/***/ }, -/* 68 */ -/***/ function(module, exports, __webpack_require__) { + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + return format; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + /************************************ + Parsing + ************************************/ - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // 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 config._locale._meridiemParse; + case 'x': + return parseTokenOffsetMs; + 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 strict ? config._locale._ordinalParse : config._locale._ordinalParseLenient; + default : + a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), 'i')); + return a; + } + } - var ShapeBase = _interopRequire(__webpack_require__(69)); + function utcOffsetFromString(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]); - var Diamond = (function (ShapeBase) { - function Diamond(options, body, labelModule) { - _classCallCheck(this, Diamond); + return parts[0] === '+' ? minutes : -minutes; + } - _get(Object.getPrototypeOf(Diamond.prototype), "constructor", this).call(this, options, body, labelModule); - } + // function to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, datePartArray = config._a; - _inherits(Diamond, ShapeBase); + 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 = config._locale.monthsParse(input, token, config._strict); + // 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.match(/\d{1,2}/)[0], 10)); + } + break; + // DAY OF YEAR + case 'DDD' : // fall through to DDDD + case 'DDDD' : + if (input != null) { + config._dayOfYear = toInt(input); + } - _prototypeProperties(Diamond, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "diamond", 4, x, y, selected, hover); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true + 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._meridiem = input; + // config._isPm = config._locale.isPM(input); + break; + // HOUR + case 'h' : // fall through to hh + case 'hh' : + config._pf.bigHour = true; + /* falls through */ + 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 OFFSET (MILLISECONDS) + case 'x': + config._d = new Date(toInt(input)); + 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 = utcOffsetFromString(input); + break; + // WEEKDAY - human + case 'dd': + case 'ddd': + case 'dddd': + a = config._locale.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); + } } - }); - - return Diamond; - })(ShapeBase); - module.exports = Diamond; + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; -/***/ }, -/* 69 */ -/***/ function(module, exports, __webpack_require__) { + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - "use strict"; + // 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 { + dow = config._locale._week.dow; + doy = config._locale._week.doy; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); + week = dfl(w.w, 1); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + 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); - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // 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; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + if (config._d) { + return; + } - /** - * Created by Alex on 3/19/2015. - */ - var NodeBase = _interopRequire(__webpack_require__(63)); + currentDate = currentDateArray(config); - var ShapeBase = (function (NodeBase) { - function ShapeBase(options, body, labelModule) { - _classCallCheck(this, ShapeBase); + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } - _get(Object.getPrototypeOf(ShapeBase.prototype), "constructor", this).call(this, options, body, labelModule); - } + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); - _inherits(ShapeBase, NodeBase); + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } - _prototypeProperties(ShapeBase, null, { - _resizeShape: { - value: function _resizeShape() { - if (this.width === undefined) { - var size = 2 * this.options.size; - this.width = size; - this.height = size; + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); } - }, - writable: true, - configurable: true - }, - _drawShape: { - value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { - this._resizeShape(); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + // 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]; + } - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + // 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]; + } - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](x, y, this.options.size); - ctx.fill(); - ctx.stroke(); + // Check for 24:00:00.000 + if (config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0) { + config._nextDay = true; + config._a[HOUR] = 0; + } - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } - if (this.options.label !== undefined) { - var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. - this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + if (config._nextDay) { + config._a[HOUR] = 24; } - }, - writable: true, - configurable: true } - }); - - return ShapeBase; - })(NodeBase); - - module.exports = ShapeBase; - -/***/ }, -/* 70 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var ShapeBase = _interopRequire(__webpack_require__(69)); - - var Dot = (function (ShapeBase) { - function Dot(options, body, labelModule) { - _classCallCheck(this, Dot); + function dateFromObject(config) { + var normalizedInput; - _get(Object.getPrototypeOf(Dot.prototype), "constructor", this).call(this, options, body, labelModule); - } + if (config._d) { + return; + } - _inherits(Dot, ShapeBase); + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day || normalizedInput.date, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; - _prototypeProperties(Dot, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "circle", 2, x, y, selected, hover); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this.options.size + this.options.borderWidth; - }, - writable: true, - configurable: true + dateFromConfig(config); } - }); - - return Dot; - })(ShapeBase); - - module.exports = Dot; - -/***/ }, -/* 71 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var NodeBase = _interopRequire(__webpack_require__(63)); - - var Ellipse = (function (NodeBase) { - function Ellipse(options, body, labelModule) { - _classCallCheck(this, Ellipse); - - _get(Object.getPrototypeOf(Ellipse.prototype), "constructor", this).call(this, options, body, labelModule); - } - - _inherits(Ellipse, NodeBase); - - _prototypeProperties(Ellipse, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } + 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()]; } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + } - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + // date from string and format string + function makeDateFromStringAndFormat(config) { + if (config._f === moment.ISO_8601) { + parseISO(config); + return; + } - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + config._a = []; + config._pf.empty = true; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.ellipse(this.left, this.top, this.width, this.height); - ctx.fill(); - ctx.stroke(); + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - this.boundingBox.left = this.left; - this.boundingBox.top = this.top; - this.boundingBox.bottom = this.top + this.height; - this.boundingBox.right = this.left + this.width; + 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); + } - this.labelModule.draw(ctx, x, y, selected); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); - }, - writable: true, - configurable: true + // clear _12h flag if hour is <= 12 + if (config._pf.bigHour === true && config._a[HOUR] <= 12) { + config._pf.bigHour = undefined; + } + // handle meridiem + config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], + config._meridiem); + dateFromConfig(config); + checkOverflow(config); } - }); - return Ellipse; - })(NodeBase); + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } - module.exports = Ellipse; + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } -/***/ }, -/* 72 */ -/***/ function(module, exports, __webpack_require__) { + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + bestMoment, - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + scoreToBeat, + i, + currentScore; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + if (config._f.length === 0) { + config._pf.invalidFormat = true; + config._d = new Date(NaN); + return; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = copyConfig({}, config); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } + tempConfig._pf = defaultParsingFlags(); + tempConfig._f = config._f[i]; + makeDateFromStringAndFormat(tempConfig); - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + if (!isValid(tempConfig)) { + continue; + } - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; - var NodeBase = _interopRequire(__webpack_require__(63)); + tempConfig._pf.score = currentScore; - var Icon = (function (NodeBase) { - function Icon(options, body, labelModule) { - _classCallCheck(this, Icon); + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } - _get(Object.getPrototypeOf(Icon.prototype), "constructor", this).call(this, options, body, labelModule); - } + extend(config, bestMoment || tempConfig); + } - _inherits(Icon, NodeBase); + // date from iso format + function parseISO(config) { + var i, l, + string = config._i, + match = isoRegex.exec(string); - _prototypeProperties(Icon, null, { - resize: { - value: function resize(ctx) { - if (this.width === undefined) { - var margin = 5; - var iconSize = { - width: Number(this.options.icon.size), - height: Number(this.options.icon.size) - }; - this.width = iconSize.width + 2 * margin; - this.height = iconSize.height + 2 * margin; + 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; } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.options.icon.size = this.options.icon.size || 50; - - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; - this._icon(ctx, x, y, selected); + } + // date from iso format or fallback + function makeDateFromString(config) { + parseISO(config); + if (config._isValid === false) { + delete config._isValid; + moment.createFromInputFallback(config); + } + } - this.boundingBox.top = y - this.options.icon.size * 0.5; - this.boundingBox.left = x - this.options.icon.size * 0.5; - this.boundingBox.right = x + this.options.icon.size * 0.5; - this.boundingBox.bottom = y + this.options.icon.size * 0.5; + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; + } - if (this.options.label !== undefined) { - var iconTextSpacing = 5; - this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + function makeDateFromInput(config) { + var input = config._i, matched; + if (input === undefined) { + config._d = new Date(); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if ((matched = aspNetJsonRegex.exec(input)) !== null) { + config._d = new Date(+matched[1]); + } else if (typeof input === 'string') { + makeDateFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + dateFromConfig(config); + } else if (typeof(input) === 'object') { + dateFromObject(config); + } else if (typeof(input) === 'number') { + // from milliseconds + config._d = new Date(input); + } else { + moment.createFromInputFallback(config); } - }, - writable: true, - configurable: true - }, - _icon: { - value: function _icon(ctx, x, y, selected) { - var iconSize = Number(this.options.icon.size); - var relativeIconSize = iconSize * this.body.view.scale; + } - if (this.options.icon.code && relativeIconSize > this.options.scaling.label.drawThreshold - 1) { - ctx.font = (selected ? "bold " : "") + iconSize + "px " + this.options.icon.face; + 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); - // draw icon - ctx.fillStyle = this.options.icon.color || "black"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.options.icon.code, x, y); + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); } - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - this._distanceToBorder(angle); - }, - writable: true, - configurable: true + return date; } - }); - return Icon; - })(NodeBase); + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; + } - module.exports = Icon; + function parseWeekday(input, locale) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = locale.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } + } + return input; + } -/***/ }, -/* 73 */ -/***/ function(module, exports, __webpack_require__) { + /************************************ + Relative Time + ************************************/ - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + function relativeTime(posNegDuration, withoutSuffix, locale) { + var duration = moment.duration(posNegDuration).abs(), + seconds = round(duration.as('s')), + minutes = round(duration.as('m')), + hours = round(duration.as('h')), + days = round(duration.as('d')), + months = round(duration.as('M')), + years = round(duration.as('y')), - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + 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.d && ['dd', days] || + months === 1 && ['M'] || + months < relativeTimeThresholds.M && ['MM', months] || + years === 1 && ['y'] || ['yy', years]; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + args[2] = withoutSuffix; + args[3] = +posNegDuration > 0; + args[4] = locale; + return substituteTimeAgo.apply({}, args); + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var CircleImageBase = _interopRequire(__webpack_require__(65)); + /************************************ + Week of Year + ************************************/ - var Image = (function (CircleImageBase) { - function Image(options, body, labelModule, imageObj) { - _classCallCheck(this, Image); - _get(Object.getPrototypeOf(Image.prototype), "constructor", this).call(this, options, body, labelModule); - this.imageObj = imageObj; - } + // 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; - _inherits(Image, CircleImageBase); - _prototypeProperties(Image, null, { - resize: { - value: function resize() { - if (!this.width || !this.height) { - // undefined or 0 - var width, height; - if (this.value) { - var scale = this.imageObj.height / this.imageObj.width; - if (scale !== undefined) { - width = this.options.size || this.imageObj.width; - height = this.options.size * scale || this.imageObj.height; - } else { - width = 0; - height = 0; - } - } else { - width = this.imageObj.width; - height = this.imageObj.height; - } - this.width = width; - this.height = height; + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - this._drawImageAtPosition(ctx); - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } - this._drawImageLabel(ctx, x, y, selected || hover); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); - }, - writable: true, - configurable: true + adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; } - }); - - return Image; - })(CircleImageBase); - - module.exports = Image; - -/***/ }, -/* 74 */ -/***/ function(module, exports, __webpack_require__) { - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + //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; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + 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; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + /************************************ + Top Level Functions + ************************************/ - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + function makeMoment(config) { + var input = config._i, + format = config._f, + res; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + config._locale = config._locale || moment.localeData(config._l); - var ShapeBase = _interopRequire(__webpack_require__(69)); + if (input === null || (format === undefined && input === '')) { + return moment.invalid({nullInput: true}); + } - var Square = (function (ShapeBase) { - function Square(options, body, labelModule) { - _classCallCheck(this, Square); + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } - _get(Object.getPrototypeOf(Square.prototype), "constructor", this).call(this, options, body, labelModule); - } + if (moment.isMoment(input)) { + return new Moment(input, true); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } + } else { + makeDateFromInput(config); + } - _inherits(Square, ShapeBase); + res = new Moment(config); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } - _prototypeProperties(Square, null, { - resize: { - value: function resize() { - this._resizeShape(); - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "square", 2, x, y, selected, hover); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true + return res; } - }); - - return Square; - })(ShapeBase); - - module.exports = Square; -/***/ }, -/* 75 */ -/***/ function(module, exports, __webpack_require__) { + moment = function (input, format, locale, strict) { + var c; - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + if (typeof(locale) === 'boolean') { + strict = locale; + locale = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._i = input; + c._f = format; + c._l = locale; + c._strict = strict; + c._isUTC = false; + c._pf = defaultParsingFlags(); - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + return makeMoment(c); + }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + moment.suppressDeprecationWarnings = false; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + 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 + (config._useUTC ? ' UTC' : '')); + } + ); - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // 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; + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + moment.min = function () { + var args = [].slice.call(arguments, 0); - var ShapeBase = _interopRequire(__webpack_require__(69)); + return pickBy('isBefore', args); + }; - var Star = (function (ShapeBase) { - function Star(options, body, labelModule) { - _classCallCheck(this, Star); + moment.max = function () { + var args = [].slice.call(arguments, 0); - _get(Object.getPrototypeOf(Star.prototype), "constructor", this).call(this, options, body, labelModule); - } + return pickBy('isAfter', args); + }; - _inherits(Star, ShapeBase); + // creating with utc + moment.utc = function (input, format, locale, strict) { + var c; - _prototypeProperties(Star, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "star", 4, x, y, selected, hover); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true - } - }); + if (typeof(locale) === 'boolean') { + strict = locale; + locale = 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 = locale; + c._i = input; + c._f = format; + c._strict = strict; + c._pf = defaultParsingFlags(); - return Star; - })(ShapeBase); + return makeMoment(c).utc(); + }; - module.exports = Star; + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; -/***/ }, -/* 76 */ -/***/ function(module, exports, __webpack_require__) { + // duration + moment.duration = function (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso, + diffRes; - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + 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]) + }; + } else if (duration == null) {// checks for null or undefined + duration = {}; + } else if (typeof duration === 'object' && + ('from' in duration || 'to' in duration)) { + diffRes = momentsDifference(moment(duration.from), moment(duration.to)); - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + ret = new Duration(duration); - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + if (moment.isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + return ret; + }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // version number + moment.version = VERSION; - var NodeBase = _interopRequire(__webpack_require__(63)); + // default format + moment.defaultFormat = isoFormat; - var Text = (function (NodeBase) { - function Text(options, body, labelModule) { - _classCallCheck(this, Text); + // constant that refers to the ISO standard + moment.ISO_8601 = function () {}; - _get(Object.getPrototypeOf(Text.prototype), "constructor", this).call(this, options, body, labelModule); - } + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + moment.momentProperties = momentProperties; - _inherits(Text, NodeBase); + // 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 () {}; - _prototypeProperties(Text, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + // This function allows you to set a threshold for relative time strings + moment.relativeTimeThreshold = function (threshold, limit) { + if (relativeTimeThresholds[threshold] === undefined) { + return false; } - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected || hover); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + if (limit === undefined) { + return relativeTimeThresholds[threshold]; + } + relativeTimeThresholds[threshold] = limit; + return true; + }; - this.labelModule.draw(ctx, x, y, selected || hover); + moment.lang = deprecate( + 'moment.lang is deprecated. Use moment.locale instead.', + function (key, value) { + return moment.locale(key, value); + } + ); - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true - } - }); + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + moment.locale = function (key, values) { + var data; + if (key) { + if (typeof(values) !== 'undefined') { + data = moment.defineLocale(key, values); + } + else { + data = moment.localeData(key); + } - return Text; - })(NodeBase); + if (data) { + moment.duration._locale = moment._locale = data; + } + } - module.exports = Text; + return moment._locale._abbr; + }; -/***/ }, -/* 77 */ -/***/ function(module, exports, __webpack_require__) { + moment.defineLocale = function (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); + } + locales[name].set(values); - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + // backwards compat for now: also set the locale + moment.locale(name); - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } + }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + moment.langData = deprecate( + 'moment.langData is deprecated. Use moment.localeData instead.', + function (key) { + return moment.localeData(key); + } + ); - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + // returns locale data + moment.localeData = function (key) { + var locale; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + if (!key) { + return moment._locale; + } - var ShapeBase = _interopRequire(__webpack_require__(69)); + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } - var Triangle = (function (ShapeBase) { - function Triangle(options, body, labelModule) { - _classCallCheck(this, Triangle); + return chooseLocale(key); + }; - _get(Object.getPrototypeOf(Triangle.prototype), "constructor", this).call(this, options, body, labelModule); - } + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment || + (obj != null && hasOwnProp(obj, '_isAMomentObject')); + }; - _inherits(Triangle, ShapeBase); + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; - _prototypeProperties(Triangle, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "triangle", 3, x, y, selected, hover); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); } - }); - return Triangle; - })(ShapeBase); + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; - module.exports = Triangle; + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } -/***/ }, -/* 78 */ -/***/ function(module, exports, __webpack_require__) { + return m; + }; - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + moment.parseZone = function () { + return moment.apply(null, arguments).parseZone(); + }; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + moment.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + moment.isDate = isDate; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + /************************************ + Moment Prototype + ************************************/ - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + extend(moment.fn = Moment.prototype, { - var ShapeBase = _interopRequire(__webpack_require__(69)); + clone : function () { + return moment(this); + }, - var TriangleDown = (function (ShapeBase) { - function TriangleDown(options, body, labelModule) { - _classCallCheck(this, TriangleDown); + valueOf : function () { + return +this._d - ((this._offset || 0) * 60000); + }, - _get(Object.getPrototypeOf(TriangleDown.prototype), "constructor", this).call(this, options, body, labelModule); - } + unix : function () { + return Math.floor(+this / 1000); + }, - _inherits(TriangleDown, ShapeBase); + toString : function () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + }, - _prototypeProperties(TriangleDown, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); - }, - writable: true, - configurable: true - }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "triangleDown", 3, x, y, selected, hover); - }, - writable: true, - configurable: true - }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true - } - }); + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, - return TriangleDown; - })(ShapeBase); + toISOString : function () { + var m = moment(this).utc(); + if (0 < m.year() && m.year() <= 9999) { + if ('function' === typeof Date.prototype.toISOString) { + // native implementation is ~50x faster, use it when we can + return this.toDate().toISOString(); + } else { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + }, - module.exports = TriangleDown; + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, -/***/ }, -/* 79 */ -/***/ function(module, exports, __webpack_require__) { + isValid : function () { + return isValid(this); + }, - "use strict"; + isDSTShifted : function () { + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + return false; + }, - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + parsingFlags : function () { + return extend({}, this._pf); + }, - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + invalidAt: function () { + return this._pf.overflow; + }, - /** - * Created by Alex on 3/4/2015. - */ + utc : function (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + }, + local : function (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; - var util = __webpack_require__(1); - var DataSet = __webpack_require__(7); - var DataView = __webpack_require__(9); + if (keepLocalTime) { + this.subtract(this._dateUtcOffset(), 'm'); + } + } + return this; + }, - var Edge = _interopRequire(__webpack_require__(80)); + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.localeData().postformat(output); + }, - var EdgesHandler = (function () { - function EdgesHandler(body, images, groups) { - var _this = this; - _classCallCheck(this, EdgesHandler); + add : createAdder(1, 'add'), - this.body = body; - this.images = images; - this.groups = groups; + subtract : createAdder(-1, 'subtract'), - // create the edge API in the body container - this.body.functions.createEdge = this.create.bind(this); + diff : function (input, units, asFloat) { + var that = makeAs(input, this), + zoneDiff = (that.utcOffset() - this.utcOffset()) * 6e4, + anchor, diff, output, daysAdjust; - this.edgesListeners = { - add: function (event, params) { - _this.add(params.items); - }, - update: function (event, params) { - _this.update(params.items); - }, - remove: function (event, params) { - _this.remove(params.items); - } - }; + units = normalizeUnits(units); - this.options = {}; - this.defaultOptions = { - arrows: { - to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: { enabled: false, scaleFactor: 1 }, - from: { enabled: false, scaleFactor: 1 } - }, - color: { - color: "#848484", - highlight: "#848484", - hover: "#848484", - inherit: { - enabled: true, - source: "from", // from / true - useGradients: false // release in 4.0 + if (units === 'year' || units === 'month' || units === 'quarter') { + output = monthDiff(this, that); + if (units === 'quarter') { + output = output / 3; + } else if (units === 'year') { + output = output / 12; + } + } else { + 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); }, - opacity: 1 - }, - dashes: { - enabled: false, - preset: "dotted", - length: 10, - gap: 5, - altLength: undefined - }, - font: { - color: "#343434", - size: 14, // px - face: "arial", - background: "none", - stroke: 1, // px - strokeColor: "white", - align: "horizontal" - }, - hidden: false, - hoverWidth: 1.5, - label: undefined, - length: undefined, - physics: true, - scaling: { - min: 1, - max: 15, - label: { - enabled: true, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 3 + + from : function (time, withoutSuffix) { + return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); }, - customScalingFunction: function (min, max, total, value) { - if (max == min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - selfReferenceSize: 20, - smooth: { - enabled: true, - dynamic: true, - type: "continuous", - roundness: 0.5 - }, - title: undefined, - width: 1, - widthSelectionMultiplier: 2, - value: 1 - }; - util.extend(this.options, this.defaultOptions); + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, + + calendar : function (time) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're locat/utc/offset + // 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.localeData().calendar(format, this, moment(now))); + }, + + isLeapYear : function () { + return isLeapYear(this.year()); + }, + + isDST : function () { + return (this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset()); + }, + + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } + }, + + month : makeAccessor('Month', true), + + 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 */ + } + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } - // this allows external modules to force all dynamic curves to turn static. - this.body.emitter.on("_forceDisableDynamicCurves", function (type) { - var emitChange = false; - for (var edgeId in _this.body.edges) { - if (_this.body.edges.hasOwnProperty(edgeId)) { - var edgeOptions = _this.body.edges[edgeId].options.smooth; - if (edgeOptions.enabled === true && edgeOptions.dynamic === true) { - if (type === undefined) { - edge.setOptions({ smooth: false }); - } else { - edge.setOptions({ smooth: { dynamic: false, type: type } }); + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); } - emitChange = true; - } - } - } - if (emitChange === true) { - _this.body.emitter.emit("_dataChanged"); - } - }); - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on("_dataUpdated", function () { - _this.reconnectEdges(); - _this.markAllEdgesAsDirty(); - }); - } + return this; + }, - _prototypeProperties(EdgesHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.mergeOptions(this.options, options, "smooth"); - util.mergeOptions(this.options, options, "dashes"); + endOf: function (units) { + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond') { + return this; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + }, - // hanlde multiple input cases for arrows - if (options.arrows !== undefined) { - if (typeof options.arrows === "string") { - var arrows = options.arrows.toLowerCase(); - if (arrows.indexOf("to") != -1) { - this.options.arrows.to.enabled = true; - } - if (arrows.indexOf("middle") != -1) { - this.options.arrows.middle.enabled = true; - } - if (arrows.indexOf("from") != -1) { - this.options.arrows.from.enabled = true; - } - } else if (typeof options.arrows === "object") { - util.mergeOptions(this.options.arrows, options.arrows, "to"); - util.mergeOptions(this.options.arrows, options.arrows, "middle"); - util.mergeOptions(this.options.arrows, options.arrows, "from"); + isAfter: function (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = moment.isMoment(input) ? input : moment(input); + return +this > +input; } else { - throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); + inputMs = moment.isMoment(input) ? +input : +moment(input); + return inputMs < +this.clone().startOf(units); } - } + }, - // hanlde multiple input cases for color - if (options.color !== undefined) { - if (util.isString(options.color)) { - util.assignAllKeys(this.options.color, options.color); - this.options.color.inherit.enabled = false; + isBefore: function (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = moment.isMoment(input) ? input : moment(input); + return +this < +input; } else { - util.extend(this.options.color, options.color); - if (options.color.inherit === undefined) { - this.options.color.inherit.enabled = false; - } + inputMs = moment.isMoment(input) ? +input : +moment(input); + return +this.clone().endOf(units) < inputMs; } - util.mergeOptions(this.options.color, options.color, "inherit"); - } - - // font cases are handled by the Label class - } - }, - writable: true, - configurable: true - }, - setData: { - + }, - /** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private - * @private - */ - value: function setData(edges) { - var _this = this; - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var oldEdgesData = this.body.data.edges; + isBetween: function (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + }, - if (edges instanceof DataSet || edges instanceof DataView) { - this.body.data.edges = edges; - } else if (Array.isArray(edges)) { - this.body.data.edges = new DataSet(); - this.body.data.edges.add(edges); - } else if (!edges) { - this.body.data.edges = new DataSet(); - } else { - throw new TypeError("Array or DataSet expected"); - } + isSame: function (input, units) { + var inputMs; + units = normalizeUnits(units || 'millisecond'); + if (units === 'millisecond') { + input = moment.isMoment(input) ? input : moment(input); + return +this === +input; + } else { + inputMs = +moment(input); + return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + } + }, - // TODO: is this null or undefined or false? - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.off(event, callback); - }); - } + 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; + } + ), - // remove drawn edges - this.body.edges = {}; + 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; + } + ), - // TODO: is this null or undefined or false? - if (this.body.data.edges) { - // subscribe to new dataset - util.forEach(this.edgesListeners, function (callback, event) { - _this.body.data.edges.on(event, callback); - }); + zone : deprecate( + 'moment().zone is deprecated, use moment().utcOffset instead. ' + + 'https://github.com/moment/moment/issues/1779', + function (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } - // draw all new nodes - var ids = this.body.data.edges.getIds(); - this.add(ids, true); - } + this.utcOffset(input, keepLocalTime); - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - add: { + return this; + } else { + return -this.utcOffset(); + } + } + ), + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + utcOffset : function (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (input != null) { + if (typeof input === 'string') { + input = utcOffsetFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = this._dateUtcOffset(); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + addOrSubtractDurationFromMoment(this, + moment.duration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + moment.updateOffset(this, true); + this._changeInProgress = null; + } + } - /** - * Add edges - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var edges = this.body.edges; - var edgesData = this.body.data.edges; + return this; + } else { + return this._isUTC ? offset : this._dateUtcOffset(); + } + }, - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; + isLocal : function () { + return !this._isUTC; + }, - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } + isUtcOffset : function () { + return this._isUTC; + }, - var data = edgesData.get(id, { showInternalIds: true }); - edges[id] = this.create(data); - } + isUtc : function () { + return this._isUTC && this._offset === 0; + }, - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - update: { + zoneAbbr : function () { + return this._isUTC ? 'UTC' : ''; + }, + zoneName : function () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + }, + parseZone : function () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(utcOffsetFromString(this._i)); + } + return this; + }, - /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids) { - var edges = this.body.edges; - var edgesData = this.body.data.edges; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var data = edgesData.get(id); - var edge = edges[id]; - if (edge === null) { - // update edge - edge.disconnect(); - dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. - edge.connect(); - } else { - // create edge - this.body.edges[id] = this.create(data); - dataChanged = true; - } - } + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).utcOffset(); + } - if (dataChanged === true) { - this.body.emitter.emit("_dataChanged"); - } else { - this.body.emitter.emit("_dataUpdated"); - } - }, - writable: true, - configurable: true - }, - remove: { + return (this.utcOffset() - input) % 60 === 0; + }, + 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((input - dayOfYear), 'd'); + }, - /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var edges = this.body.edges; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge !== undefined) { - if (edge.via != null) { - delete this.body.supportNodes[edge.via.id]; - } - edge.disconnect(); - delete edges[id]; - } - } + quarter : function (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + }, - this.body.emitter.emit("_dataChanged"); - }, - writable: true, - configurable: true - }, - create: { - value: function create(properties) { - return new Edge(properties, this.body, this.options); - }, - writable: true, - configurable: true - }, - markAllEdgesAsDirty: { - value: function markAllEdgesAsDirty() { - for (var edgeId in this.body.edges) { - this.body.edges[edgeId].colorDirty = true; - } - }, - writable: true, - configurable: true - }, - reconnectEdges: { + weekYear : function (input) { + var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; + return input == null ? year : this.add((input - year), 'y'); + }, + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add((input - year), 'y'); + }, + week : function (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + }, - /** - * Reconnect all edges - * @private - */ - value: function reconnectEdges() { - var id; - var nodes = this.body.nodes; - var edges = this.body.edges; + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + }, - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } + weekday : function (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + }, - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } - } - }, - writable: true, - configurable: true - } - }); + 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); + }, - return EdgesHandler; - })(); + isoWeeksInYear : function () { + return weeksInYear(this.year(), 1, 4); + }, - module.exports = EdgesHandler; + weeksInYear : function () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + }, -/***/ }, -/* 80 */ -/***/ function(module, exports, __webpack_require__) { + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, - "use strict"; + set : function (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } + } + else { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + this[units](value); + } + } + return this; + }, - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + // If passed a locale key, it will set the locale for this + // instance. Otherwise, it will return the locale configuration + // variables for this instance. + locale : function (key) { + var newLocaleData; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = moment.localeData(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + }, - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + lang : deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ), - var util = __webpack_require__(1); + localeData : function () { + return this._locale; + }, + _dateUtcOffset : function () { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(this._d.getTimezoneOffset() / 15) * 15; + } - var Label = _interopRequire(__webpack_require__(61)); + }); - var BezierEdgeDynamic = _interopRequire(__webpack_require__(81)); + function rawMonthSetter(mom, value) { + var dayOfMonth; - var BezierEdgeStatic = _interopRequire(__webpack_require__(84)); + // TODO: Move this out of here! + if (typeof value === 'string') { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } + } - var StraightEdge = _interopRequire(__webpack_require__(85)); + dayOfMonth = Math.min(mom.date(), + daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with options. Must contain - * At least options from and to. - * Available options: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color - */ - var Edge = (function () { - function Edge(options, body, globalOptions) { - _classCallCheck(this, Edge); + function rawGetter(mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } - if (body === undefined) { - throw "No body provided"; + function rawSetter(mom, unit, value) { + if (unit === 'Month') { + return rawMonthSetter(mom, value); + } else { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } } - this.options = util.bridgeObject(globalOptions); - this.body = body; - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.value = undefined; - this.selected = false; - this.hover = false; - this.labelDirty = true; - this.colorDirty = true; + 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); + } + }; + } - this.from = undefined; // a node - this.to = undefined; // a node + 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)); - this.edgeType = undefined; + // 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; - this.connected = false; + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; - this.labelModule = new Label(this.body, this.options); + // alias isUtc for dev-friendliness + moment.fn.isUTC = moment.fn.isUtc; - this.setOptions(options); + /************************************ + Duration Prototype + ************************************/ - this.controlNodesEnabled = false; - this.controlNodes = { from: undefined, to: undefined, positions: {} }; - this.connectedNode = undefined; - } - _prototypeProperties(Edge, null, { - setOptions: { + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; + } + function yearsToDays (years) { + // years * 365 + absRound(years / 4) - + // absRound(years / 100) + absRound(years / 400); + return years * 146097 / 400; + } - /** - * Set or overwrite options for the edge - * @param {Object} options an object with options - * @param doNotEmit - */ - value: function setOptions(options) { - if (!options) { - return; - } - this.colorDirty = true; + extend(moment.duration.fn = Duration.prototype, { - var fields = ["id", "font", "from", "hidden", "hoverWidth", "label", "length", "line", "opacity", "physics", "scaling", "selfReferenceSize", "to", "title", "value", "width", "widthMin", "widthMax", "widthSelectionMultiplier"]; - util.selectiveDeepExtend(fields, this.options, options); + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years = 0; - util.mergeOptions(this.options, options, "smooth"); - util.mergeOptions(this.options, options, "dashes"); + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - if (options.id !== undefined) { - this.id = options.id; - } - if (options.from !== undefined) { - this.fromId = options.from; - } - if (options.to !== undefined) { - this.toId = options.to; - } - if (options.title !== undefined) { - this.title = options.title; - } - if (options.value !== undefined) { - this.value = options.value; - } + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; - // hanlde multiple input cases for arrows - if (options.arrows !== undefined) { - if (typeof options.arrows === "string") { - var arrows = options.arrows.toLowerCase(); - if (arrows.indexOf("to") != -1) { - this.options.arrows.to.enabled = true; - } - if (arrows.indexOf("middle") != -1) { - this.options.arrows.middle.enabled = true; - } - if (arrows.indexOf("from") != -1) { - this.options.arrows.from.enabled = true; - } - } else if (typeof options.arrows === "object") { - util.mergeOptions(this.options.arrows, options.arrows, "to"); - util.mergeOptions(this.options.arrows, options.arrows, "middle"); - util.mergeOptions(this.options.arrows, options.arrows, "from"); - } else { - throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); - } - } + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; - // hanlde multiple input cases for color - if (options.color !== undefined) { - if (util.isString(options.color)) { - util.assignAllKeys(this.options.color, options.color); - this.options.color.inherit.enabled = false; - } else { - util.extend(this.options.color, options.color); - if (options.color.inherit === undefined) { - this.options.color.inherit.enabled = false; - } - } - util.mergeOptions(this.options.color, options.color, "inherit"); - } + hours = absRound(minutes / 60); + data.hours = hours % 24; - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); + days += absRound(hours / 24); - this.labelModule.setOptions(this.options); + // Accurately convert days to years, assume start from year 0. + years = absRound(daysToYears(days)); + days -= absRound(yearsToDays(years)); - var dataChanged = this.updateEdgeType(); - return dataChanged; - }, - writable: true, - configurable: true - }, - updateEdgeType: { - value: function updateEdgeType() { - var dataChanged = false; - var changeInType = true; - if (this.edgeType !== undefined) { - if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { - changeInType = false; - } - if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false) { - changeInType = false; - } - if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) { - changeInType = false; - } + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absRound(days / 30); + days %= 30; - if (changeInType == true) { - dataChanged = this.edgeType.cleanup(); - } - } + // 12 months -> 1 year + years += absRound(months / 12); + months %= 12; - if (changeInType === true) { - if (this.options.smooth.enabled === true) { - if (this.options.smooth.dynamic === true) { - dataChanged = true; - this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); - } else { - this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); - } - } else { - this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); - } - } else { - // if nothing changes, we just set the options. - this.edgeType.setOptions(this.options); - } + data.days = days; + data.months = months; + data.years = years; + }, - return dataChanged; - }, - writable: true, - configurable: true - }, - togglePhysics: { + abs : function () { + this._milliseconds = Math.abs(this._milliseconds); + this._days = Math.abs(this._days); + this._months = Math.abs(this._months); + this._data.milliseconds = Math.abs(this._data.milliseconds); + this._data.seconds = Math.abs(this._data.seconds); + this._data.minutes = Math.abs(this._data.minutes); + this._data.hours = Math.abs(this._data.hours); + this._data.months = Math.abs(this._data.months); + this._data.years = Math.abs(this._data.years); - /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { - if (this.via === undefined) { - this.via.pptions.physics = status; - } - } - this.options.physics = status; - }, - writable: true, - configurable: true - }, - connect: { + return this; + }, - /** - * Connect an edge to its nodes - */ - value: function connect() { - this.disconnect(); + weeks : function () { + return absRound(this.days() / 7); + }, - this.from = this.body.nodes[this.fromId] || undefined; - this.to = this.body.nodes[this.toId] || undefined; - this.connected = this.from !== undefined && this.to !== undefined; + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; + }, - if (this.connected === true) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } else { - if (this.from) { - this.from.detachEdge(this); - } - if (this.to) { - this.to.detachEdge(this); - } - } - }, - writable: true, - configurable: true - }, - disconnect: { + humanize : function (withSuffix) { + var output = relativeTime(this, !withSuffix, this.localeData()); + if (withSuffix) { + output = this.localeData().pastFuture(+this, output); + } - /** - * Disconnect an edge from its nodes - */ - value: function disconnect() { - if (this.from) { - this.from.detachEdge(this); - this.from = undefined; - } - if (this.to) { - this.to.detachEdge(this); - this.to = undefined; - } + return this.localeData().postformat(output); + }, - this.connected = false; - }, - writable: true, - configurable: true - }, - getTitle: { + add : function (input, val) { + // supports only 2.0-style add(1, 's') or add(moment) + var dur = moment.duration(input, val); + this._milliseconds += dur._milliseconds; + this._days += dur._days; + this._months += dur._months; - /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. - */ - value: function getTitle() { - return typeof this.title === "function" ? this.title() : this.title; - }, - writable: true, - configurable: true - }, - isSelected: { + this._bubble(); + return this; + }, - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - }, - writable: true, - configurable: true - }, - getValue: { + subtract : function (input, val) { + var dur = moment.duration(input, val); + this._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; + this._bubble(); - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.value; - }, - writable: true, - configurable: true - }, - setValueRange: { + return this; + }, + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); + }, - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - * @param total - */ - value: function setValueRange(min, max, total) { - if (this.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); - var widthDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled == true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.width = this.options.scaling.min + scale * widthDiff; - } - }, - writable: true, - configurable: true - }, - draw: { + as : function (units) { + var days, months; + units = normalizeUnits(units); + if (units === 'month' || units === 'year') { + days = this._days + this._milliseconds / 864e5; + months = this._months + daysToYears(days) * 12; + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(yearsToDays(this._months / 12)); + switch (units) { + case 'week': return days / 7 + this._milliseconds / 6048e5; + case 'day': return days + this._milliseconds / 864e5; + case 'hour': return days * 24 + this._milliseconds / 36e5; + case 'minute': return days * 24 * 60 + this._milliseconds / 6e4; + case 'second': return days * 24 * 60 * 60 + this._milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 24 * 60 * 60 * 1000) + this._milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } + }, - /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - var via = this.edgeType.drawLine(ctx, this.selected, this.hover); - this.drawArrows(ctx, via); - this.drawLabel(ctx, via); - }, - writable: true, - configurable: true - }, - drawArrows: { - value: function drawArrows(ctx, viaNode) { - if (this.options.arrows.from.enabled === true) { - this.edgeType.drawArrowHead(ctx, "from", viaNode, this.selected, this.hover); - } - if (this.options.arrows.middle.enabled === true) { - this.edgeType.drawArrowHead(ctx, "middle", viaNode, this.selected, this.hover); - } - if (this.options.arrows.to.enabled === true) { - this.edgeType.drawArrowHead(ctx, "to", viaNode, this.selected, this.hover); - } - }, - writable: true, - configurable: true - }, - drawLabel: { - value: function drawLabel(ctx, viaNode) { - if (this.options.label !== undefined) { - // set style - var node1 = this.from; - var node2 = this.to; - var selected = this.from.selected || this.to.selected || this.selected; - if (node1.id != node2.id) { - var point = this.edgeType.getPoint(0.5, viaNode); - ctx.save(); + lang : moment.fn.lang, + locale : moment.fn.locale, - // if the label has to be rotated: - if (this.options.font.align !== "horizontal") { - this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); - ctx.translate(point.x, this.labelModule.size.yLine); - this._rotateForLabelAlignment(ctx); + toIsoString : deprecate( + 'toIsoString() is deprecated. Please use toISOString() instead ' + + '(notice the capitals)', + function () { + return this.toISOString(); } + ), + + 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); - // draw the label - this.labelModule.draw(ctx, point.x, point.y, selected); - ctx.restore(); - } else { - var x, y; - var radius = this.options.selfReferenceSize; - if (node1.width > node1.height) { - x = node1.x + node1.width * 0.5; - y = node1.y - radius; - } else { - x = node1.x + radius; - y = node1.y - node1.height * 0.5; + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; } - point = this._pointOnCircle(x, y, radius, 0.125); - this.labelModule.draw(ctx, point.x, point.y, selected); - } - } - }, - writable: true, - configurable: true - }, - isOverlappingWith: { + 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' : ''); + }, + localeData : function () { + return this._locale; + }, - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top - * @return {boolean} True if location is located on the edge - */ - value: function isOverlappingWith(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; + toJSON : function () { + return this.toISOString(); + } + }); - var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + moment.duration.fn.toString = moment.duration.fn.toISOString; - return dist < distMax; - } else { - return false; + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } + + for (i in unitMillisecondFactors) { + if (hasOwnProp(unitMillisecondFactors, i)) { + makeDurationGetter(i.toLowerCase()); } - }, - writable: true, - configurable: true - }, - _rotateForLabelAlignment: { + } + + moment.duration.fn.asMilliseconds = function () { + return this.as('ms'); + }; + moment.duration.fn.asSeconds = function () { + return this.as('s'); + }; + moment.duration.fn.asMinutes = function () { + return this.as('m'); + }; + moment.duration.fn.asHours = function () { + return this.as('h'); + }; + moment.duration.fn.asDays = function () { + return this.as('d'); + }; + moment.duration.fn.asWeeks = function () { + return this.as('weeks'); + }; + moment.duration.fn.asMonths = function () { + return this.as('M'); + }; + moment.duration.fn.asYears = function () { + return this.as('y'); + }; + /************************************ + Default Locale + ************************************/ - /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _rotateForLabelAlignment(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; + // Set default locale, other locale will inherit from English. + moment.locale('en', { + ordinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; } + }); - ctx.rotate(angleInDegrees); - }, - writable: true, - configurable: true - }, - _pointOnCircle: { + /* EMBED_LOCALES */ + /************************************ + Exposing Moment + ************************************/ - /** - * Get a point on a circle - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private - */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; - }, - writable: true, - configurable: true - }, - select: { - value: function select() { - this.selected = true; - }, - writable: true, - configurable: true - }, - unselect: { - value: function unselect() { - this.selected = false; - }, - writable: true, - configurable: true + 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; + } } - }); - return Edge; - })(); + // 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; + } - module.exports = Edge; + 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__(98)(module))) /***/ }, -/* 81 */ +/* 64 */ /***/ function(module, exports, __webpack_require__) { - "use strict"; + var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 + * http://hammerjs.github.io/ + * + * Copyright (c) 2014 Jorik Tangelder; + * Licensed under the MIT license */ + (function(window, document, exportName, undefined) { + 'use strict'; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var TYPE_FUNCTION = 'function'; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var round = Math.round; + var abs = Math.abs; + var now = Date.now; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + /** + * set a timeout with a given scope + * @param {Function} fn + * @param {Number} timeout + * @param {Object} context + * @returns {number} + */ + function setTimeoutContext(fn, timeout, context) { + return setTimeout(bindFn(fn, context), timeout); + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * if the argument is an array, we want to execute the fn on each entry + * if it aint an array we don't want to do a thing. + * this is used by all the methods that accept a single and array argument. + * @param {*|Array} arg + * @param {String} fn + * @param {Object} [context] + * @returns {Boolean} + */ + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; + } + return false; + } /** - * Created by Alex on 3/20/2015. + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context */ + function each(obj, iterator, context) { + var i; - var BezierEdgeBase = _interopRequire(__webpack_require__(82)); + if (!obj) { + return; + } - var BezierEdgeDynamic = (function (BezierEdgeBase) { - function BezierEdgeDynamic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeDynamic); + if (obj.forEach) { + obj.forEach(iterator, context); + } else if (obj.length !== undefined) { + i = 0; + while (i < obj.length) { + iterator.call(context, obj[i], i, obj); + i++; + } + } else { + for (i in obj) { + obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); + } + } + } - this.via = undefined; - _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); // --> this calls the setOptions below - } + /** + * extend object. + * means that properties in dest will be overwritten by the ones in src. + * @param {Object} dest + * @param {Object} src + * @param {Boolean} [merge] + * @returns {Object} dest + */ + function extend(dest, src, merge) { + var keys = Object.keys(src); + var i = 0; + while (i < keys.length) { + if (!merge || (merge && dest[keys[i]] === undefined)) { + dest[keys[i]] = src[keys[i]]; + } + i++; + } + return dest; + } - _inherits(BezierEdgeDynamic, BezierEdgeBase); + /** + * merge the values from src in the dest. + * means that properties that exist in dest will not be overwritten by src + * @param {Object} dest + * @param {Object} src + * @returns {Object} dest + */ + function merge(dest, src) { + return extend(dest, src, true); + } - _prototypeProperties(BezierEdgeDynamic, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; - this.setupSupportNode(); - }, - writable: true, - configurable: true - }, - cleanup: { - value: function cleanup() { - if (this.via !== undefined) { - delete this.body.nodes[this.via.id]; - this.via = undefined; - return true; - } - return false; - }, - writable: true, - configurable: true - }, - setupSupportNode: { + /** + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] + */ + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - /** - * 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. - * - * The changed data is not called, if needed, it is returned by the main edge constructor. - * @private - */ - value: function setupSupportNode() { - if (this.via === undefined) { - var nodeId = "edgeId:" + this.id; - var node = this.body.functions.createNode({ - id: nodeId, - mass: 1, - shape: "circle", - image: "", - physics: true, - hidden: true - }); - this.body.nodes[nodeId] = node; - this.via = node; - this.via.parentEdgeId = this.id; - this.positionBezierNode(); - } - }, - writable: true, - configurable: true - }, - positionBezierNode: { - value: function positionBezierNode() { - if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); - } else if (this.via !== undefined) { - this.via.x = 0; - this.via.y = 0; - } - }, - writable: true, - configurable: true - }, - _line: { + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - ctx.stroke(); - return this.via; - }, - writable: true, - configurable: true - }, - getPoint: { + if (properties) { + extend(childP, properties); + } + } + /** + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} + */ + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } - /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + /** + * let a boolean value also be a function that must return a boolean + * this first item in args will be used as the context + * @param {Boolean|Function} val + * @param {Array} [args] + * @returns {Boolean} + */ + function boolOrFn(val, args) { + if (typeof val == TYPE_FUNCTION) { + return val.apply(args ? args[0] || undefined : undefined, args); + } + return val; + } - return { x: x, y: y }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - return this._findBorderPositionBezier(nearNode, ctx, this.via); - }, - writable: true, - configurable: true - }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); - }, - writable: true, - configurable: true + /** + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} + */ + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } + + /** + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler + */ + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } + + /** + * removeEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler + */ + function removeEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.removeEventListener(type, handler, false); + }); + } + + /** + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found + */ + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; + } + node = node.parentNode; } - }); - - return BezierEdgeDynamic; - })(BezierEdgeBase); + return false; + } - module.exports = BezierEdgeDynamic; + /** + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found + */ + function inStr(str, find) { + return str.indexOf(find) > -1; + } -/***/ }, -/* 82 */ -/***/ function(module, exports, __webpack_require__) { + /** + * split string on whitespace + * @param {String} str + * @returns {Array} words + */ + function splitStr(str) { + return str.trim().split(/\s+/g); + } - "use strict"; + /** + * find if a array contains the object using indexOf or a simple polyFill + * @param {Array} src + * @param {String} find + * @param {String} [findByKey] + * @return {Boolean|Number} false when not found, or the index + */ + function inArray(src, find, findByKey) { + if (src.indexOf && !findByKey) { + return src.indexOf(find); + } else { + var i = 0; + while (i < src.length) { + if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { + return i; + } + i++; + } + return -1; + } + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + /** + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} + */ + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + /** + * unique array with objects based on a key (like 'id') or just by the array's value + * @param {Array} src [{id:1},{id:2},{id:1}] + * @param {String} [key] + * @param {Boolean} [sort=False] + * @returns {Array} [{id:1},{id:2}] + */ + function uniqueArray(src, key, sort) { + var results = []; + var values = []; + var i = 0; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + while (i < src.length) { + var val = key ? src[i][key] : src[i]; + if (inArray(values, val) < 0) { + results.push(src[i]); + } + values[i] = val; + i++; + } - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); + } + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + return results; + } /** - * Created by Alex on 3/20/2015. + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ + function prefixed(obj, property) { + var prefix, prop; + var camelProp = property[0].toUpperCase() + property.slice(1); - var EdgeBase = _interopRequire(__webpack_require__(83)); + var i = 0; + while (i < VENDOR_PREFIXES.length) { + prefix = VENDOR_PREFIXES[i]; + prop = (prefix) ? prefix + camelProp : property; - var BezierEdgeBase = (function (EdgeBase) { - function BezierEdgeBase(options, body, labelModule) { - _classCallCheck(this, BezierEdgeBase); + if (prop in obj) { + return prop; + } + i++; + } + return undefined; + } - _get(Object.getPrototypeOf(BezierEdgeBase.prototype), "constructor", this).call(this, options, body, labelModule); - } + /** + * get a unique id + * @returns {number} uniqueId + */ + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } - _inherits(BezierEdgeBase, EdgeBase); + /** + * get the window object of an element + * @param {HTMLElement} element + * @returns {DocumentView|Window} + */ + function getWindowForElement(element) { + var doc = element.ownerDocument; + return (doc.defaultView || doc.parentWindow); + } - _prototypeProperties(BezierEdgeBase, null, { - _findBorderPositionBezier: { + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. - * - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - */ - value: function _findBorderPositionBezier(nearNode, ctx) { - var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; - var maxIterations = 10; - var iteration = 0; - var low = 0; - var high = 1; - var pos, angle, distanceToBorder, distanceToPoint, difference; - var threshold = 0.2; - var node = this.to; - var from = false; - if (nearNode.id === this.from.id) { - node = this.from; - from = true; - } + var SUPPORT_TOUCH = ('ontouchstart' in window); + var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined; + var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent); - while (low <= high && iteration < maxIterations) { - var middle = (low + high) * 0.5; + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - pos = this.getPoint(middle, viaNode); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - if (Math.abs(difference) < threshold) { - break; // found - } else if (difference < 0) { - // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (from == false) { - low = middle; - } else { - high = middle; - } - } else { - if (from == false) { - high = middle; - } else { - low = middle; - } - } + var COMPUTE_INTERVAL = 25; - iteration++; - } - pos.t = middle; + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; - return pos; - }, - writable: true, - configurable: true - }, - _getDistanceToBezierEdge: { + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; + var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; + var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; + var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + var PROPS_XY = ['x', 'y']; + var PROPS_CLIENT_XY = ['clientX', 'clientY']; - /** - * 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 - */ - value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var xVia = undefined, - yVia = undefined; - xVia = via.x; - yVia = via.y; - var minDistance = 1000000000; - var distance = undefined; - var i = undefined, - t = undefined, - x = undefined, - y = undefined; - var lastX = x1; - var lastY = y1; - for (i = 1; 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; - } + /** + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} + * @constructor + */ + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; - return minDistance; - }, - writable: true, - configurable: true - } - }); + // smaller wrapper around the handler, for the scope and the enabled state of the manager, + // so when disabled the input events are completely bypassed. + this.domHandler = function(ev) { + if (boolOrFn(manager.options.enable, [manager])) { + self.handler(ev); + } + }; - return BezierEdgeBase; - })(EdgeBase); + this.init(); - module.exports = BezierEdgeBase; + } -/***/ }, -/* 83 */ -/***/ function(module, exports, __webpack_require__) { + Input.prototype = { + /** + * should handle the inputEvent data and trigger the callback + * @virtual + */ + handler: function() { }, - "use strict"; + /** + * bind the events + */ + init: function() { + this.evEl && addEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + }, - var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; + /** + * unbind the events + */ + destroy: function() { + this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); + this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); + this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); + } + }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + /** + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} + */ + function createInputInstance(manager) { + var Type; + var inputClass = manager.options.inputClass; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + if (inputClass) { + Type = inputClass; + } else if (SUPPORT_POINTER_EVENTS) { + Type = PointerEventInput; + } else if (SUPPORT_ONLY_TOUCH) { + Type = TouchInput; + } else if (!SUPPORT_TOUCH) { + Type = MouseInput; + } else { + Type = TouchMouseInput; + } + return new (Type)(manager, inputHandler); + } /** - * Created by Alex on 3/20/2015. + * handle input events + * @param {Manager} manager + * @param {String} eventType + * @param {Object} input */ - var util = __webpack_require__(1); - - var EdgeBase = (function () { - function EdgeBase(options, body, labelModule) { - _classCallCheck(this, EdgeBase); + function inputHandler(manager, eventType, input) { + var pointersLen = input.pointers.length; + var changedPointersLen = input.changedPointers.length; + var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0)); + var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0)); - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.colorDirty = true; - } + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; - _prototypeProperties(EdgeBase, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; - }, - writable: true, - configurable: true - }, - drawLine: { + if (isFirst) { + manager.session = {}; + } - /** - * Redraw a edge as a line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function drawLine(ctx, selected, hover) { - // set style - ctx.strokeStyle = this.getColor(ctx); - ctx.lineWidth = this.getLineWidth(selected, hover); - var via = undefined; - if (this.from != this.to) { - // draw line - if (this.options.dashes.enabled == true) { - via = this._drawDashedLine(ctx); - } else { - via = this._line(ctx); - } - } else { - var _getCircleData = this._getCircleData(); + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - var _getCircleData2 = _slicedToArray(_getCircleData, 3); + // compute scale, rotation etc + computeInputData(manager, input); - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; - this._circle(ctx, x, y, radius); - } + // emit secret event + manager.emit('hammer.input', input); - return via; - }, - writable: true, - configurable: true - }, - _drawDashedLine: { - value: function _drawDashedLine(ctx) { - var via = undefined; - // only firefox and chrome support this method, else we use the legacy one. - if (ctx.setLineDash !== undefined) { - ctx.save(); - // configure the dash pattern - var pattern = [0]; - if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) { - pattern = [this.options.dashes.length, this.options.dashes.gap]; - } else { - pattern = [5, 5]; - } + manager.recognize(input); + manager.session.prevInput = input; + } - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; + /** + * extend the data with some usable properties like scale, rotate, velocity etc + * @param {Object} manager + * @param {Object} input + */ + function computeInputData(manager, input) { + var session = manager.session; + var pointers = input.pointers; + var pointersLength = pointers.length; - // draw the line - via = this._line(ctx); + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); + } - // restore the dash settings. - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; - ctx.restore(); - } else { - // unsupporting smooth lines - // draw dashes line - ctx.beginPath(); - ctx.lineCap = "round"; - if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]); - } else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap]); - } else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); - } - return via; - }, - writable: true, - configurable: true - }, - findBorderPosition: { - value: function findBorderPosition(nearNode, ctx, options) { - if (this.from != this.to) { - return this._findBorderPosition(nearNode, ctx, options); - } else { - return this._findBorderPositionCircle(nearNode, ctx, options); - } - }, - writable: true, - configurable: true - }, - findBorderPositions: { - value: function findBorderPositions(ctx) { - var from = {}; - var to = {}; - if (this.from != this.to) { - from = this._findBorderPosition(this.from, ctx); - to = this._findBorderPosition(this.to, ctx); - } else { - var _getCircleData = this._getCircleData(); + // to compute scale and rotation we need to store the multiple touches + if (pointersLength > 1 && !session.firstMultiple) { + session.firstMultiple = simpleCloneInputData(input); + } else if (pointersLength === 1) { + session.firstMultiple = false; + } - var _getCircleData2 = _slicedToArray(_getCircleData, 3); + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); - from = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); - to = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.6, high: 0.8, direction: 1 }); - } - return { from: from, to: to }; - }, - writable: true, - configurable: true - }, - _getCircleData: { - value: function _getCircleData() { - var x = undefined, - y = undefined; - var node = this.from; - var radius = this.options.selfReferenceSize; + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - // get circle coordinates - if (node.shape.width > node.shape.height) { - x = node.x + node.shape.width * 0.5; - y = node.y - radius; - } else { - x = node.x + radius; - y = node.y - node.shape.height * 0.5; - } - return [x, y, radius]; - }, - writable: true, - configurable: true - }, - _pointOnCircle: { + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - /** - * Get a point on a circle - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private - */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; - }, - writable: true, - configurable: true - }, - _findBorderPositionCircle: { + computeIntervalInputData(session, input); - /** - * This function uses binary search to look for the point where the circle crosses the border of the node. - * @param node - * @param ctx - * @param options - * @returns {*} - * @private - */ - value: function _findBorderPositionCircle(node, ctx, options) { - var x = options.x; - var y = options.y; - var low = options.low; - var high = options.high; - var direction = options.direction; + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; + } + input.target = target; + } - var maxIterations = 10; - var iteration = 0; - var radius = this.options.selfReferenceSize; - var pos = undefined, - angle = undefined, - distanceToBorder = undefined, - distanceToPoint = undefined, - difference = undefined; - var threshold = 0.05; - var middle = (low + high) * 0.5; + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; - while (low <= high && iteration < maxIterations) { - middle = (low + high) * 0.5; + if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { + prevDelta = session.prevDelta = { + x: prevInput.deltaX || 0, + y: prevInput.deltaY || 0 + }; - pos = this._pointOnCircle(x, y, radius, middle); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - if (Math.abs(difference) < threshold) { - break; // found - } else if (difference > 0) { - // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (direction > 0) { - low = middle; - } else { - high = middle; - } - } else { - if (direction > 0) { - high = middle; - } else { - low = middle; - } - } - iteration++; - } - pos.t = middle; + offset = session.offsetDelta = { + x: center.x, + y: center.y + }; + } - return pos; - }, - writable: true, - configurable: true - }, - getLineWidth: { + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); + } - /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width - * @private - */ - value: function getLineWidth(selected, hover) { - if (selected == true) { - return Math.max(Math.min(this.options.widthSelectionMultiplier * this.options.width, this.options.scaling.max), 0.3 / this.body.view.scale); - } else { - if (hover == true) { - return Math.max(Math.min(this.options.hoverWidth, this.options.scaling.max), 0.3 / this.body.view.scale); - } else { - return Math.max(this.options.width, 0.3 / this.body.view.scale); - } - } - }, - writable: true, - configurable: true - }, - getColor: { - value: function getColor(ctx) { - var colorObj = this.options.color; + /** + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input + */ + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - if (colorObj.inherit.enabled === true) { - if (colorObj.inherit.useGradients == true) { - var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); - var fromColor, toColor; - fromColor = this.from.options.color.highlight.border; - toColor = this.to.options.color.highlight.border; + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - if (this.from.selected == false && this.to.selected == false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); - } else if (this.from.selected == true && this.to.selected == false) { - toColor = this.to.options.color.border; - } else if (this.from.selected == false && this.to.selected == true) { - fromColor = this.from.options.color.border; - } - grd.addColorStop(0, fromColor); - grd.addColorStop(1, toColor); + var v = getVelocity(deltaTime, deltaX, deltaY); + velocityX = v.x; + velocityY = v.y; + velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; + direction = getDirection(deltaX, deltaY); - // -------------------- this returns -------------------- // - return grd; - } + session.lastInterval = input; + } else { + // use latest velocity info if it doesn't overtake a minimum period + velocity = last.velocity; + velocityX = last.velocityX; + velocityY = last.velocityY; + direction = last.direction; + } - if (this.colorDirty === true) { - if (colorObj.inherit.source == "to") { - colorObj.highlight = this.to.options.color.highlight.border; - colorObj.hover = this.to.options.color.hover.border; - colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); - } else { - // (this.options.color.inherit.source == "from") { - colorObj.highlight = this.from.options.color.highlight.border; - colorObj.hover = this.from.options.color.hover.border; - colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); - } - } - } + input.velocity = velocity; + input.velocityX = velocityX; + input.velocityY = velocityY; + input.direction = direction; + } - // if color inherit is on and gradients are used, the function has already returned by now. - this.colorDirty = false; + /** + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData + */ + function simpleCloneInputData(input) { + // make a simple copy of the pointers because we will get a reference if we don't + // we only need clientXY for the calculations + var pointers = []; + var i = 0; + while (i < input.pointers.length) { + pointers[i] = { + clientX: round(input.pointers[i].clientX), + clientY: round(input.pointers[i].clientY) + }; + i++; + } - if (this.selected == true) { - return colorObj.highlight; - } else if (this.hover == true) { - return colorObj.hover; - } else { - return colorObj.color; - } - }, - writable: true, - configurable: true - }, - _circle: { + return { + timeStamp: now(), + pointers: pointers, + center: getCenter(pointers), + deltaX: input.deltaX, + deltaY: input.deltaY + }; + } - /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @private - */ - value: function _circle(ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); - }, - writable: true, - configurable: true - }, - getDistanceToEdge: { + /** + * get the center of all the pointers + * @param {Array} pointers + * @return {Object} center contains `x` and `y` properties + */ + function getCenter(pointers) { + var pointersLength = pointers.length; + // no need to loop when only one touch + if (pointersLength === 1) { + return { + x: round(pointers[0].clientX), + y: round(pointers[0].clientY) + }; + } - /** - * 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 - */ - value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var returnValue = 0; - if (this.from != this.to) { - returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); - } else { - var _getCircleData = this._getCircleData(); + var x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; + i++; + } - var _getCircleData2 = _slicedToArray(_getCircleData, 3); + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; + } - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; - var dx = x - x3; - var dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); - } + /** + * calculate the velocity between two points. unit is in px per ms. + * @param {Number} deltaTime + * @param {Number} x + * @param {Number} y + * @return {Object} velocity `x` and `y` + */ + function getVelocity(deltaTime, x, y) { + return { + x: x / deltaTime || 0, + y: y / deltaTime || 0 + }; + } - if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { - return 0; - } else { - return returnValue; - } - }, - writable: true, - configurable: true - }, - _getDistanceToLine: { - value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { - var px = x2 - x1; - var py = y2 - y1; - var something = px * px + py * py; - var u = ((x3 - x1) * px + (y3 - y1) * py) / something; + /** + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction + */ + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; + } - if (u > 1) { - u = 1; - } else if (u < 0) { - u = 0; - } + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; + } - var x = x1 + u * px; - var y = y1 + u * py; - var dx = x - x3; - var dy = y - y3; + /** + * calculate the absolute distance between two points + * @param {Object} p1 {x, y} + * @param {Object} p2 {x, y} + * @param {Array} [props] containing x and y keys + * @return {Number} distance + */ + function getDistance(p1, p2, props) { + if (!props) { + props = PROPS_XY; + } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance + return Math.sqrt((x * x) + (y * y)); + } - return Math.sqrt(dx * dx + dy * dy); - }, - writable: true, - configurable: true - }, - drawArrowHead: { + /** + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle + */ + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; + } + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; + } - /** - * - * @param ctx - * @param position - * @param viaNode - */ - value: function drawArrowHead(ctx, position, viaNode, selected, hover) { - // set style - ctx.strokeStyle = this.getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this.getLineWidth(selected, hover); + /** + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation + */ + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); + } - // set lets - var angle = undefined; - var length = undefined; - var arrowPos = undefined; - var node1 = undefined; - var node2 = undefined; - var guideOffset = undefined; - var scaleFactor = undefined; + /** + * calculate the scale factor between two pointersets + * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} scale + */ + function getScale(start, end) { + return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); + } - if (position == "from") { - node1 = this.from; - node2 = this.to; - guideOffset = 0.1; - scaleFactor = this.options.arrows.from.scaleFactor; - } else if (position == "to") { - node1 = this.to; - node2 = this.from; - guideOffset = -0.1; - scaleFactor = this.options.arrows.to.scaleFactor; - } else { - node1 = this.to; - node2 = this.from; - scaleFactor = this.options.arrows.middle.scaleFactor; - } + var MOUSE_INPUT_MAP = { + mousedown: INPUT_START, + mousemove: INPUT_MOVE, + mouseup: INPUT_END + }; - // if not connected to itself - if (node1 != node2) { - if (position !== "middle") { - // draw arrow head - if (this.options.smooth.enabled == true) { - arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); - var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); - angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); - } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.findBorderPosition(node1, ctx); - } - } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. - } - // draw arrow at the end of the line - length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(arrowPos.x, arrowPos.y, angle, length); - ctx.fill(); - ctx.stroke(); - } else { - // draw circle - var _angle = undefined, - point = undefined; - var _getCircleData = this._getCircleData(); + var MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - var _getCircleData2 = _slicedToArray(_getCircleData, 3); + /** + * Mouse events input + * @constructor + * @extends Input + */ + function MouseInput() { + this.evEl = MOUSE_ELEMENT_EVENTS; + this.evWin = MOUSE_WINDOW_EVENTS; - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; + this.allow = true; // used by Input.TouchMouse to disable mouse events + this.pressed = false; // mousedown state + Input.apply(this, arguments); + } - if (position == "from") { - point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; - } else if (position == "to") { - point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.6, high: 1, direction: 1 }); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; - } else { - point = this._pointOnCircle(x, y, radius, 0.175); - _angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; - } + inherit(MouseInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; - // draw the arrowhead - var _length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(point.x, point.y, _angle, _length); - ctx.fill(); - ctx.stroke(); + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = true; } - }, - writable: true, - configurable: true - } - }); - - return EdgeBase; - })(); - module.exports = EdgeBase; + if (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; + } -/***/ }, -/* 84 */ -/***/ function(module, exports, __webpack_require__) { + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; + } - "use strict"; + if (eventType & INPUT_END) { + this.pressed = false; + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + this.callback(this.manager, eventType, { + pointers: [ev], + changedPointers: [ev], + pointerType: INPUT_TYPE_MOUSE, + srcEvent: ev + }); + } + }); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var POINTER_INPUT_MAP = { + pointerdown: INPUT_START, + pointermove: INPUT_MOVE, + pointerup: INPUT_END, + pointercancel: INPUT_CANCEL, + pointerout: INPUT_CANCEL + }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + // in IE10 the pointer types is defined as an enum + var IE10_POINTER_TYPE_ENUM = { + 2: INPUT_TYPE_TOUCH, + 3: INPUT_TYPE_PEN, + 4: INPUT_TYPE_MOUSE, + 5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816 + }; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var POINTER_ELEMENT_EVENTS = 'pointerdown'; + var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // IE10 has prefixed support, and case-sensitive + if (window.MSPointerEvent) { + POINTER_ELEMENT_EVENTS = 'MSPointerDown'; + POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; + } /** - * Created by Alex on 3/20/2015. + * Pointer events input + * @constructor + * @extends Input */ + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - var BezierEdgeBase = _interopRequire(__webpack_require__(82)); - - var BezierEdgeStatic = (function (BezierEdgeBase) { - function BezierEdgeStatic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeStatic); - - _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); - } - - _inherits(BezierEdgeStatic, BezierEdgeBase); + Input.apply(this, arguments); - _prototypeProperties(BezierEdgeStatic, null, { - cleanup: { - value: function cleanup() { - return false; - }, - writable: true, - configurable: true - }, - _line: { - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - var via = this._getViaCoordinates(); + this.store = (this.manager.session.pointerEvents = []); + } - // fallback to normal straight edges - if (via.x === undefined) { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return undefined; - } else { - ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); - ctx.stroke(); - return via; - } - }, - writable: true, - configurable: true - }, - _getViaCoordinates: { - value: function _getViaCoordinates() { - var xVia = undefined; - var yVia = undefined; - var factor = this.options.smooth.roundness; - var type = this.options.smooth.type; - 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; - } - } - } 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; - } else { - yVia = this.to.y + (1 - factor) * dy; - } - } 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 { - xVia = this.to.x + (1 - factor) * dx; - } - 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; - } else { - yVia = this.to.y + (1 - factor) * dy; - } - } else if (type == "curvedCW") { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; + inherit(PointerEventInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); - yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); - } else if (type == "curvedCCW") { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; + var isTouch = (pointerType == INPUT_TYPE_TOUCH); - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); - yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); - } 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) { - 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) { - 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) { - 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) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - xVia = this.to.x > xVia ? this.to.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; - yVia = this.to.y > yVia ? this.to.y : yVia; - } else if (this.from.x > this.to.x) { - 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.y < this.to.y) { - if (this.from.x < this.to.x) { - 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) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - yVia = this.to.y < yVia ? this.to.y : yVia; - } + // start and mouse must be down + if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { + if (storeIndex < 0) { + store.push(ev); + storeIndex = store.length - 1; } - } + } else if (eventType & (INPUT_END | INPUT_CANCEL)) { + removePointer = true; } - return { x: xVia, y: yVia }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - var options = arguments[2] === undefined ? {} : arguments[2]; - return this._findBorderPositionBezier(nearNode, ctx, options.via); - }, - writable: true, - configurable: true - }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); - }, - writable: true, - configurable: true - }, - getPoint: { - - /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; - - return { x: x, y: y }; - }, - writable: true, - configurable: true - } - }); - - return BezierEdgeStatic; - })(BezierEdgeBase); - - module.exports = BezierEdgeStatic; - -/***/ }, -/* 85 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + // update the event in the store + store[storeIndex] = ev; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); + } + } + }); - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * Created by Alex on 3/20/2015. + * Touch events input + * @constructor + * @extends Input */ + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; - var EdgeBase = _interopRequire(__webpack_require__(83)); + Input.apply(this, arguments); + } - var StraightEdge = (function (EdgeBase) { - function StraightEdge(options, body, labelModule) { - _classCallCheck(this, StraightEdge); + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); - } + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; + } - _inherits(StraightEdge, EdgeBase); + if (!this.started) { + return; + } - _prototypeProperties(StraightEdge, null, { - cleanup: { - value: function cleanup() { - return false; - }, - writable: true, - configurable: true - }, - _line: { - /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return undefined; - }, - writable: true, - configurable: true - }, - getPoint: { + var touches = normalizeSingleTouches.call(this, ev, type); - /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} - * @private - */ - value: function getPoint(percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y - }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - var node1 = this.to; - var node2 = this.from; - if (nearNode.id === this.from.id) { - node1 = this.from; - node2 = this.to; + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; } - var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - var dx = node1.x - node2.x; - var dy = node1.y - node2.y; - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = nearNode.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - - var borderPos = {}; - borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; - borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); - return borderPos; - }, - writable: true, - configurable: true - }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); - }, - writable: true, - configurable: true + /** + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] + */ + function normalizeSingleTouches(ev, type) { + var all = toArray(ev.touches); + var changed = toArray(ev.changedTouches); + + if (type & (INPUT_END | INPUT_CANCEL)) { + all = uniqueArray(all.concat(changed), 'identifier', true); } - }); - return StraightEdge; - })(EdgeBase); + return [all, changed]; + } - module.exports = StraightEdge; + var TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; -/***/ }, -/* 86 */ -/***/ function(module, exports, __webpack_require__) { + var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; - "use strict"; + /** + * Multi-user touch events input + * @constructor + * @extends Input + */ + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + Input.apply(this, arguments); + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + inherit(TouchInput, Input, { + handler: function MTEhandler(ev) { + var type = TOUCH_INPUT_MAP[ev.type]; + var touches = getTouches.call(this, ev, type); + if (!touches) { + return; + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); /** - * Created by Alex on 2/23/2015. + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] */ + function getTouches(ev, type) { + var allTouches = toArray(ev.touches); + var targetIds = this.targetIds; - var BarnesHutSolver = _interopRequire(__webpack_require__(87)); - - var Repulsion = _interopRequire(__webpack_require__(88)); - - var HierarchicalRepulsion = _interopRequire(__webpack_require__(89)); + // when there is only one touch, the process can be simplified + if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) { + targetIds[allTouches[0].identifier] = true; + return [allTouches, allTouches]; + } - var SpringSolver = _interopRequire(__webpack_require__(90)); + var i, + targetTouches, + changedTouches = toArray(ev.changedTouches), + changedTargetTouches = [], + target = this.target; - var HierarchicalSpringSolver = _interopRequire(__webpack_require__(91)); + // get target touches from touches + targetTouches = allTouches.filter(function(touch) { + return hasParent(touch.target, target); + }); - var CentralGravitySolver = _interopRequire(__webpack_require__(92)); + // collect touches + if (type === INPUT_START) { + i = 0; + while (i < targetTouches.length) { + targetIds[targetTouches[i].identifier] = true; + i++; + } + } - var util = __webpack_require__(1); + // filter changed touches to only contain touches that exist in the collected target ids + i = 0; + while (i < changedTouches.length) { + if (targetIds[changedTouches[i].identifier]) { + changedTargetTouches.push(changedTouches[i]); + } + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; + } + i++; + } - var PhysicsEngine = (function () { - function PhysicsEngine(body) { - var _this = this; - _classCallCheck(this, PhysicsEngine); + if (!changedTargetTouches.length) { + return; + } - this.body = body; - this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; + } - this.physicsEnabled = true; - this.simulationInterval = 1000 / 60; - this.requiresTimeout = true; - this.previousStates = {}; - this.freezeCache = {}; - this.renderTimer == undefined; + /** + * Combined touch and mouse input + * + * Touch has a higher priority then mouse, and while touching no mouse events are allowed. + * This because touch devices also emit mouse events while doing a touch. + * + * @constructor + * @extends Input + */ + function TouchMouseInput() { + Input.apply(this, arguments); - this.stabilized = false; - this.stabilizationIterations = 0; - this.ready = false; // will be set to true if the stabilize + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } - // default options - this.options = {}; - this.defaultOptions = { - barnesHut: { - thetaInverted: 1 / 0.5, // inverted to save time during calculation - gravitationalConstant: -2000, - centralGravity: 0.3, - springLength: 95, - springConstant: 0.04, - damping: 0.09 - }, - repulsion: { - centralGravity: 0, - springLength: 200, - springConstant: 0.05, - nodeDistance: 100, - damping: 0.09 - }, - hierarchicalRepulsion: { - centralGravity: 0, - springLength: 100, - springConstant: 0.01, - nodeDistance: 120, - damping: 0.09 - }, - solver: "BarnesHut", - timestep: 0.5, - maxVelocity: 50, - minVelocity: 0.1, // px/s - stabilization: { - enabled: true, - iterations: 1000, // maximum number of iteration to stabilize - updateInterval: 100, - onlyDynamicEdges: false, - zoomExtent: true - } - }; - util.extend(this.options, this.defaultOptions); + inherit(TouchMouseInput, Input, { + /** + * handle mouse and touch events + * @param {Hammer} manager + * @param {String} inputEvent + * @param {Object} inputData + */ + handler: function TMEhandler(manager, inputEvent, inputData) { + var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), + isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); - this.body.emitter.on("initPhysics", function () { - _this.initPhysics(); - }); - this.body.emitter.on("resetPhysics", function () { - _this.stopSimulation();_this.ready = false; - }); - this.body.emitter.on("disablePhysics", function () { - _this.physicsEnabled = false;_this.stopSimulation(); - }); - this.body.emitter.on("restorePhysics", function () { - _this.setOptions(_this.options); - if (_this.ready === true) { - _this.stabilized = false; - _this.runSimulation(); - } - }); - this.body.emitter.on("startSimulation", function () { - if (_this.ready === true) { - _this.stabilized = false; - _this.runSimulation(); - } - }); - this.body.emitter.on("stopSimulation", function () { - _this.stopSimulation(); - }); - } + // when we're in a touch event, so block all upcoming mouse events + // most mobile browser also emit mouseevents, right after touchstart + if (isTouch) { + this.mouse.allow = false; + } else if (isMouse && !this.mouse.allow) { + return; + } - _prototypeProperties(PhysicsEngine, null, { - setOptions: { - value: function setOptions(options) { - if (options === false) { - this.physicsEnabled = false; - this.stopSimulation(); - } else { - this.physicsEnabled = true; - if (options !== undefined) { - util.selectiveNotDeepExtend(["stabilization"], this.options, options); - util.mergeOptions(this.options, options, "stabilization"); - } - this.init(); + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; } - }, - writable: true, - configurable: true + + this.callback(manager, inputEvent, inputData); }, - init: { - value: function init() { - var options; - if (this.options.solver == "repulsion") { - options = this.options.repulsion; - this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - } else if (this.options.solver == "hierarchicalRepulsion") { - options = this.options.hierarchicalRepulsion; - this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); - this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); - } else { - // barnesHut - options = this.options.barnesHut; - this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + + /** + * remove the event listeners + */ + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); + } + }); + + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; + + // magical touchAction value + var TOUCH_ACTION_COMPUTE = 'compute'; + var TOUCH_ACTION_AUTO = 'auto'; + var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented + var TOUCH_ACTION_NONE = 'none'; + var TOUCH_ACTION_PAN_X = 'pan-x'; + var TOUCH_ACTION_PAN_Y = 'pan-y'; + + /** + * Touch Action + * sets the touchAction property or uses the js alternative + * @param {Manager} manager + * @param {String} value + * @constructor + */ + function TouchAction(manager, value) { + this.manager = manager; + this.set(value); + } + + TouchAction.prototype = { + /** + * set the touchAction value on the element or enable the polyfill + * @param {String} value + */ + set: function(value) { + // find out the touch-action by the event handlers + if (value == TOUCH_ACTION_COMPUTE) { + value = this.compute(); } - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - this.modelOptions = options; - }, - writable: true, - configurable: true - }, - initPhysics: { - value: function initPhysics() { - if (this.physicsEnabled === true) { - this.stabilized = false; - if (this.options.stabilization.enabled === true) { - this.stabilize(); - } else { - this.ready = true; - this.body.emitter.emit("zoomExtent", { duration: 0 }, true); - this.runSimulation(); - } - } else { - this.ready = true; - this.body.emitter.emit("_redraw"); + if (NATIVE_TOUCH_ACTION) { + this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; } - }, - writable: true, - configurable: true + this.actions = value.toLowerCase().trim(); }, - stopSimulation: { - value: function stopSimulation() { - this.stabilized = true; - if (this.viewFunction !== undefined) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.viewFunction = undefined; - this.body.emitter.emit("_stopRendering"); - } - }, - writable: true, - configurable: true + + /** + * just re-set the touchAction value + */ + update: function() { + this.set(this.manager.options.touchAction); }, - runSimulation: { - value: function runSimulation() { - if (this.physicsEnabled === true) { - if (this.viewFunction === undefined) { - this.viewFunction = this.simulationStep.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); - } - } else { - this.body.emitter.emit("_redraw"); - } - }, - writable: true, - configurable: true + + /** + * compute the value for the touchAction property based on the recognizer's settings + * @returns {String} value + */ + compute: function() { + var actions = []; + each(this.manager.recognizers, function(recognizer) { + if (boolOrFn(recognizer.options.enable, [recognizer])) { + actions = actions.concat(recognizer.getTouchAction()); + } + }); + return cleanTouchActions(actions.join(' ')); }, - simulationStep: { - value: function simulationStep() { - // check if the physics have settled - var startTime = Date.now(); - this.physicsTick(); - var physicsTime = Date.now() - startTime; - // run double speed if it is a little graph - if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { - this.physicsTick(); + /** + * this method is called on each input cycle and provides the preventing of the browser behavior + * @param {Object} input + */ + preventDefaults: function(input) { + // not needed with native support for the touchAction property + if (NATIVE_TOUCH_ACTION) { + return; + } - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - this.runDoubleSpeed = true; + var srcEvent = input.srcEvent; + var direction = input.offsetDirection; + + // if the touch action did prevented once this session + if (this.manager.session.prevented) { + srcEvent.preventDefault(); + return; } - if (this.stabilized === true) { - if (this.stabilizationIterations > 1) { - // trigger the "stabilized" event. - // The event is triggered on the next tick, to prevent the case that - // it is fired while initializing the Network, in which case you would not - // be able to catch it - var me = this; - var params = { - iterations: this.stabilizationIterations - }; - this.stabilizationIterations = 0; - this.startedStabilization = false; - setTimeout(function () { - me.body.emitter.emit("stabilized", params); - }, 0); - } else { - this.stabilizationIterations = 0; - } - this.stopSimulation(); + var actions = this.actions; + var hasNone = inStr(actions, TOUCH_ACTION_NONE); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + + if (hasNone || + (hasPanY && direction & DIRECTION_HORIZONTAL) || + (hasPanX && direction & DIRECTION_VERTICAL)) { + return this.preventSrc(srcEvent); } - }, - writable: true, - configurable: true }, - physicsTick: { - /** - * A single simulation step (or "tick") in the physics simulation - * - * @private - */ - value: function physicsTick() { - if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); + /** + * call preventDefault to prevent the browser's default behavior (scrolling in most cases) + * @param {Object} srcEvent + */ + preventSrc: function(srcEvent) { + this.manager.session.prevented = true; + srcEvent.preventDefault(); + } + }; + + /** + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} + */ + function cleanTouchActions(actions) { + // none + if (inStr(actions, TOUCH_ACTION_NONE)) { + return TOUCH_ACTION_NONE; + } + + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; + } + + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; + } + + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; + } + + return TOUCH_ACTION_AUTO; + } + + /** + * Recognizer flow explained; * + * All recognizers have the initial state of POSSIBLE when a input session starts. + * The definition of a input session is from the first input until the last input, with all it's movement in it. * + * Example session for mouse-input: mousedown -> mousemove -> mouseup + * + * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed + * which determines with state it should be. + * + * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to + * POSSIBLE to give it another change on the next cycle. + * + * Possible + * | + * +-----+---------------+ + * | | + * +-----+-----+ | + * | | | + * Failed Cancelled | + * +-------+------+ + * | | + * Recognized Began + * | + * Changed + * | + * Ended/Recognized + */ + var STATE_POSSIBLE = 1; + var STATE_BEGAN = 2; + var STATE_CHANGED = 4; + var STATE_ENDED = 8; + var STATE_RECOGNIZED = STATE_ENDED; + var STATE_CANCELLED = 16; + var STATE_FAILED = 32; + + /** + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options + */ + function Recognizer(options) { + this.id = uniqueId(); + + this.manager = null; + this.options = merge(options || {}, this.defaults); - // determine if the network has stabilzied - if (this.stabilized === true) { - this.revert(); - } else { - // this is here to ensure that there is no start event when the network is already stable. - if (this.startedStabilization == false) { - this.body.emitter.emit("startStabilizing"); - this.startedStabilization = true; - } - } + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - this.stabilizationIterations++; - } - }, - writable: true, - configurable: true - }, - updatePhysicsIndices: { + this.state = STATE_POSSIBLE; - /** - * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also - * handled in the calculateForces function. We then use a quadratic curve with the center node as control. - * This function joins the datanodes and invisible (called support) nodes into one object. - * We do this so we do not contaminate this.body.nodes with the support nodes. - * - * @private - */ - value: function updatePhysicsIndices() { - this.physicsBody.forces = {}; - this.physicsBody.physicsNodeIndices = []; - this.physicsBody.physicsEdgeIndices = []; - var nodes = this.body.nodes; - var edges = this.body.edges; + this.simultaneous = {}; + this.requireFail = []; + } - // get node indices for physics - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].options.physics === true) { - this.physicsBody.physicsNodeIndices.push(nodeId); - } - } - } + Recognizer.prototype = { + /** + * @virtual + * @type {Object} + */ + defaults: {}, - // get edge indices for physics - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].options.physics === true) { - this.physicsBody.physicsEdgeIndices.push(edgeId); - } - } - } + /** + * set options + * @param {Object} options + * @return {Recognizer} + */ + set: function(options) { + extend(this.options, options); - // get the velocity and the forces vector - for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { - var nodeId = this.physicsBody.physicsNodeIndices[i]; - this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; + // also update the touchAction, in case something changed about the directions/enabled state + this.manager && this.manager.touchAction.update(); + return this; + }, - // forces can be reset because they are recalculated. Velocities have to persist. - if (this.physicsBody.velocities[nodeId] === undefined) { - this.physicsBody.velocities[nodeId] = { x: 0, y: 0 }; - } + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; } - // clean deleted nodes from the velocity vector - for (var nodeId in this.physicsBody.velocities) { - if (nodes[nodeId] === undefined) { - delete this.physicsBody.velocities[nodeId]; - } + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); } - }, - writable: true, - configurable: true + return this; }, - revert: { - value: function revert() { - var nodeIds = Object.keys(this.previousStates); - var nodes = this.body.nodes; - var velocities = this.physicsBody.velocities; - for (var i = 0; i < nodeIds.length; i++) { - var nodeId = nodeIds[i]; - if (nodes[nodeId] !== undefined) { - velocities[nodeId].x = this.previousStates[nodeId].vx; - velocities[nodeId].y = this.previousStates[nodeId].vy; - nodes[nodeId].x = this.previousStates[nodeId].x; - nodes[nodeId].y = this.previousStates[nodeId].y; - } else { - delete this.previousStates[nodeId]; - } + /** + * drop the simultaneous link. it doesnt remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRecognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { + return this; } - }, - writable: true, - configurable: true + + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; }, - moveNodes: { - value: function moveNodes() { - var nodesPresent = false; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; - var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - var nodeVelocity = this._performStep(nodeId, maxVelocity); - // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized - stabilized = nodeVelocity < vminCorrected && stabilized === true; - nodesPresent = true; + /** + * recognizer can only run when an other is failing + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + requireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { + return this; } - - if (nodesPresent == true) { - if (vminCorrected > 0.5 * this.options.maxVelocity) { - return false; - } else { - return stabilized; - } + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); } - return true; - }, - writable: true, - configurable: true + return this; }, - _performStep: { - value: function _performStep(nodeId, maxVelocity) { - var node = this.body.nodes[nodeId]; - var timestep = this.options.timestep; - var forces = this.physicsBody.forces; - var velocities = this.physicsBody.velocities; - - // store the state so we can revert - this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; - if (node.options.fixed.x === false) { - var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force - var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration - velocities[nodeId].x += ax * timestep; // velocity - velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; - node.x += velocities[nodeId].x * timestep; // position - } else { - forces[nodeId].x = 0; - velocities[nodeId].x = 0; + /** + * drop the requireFailure link. it does not remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRequireFailure: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) { + return this; } - if (node.options.fixed.y === false) { - var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force - var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration - velocities[nodeId].y += ay * timestep; // velocity - velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; - node.y += velocities[nodeId].y * timestep; // position - } else { - forces[nodeId].y = 0; - velocities[nodeId].y = 0; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); } - - var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); - return totalVelocity; - }, - writable: true, - configurable: true - }, - calculateForces: { - value: function calculateForces() { - this.gravitySolver.solve(); - this.nodesSolver.solve(); - this.edgesSolver.solve(); - }, - writable: true, - configurable: true + return this; }, - _freezeNodes: { - + /** + * has require failures boolean + * @returns {boolean} + */ + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, + /** + * You should use `tryEmit` instead of `emit` directly to check + * that all the needed recognizers has failed before emitting. + * @param {Object} input + */ + emit: function(input) { + var self = this; + var state = this.state; + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); + } + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); + } + emit(); // simple 'eventName' events + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); + } + }, - /** - * 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 - */ - value: function _freezeNodes() { - var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].x && nodes[id].y) { - this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; - nodes[id].options.fixed.x = true; - nodes[id].options.fixed.y = true; - } - } + /** + * Check that all the require failure recognizers has failed, + * if true, it emits a gesture event, + * otherwise, setup the state to FAILED. + * @param {Object} input + */ + tryEmit: function(input) { + if (this.canEmit()) { + return this.emit(input); } - }, - writable: true, - configurable: true + // it's failing anyway + this.state = STATE_FAILED; }, - _restoreFrozenNodes: { - /** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * - * @private - */ - value: function _restoreFrozenNodes() { - var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (this.freezeCache[id] !== undefined) { - nodes[id].options.fixed.x = this.freezeCache[id].x; - nodes[id].options.fixed.y = this.freezeCache[id].y; + /** + * can we emit? + * @returns {boolean} + */ + canEmit: function() { + var i = 0; + while (i < this.requireFail.length) { + if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) { + return false; } - } + i++; } - this.freezeCache = {}; - }, - writable: true, - configurable: true + return true; }, - stabilize: { - /** - * Find a stable position for all nodes - * @private - */ - value: function stabilize() { - if (this.options.stabilization.onlyDynamicEdges == true) { - this._freezeNodes(); + /** + * update the recognizer + * @param {Object} inputData + */ + recognize: function(inputData) { + // make a new copy of the inputData + // so we can change the inputData without messing up the other recognizers + var inputDataClone = extend({}, inputData); + + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; } - this.stabilizationSteps = 0; - setTimeout(this._stabilizationBatch.bind(this), 0); - }, - writable: true, - configurable: true - }, - _stabilizationBatch: { - value: function _stabilizationBatch() { - var count = 0; - while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { - this.physicsTick(); - this.stabilizationSteps++; - count++; + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; } - if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) { - this.body.emitter.emit("stabilizationProgress", { steps: this.stabilizationSteps, total: this.options.stabilization.iterations }); - setTimeout(this._stabilizationBatch.bind(this), 0); - } else { - this._finalizeStabilization(); + this.state = this.process(inputDataClone); + + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); } - }, - writable: true, - configurable: true }, - _finalizeStabilization: { - value: function _finalizeStabilization() { - if (this.options.stabilization.zoomExtent == true) { - this.body.emitter.emit("zoomExtent", { duration: 0 }); - } - if (this.options.stabilization.onlyDynamicEdges == true) { - this._restoreFrozenNodes(); - } + /** + * return the state of the recognizer + * the actual recognizing happens in this method + * @virtual + * @param {Object} inputData + * @returns {Const} STATE + */ + process: function(inputData) { }, // jshint ignore:line - this.body.emitter.emit("stabilizationIterationsDone"); - this.body.emitter.emit("_requestRedraw"); - this.ready = true; - }, - writable: true, - configurable: true + /** + * return the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, + + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } + }; + + /** + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state + */ + function stateStr(state) { + if (state & STATE_CANCELLED) { + return 'cancel'; + } else if (state & STATE_ENDED) { + return 'end'; + } else if (state & STATE_CHANGED) { + return 'move'; + } else if (state & STATE_BEGAN) { + return 'start'; } - }); + return ''; + } - return PhysicsEngine; - })(); + /** + * direction cons to string + * @param {Const} direction + * @returns {String} + */ + function directionStr(direction) { + if (direction == DIRECTION_DOWN) { + return 'down'; + } else if (direction == DIRECTION_UP) { + return 'up'; + } else if (direction == DIRECTION_LEFT) { + return 'left'; + } else if (direction == DIRECTION_RIGHT) { + return 'right'; + } + return ''; + } - module.exports = PhysicsEngine; + /** + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} + */ + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); + } + return otherRecognizer; + } -/***/ }, -/* 87 */ -/***/ function(module, exports, __webpack_require__) { + /** + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer + */ + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } - "use strict"; + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, + + /** + * Used to check if it the recognizer receives valid input, like input.distance > 10. + * @memberof AttrRecognizer + * @param {Object} input + * @returns {Boolean} recognized + */ + attrTest: function(input) { + var optionPointers = this.options.pointers; + return optionPointers === 0 || input.pointers.length === optionPointers; + }, + + /** + * Process the input and return the state for the recognizer + * @memberof AttrRecognizer + * @param {Object} input + * @returns {*} State + */ + process: function(input) { + var state = this.state; + var eventType = input.eventType; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // on cancel input and we've recognized before, return STATE_CANCELLED + if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { + return state | STATE_CANCELLED; + } else if (isRecognized || isValid) { + if (eventType & INPUT_END) { + return state | STATE_ENDED; + } else if (!(state & STATE_BEGAN)) { + return STATE_BEGAN; + } + return state | STATE_CHANGED; + } + return STATE_FAILED; + } + }); /** - * Created by Alex on 2/23/2015. + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - var BarnesHutSolver = (function () { - function BarnesHutSolver(body, physicsBody, options) { - _classCallCheck(this, BarnesHutSolver); - - this.body = body; - this.physicsBody = physicsBody; - this.barnesHutTree; - this.setOptions(options); - } + this.pX = null; + this.pY = null; + } - _prototypeProperties(BarnesHutSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL }, - solve: { - - - /** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. - * - * @private - */ - value: function solve() { - if (this.options.gravitationalConstant != 0) { - var node; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var nodeCount = nodeIndices.length; - // create the tree - var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + getTouchAction: function() { + var direction = this.options.direction; + var actions = []; + if (direction & DIRECTION_HORIZONTAL) { + actions.push(TOUCH_ACTION_PAN_Y); + } + if (direction & DIRECTION_VERTICAL) { + actions.push(TOUCH_ACTION_PAN_X); + } + return actions; + }, - // for debugging - this.barnesHutTree = barnesHutTree; + directionTest: function(input) { + var options = this.options; + var hasMoved = true; + var distance = input.distance; + var direction = input.direction; + var x = input.deltaX; + var y = input.deltaY; - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHutSolver condition - this._getForceContribution(barnesHutTree.root.children.NW, node); - this._getForceContribution(barnesHutTree.root.children.NE, node); - this._getForceContribution(barnesHutTree.root.children.SW, node); - this._getForceContribution(barnesHutTree.root.children.SE, node); + // lock to axis? + if (!(direction & options.direction)) { + if (options.direction & DIRECTION_HORIZONTAL) { + direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT; + hasMoved = x != this.pX; + distance = Math.abs(input.deltaX); + } else { + direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN; + hasMoved = y != this.pY; + distance = Math.abs(input.deltaY); } - } } - }, - writable: true, - configurable: true + input.direction = direction; + return hasMoved && distance > options.threshold && direction & options.direction; }, - _getForceContribution: { + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - /** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. - * - * @param parentBranch - * @param node - * @private - */ - value: function _getForceContribution(parentBranch, node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx, dy, distance; + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - // BarnesHutSolver condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.options.thetaInverted) { - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.1 * Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; + this._super.emit.call(this, input); + } + }); - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; - } else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { - this._getForceContribution(parentBranch.children.NW, node); - this._getForceContribution(parentBranch.children.NE, node); - this._getForceContribution(parentBranch.children.SW, node); - this._getForceContribution(parentBranch.children.SE, node); - } else { - // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { - // if it is not self - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.5 * Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; + /** + * Pinch + * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). + * @constructor + * @extends AttrRecognizer + */ + function PinchRecognizer() { + AttrRecognizer.apply(this, arguments); + } - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; - } - } - } - } - }, - writable: true, - configurable: true + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 }, - _formBarnesHutTree: { - - /** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. - * - * @param nodes - * @param nodeIndices - * @private - */ - value: function _formBarnesHutTree(nodes, nodeIndices) { - var node; - var nodeCount = nodeIndices.length; + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX = -Number.MAX_VALUE, - maxY = -Number.MAX_VALUE; + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { - minX = x; - } - if (x > maxX) { - maxX = x; - } - if (y < minY) { - minY = y; - } - if (y > maxY) { - maxY = y; - } - } + emit: function(input) { + this._super.emit.call(this, input); + if (input.scale !== 1) { + var inOut = input.scale < 1 ? 'in' : 'out'; + this.manager.emit(this.options.event + inOut, input); } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) { - minY -= 0.5 * sizeDiff; - maxY += 0.5 * sizeDiff; - } // xSize > ySize - else { - minX += 0.5 * sizeDiff; - maxX -= 0.5 * sizeDiff; - } // xSize < ySize - - - var minimumTreeSize = 0.00001; - var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), - centerY = 0.5 * (minY + maxY); + } + }); - // construct the barnesHutTree - var barnesHutTree = { - root: { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { - minX: centerX - halfRootSize, maxX: centerX + halfRootSize, - minY: centerY - halfRootSize, maxY: centerY + halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data: null }, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); + /** + * Press + * Recognized when the pointer is down for x ms without any movement. + * @constructor + * @extends Recognizer + */ + function PressRecognizer() { + Recognizer.apply(this, arguments); - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root, node); - } - } + this._timer = null; + this._input = null; + } - // make global - return barnesHutTree; - }, - writable: true, - configurable: true + inherit(PressRecognizer, Recognizer, { + /** + * @namespace + * @memberof PressRecognizer + */ + defaults: { + event: 'press', + pointers: 1, + time: 500, // minimal time of the pointer to be pressed + threshold: 5 // a minimal movement is ok, but keep it low }, - _updateBranchMass: { - - /** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private - */ - value: function _updateBranchMass(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1 / totalMass; + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; + process: function(input) { + var options = this.options; + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTime = input.deltaTime > options.time; - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; + this._input = input; - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); - parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; - }, - writable: true, - configurable: true + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) { + this.reset(); + } else if (input.eventType & INPUT_START) { + this.reset(); + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.time, this); + } else if (input.eventType & INPUT_END) { + return STATE_RECOGNIZED; + } + return STATE_FAILED; }, - _placeInTree: { + reset: function() { + clearTimeout(this._timer); + }, - /** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate - * @private - */ - value: function _placeInTree(parentBranch, node, skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch, node); + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; } - if (parentBranch.children.NW.range.maxX > node.x) { - // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { - // in NW - this._placeInRegion(parentBranch, node, "NW"); - } else { - // in SW - this._placeInRegion(parentBranch, node, "SW"); - } + if (input && (input.eventType & INPUT_END)) { + this.manager.emit(this.options.event + 'up', input); } else { - // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { - // in NE - this._placeInRegion(parentBranch, node, "NE"); - } else { - // in SE - this._placeInRegion(parentBranch, node, "SE"); - } + this._input.timeStamp = now(); + this.manager.emit(this.options.event, this._input); } - }, - writable: true, - configurable: true - }, - _placeInRegion: { + } + }); + /** + * Rotate + * Recognized when two or more pointer are moving in a circular motion. + * @constructor + * @extends AttrRecognizer + */ + function RotateRecognizer() { + AttrRecognizer.apply(this, arguments); + } - /** - * actually place the node in a region (or branch) - * - * @param parentBranch - * @param node - * @param region - * @private - */ - value: function _placeInRegion(parentBranch, node, region) { - switch (parentBranch.children[region].childrenCount) { - case 0: - // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region], node); - break; - case 1: - // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && parentBranch.children[region].children.data.y == node.y) { - node.x += Math.random(); - node.y += Math.random(); - } else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region], node); - } - break; - case 4: - // place in branch - this._placeInTree(parentBranch.children[region], node); - break; - } - }, - writable: true, - configurable: true + inherit(RotateRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof RotateRecognizer + */ + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, + + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; }, - _splitBranch: { + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + } + }); - /** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. - * - * @param parentBranch - * @private - */ - value: function _splitBranch(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount == 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; - parentBranch.centerOfMass.x = 0; - parentBranch.centerOfMass.y = 0; - } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch, "NW"); - this._insertRegion(parentBranch, "NE"); - this._insertRegion(parentBranch, "SW"); - this._insertRegion(parentBranch, "SE"); + /** + * Swipe + * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function SwipeRecognizer() { + AttrRecognizer.apply(this, arguments); + } - if (containedNode != null) { - this._placeInTree(parentBranch, containedNode); - } - }, - writable: true, - configurable: true + inherit(SwipeRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof SwipeRecognizer + */ + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 }, - _insertRegion: { + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - /** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch - * - * @param parentBranch - * @param region - * @param parentRange - * @private - */ - value: function _insertRegion(parentBranch, region) { - var minX, maxX, minY, maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - } + attrTest: function(input) { + var direction = this.options.direction; + var velocity; + if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { + velocity = input.velocity; + } else if (direction & DIRECTION_HORIZONTAL) { + velocity = input.velocityX; + } else if (direction & DIRECTION_VERTICAL) { + velocity = input.velocityY; + } - parentBranch.children[region] = { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: { data: null }, - maxWidth: 0, - level: parentBranch.level + 1, - childrenCount: 0 - }; - }, - writable: true, - configurable: true + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; }, - _debug: { + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } + this.manager.emit(this.options.event, input); + } + }); + /** + * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur + * between the given interval and position. The delay option can be used to recognize multi-taps without firing + * a single tap. + * + * The eventData from the emitted event contains the property `tapCount`, which contains the amount of + * multi-taps being recognized. + * @constructor + * @extends Recognizer + */ + function TapRecognizer() { + Recognizer.apply(this, arguments); - //--------------------------- DEBUGGING BELOW ---------------------------// + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; + this._timer = null; + this._input = null; + this.count = 0; + } - /** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color - * @private - */ - value: function _debug(ctx, color) { - if (this.barnesHutTree !== undefined) { - ctx.lineWidth = 1; + inherit(TapRecognizer, Recognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'tap', + pointers: 1, + taps: 1, + interval: 300, // max time between the multi-tap taps + time: 250, // max time of the pointer to be down (like finger on the screen) + threshold: 2, // a minimal movement is ok, but keep it low + posThreshold: 10 // a multi-tap can be a bit off the initial position + }, - this._drawBranch(this.barnesHutTree.root, ctx, color); - } - }, - writable: true, - configurable: true + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; }, - _drawBranch: { + process: function(input) { + var options = this.options; - /** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color - * @private - */ - value: function _drawBranch(branch, ctx, color) { - if (color === undefined) { - color = "#FF0000"; - } + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - if (branch.childrenCount == 4) { - this._drawBranch(branch.children.NW, ctx); - this._drawBranch(branch.children.NE, ctx); - this._drawBranch(branch.children.SE, ctx); - this._drawBranch(branch.children.SW, ctx); + this.reset(); + + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.minY); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.maxY); - ctx.stroke(); + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (validMovement && validTouchTime && validPointers) { + if (input.eventType != INPUT_END) { + return this.failTimeout(); + } - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.maxY); - ctx.stroke(); + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.minY); - ctx.stroke(); + this.pTime = input.timeStamp; + this.pCenter = input.center; - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ - }, - writable: true, - configurable: true - } - }); + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; + } - return BarnesHutSolver; - })(); + this._input = input; - module.exports = BarnesHutSolver; + // if tap count matches we have recognized it, + // else it has began recognizing... + var tapCount = this.count % options.taps; + if (tapCount === 0) { + // no failing requirements, immediately trigger the tap event + // or wait as long as the multitap interval to trigger + if (!this.hasRequireFailures()) { + return STATE_RECOGNIZED; + } else { + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.interval, this); + return STATE_BEGAN; + } + } + } + return STATE_FAILED; + }, -/***/ }, -/* 88 */ -/***/ function(module, exports, __webpack_require__) { + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, - "use strict"; + reset: function() { + clearTimeout(this._timer); + }, - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); + } + } + }); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Hammer(element, options) { + options = options || {}; + options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); + return new Manager(element, options); + } /** - * Created by Alex on 2/23/2015. + * @const {string} */ + Hammer.VERSION = '2.0.4'; - var RepulsionSolver = (function () { - function RepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, RepulsionSolver); + /** + * default settings + * @namespace + */ + Hammer.defaults = { + /** + * set if DOM events are being triggered. + * But this is slower and unused by simple implementations, so disabled by default. + * @type {Boolean} + * @default false + */ + domEvents: false, - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + /** + * The value for the touchAction property/fallback. + * When set to `compute` it will magically set the correct value based on the added recognizers. + * @type {String} + * @default compute + */ + touchAction: TOUCH_ACTION_COMPUTE, - _prototypeProperties(RepulsionSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true - }, - solve: { - /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * - * @private - */ - value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2; + /** + * @type {Boolean} + * @default true + */ + enable: true, - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + /** + * EXPERIMENTAL FEATURE -- can be removed/changed + * Change the parent input target element. + * If Null, then it is being set the to main element. + * @type {Null|EventTarget} + * @default null + */ + inputTarget: null, - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, - // approximation constants - var a = -2 / 3 / nodeDistance; - var b = 4 / 3; + /** + * Default recognizer setup when calling `Hammer()` + * When creating a new Manager these will be skipped. + * @type {Array} + */ + preset: [ + // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] + [RotateRecognizer, { enable: false }], + [PinchRecognizer, { enable: false }, ['rotate']], + [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], + [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], + [TapRecognizer], + [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], + [PressRecognizer] + ], - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (var i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (var j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; + /** + * Some CSS properties can be used to improve the working of Hammer. + * Add them to this method and they will be set when creating a new Manager. + * @namespace + */ + cssProps: { + /** + * Disables text selection to improve the dragging gesture. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userSelect: 'none', - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', - // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. - if (distance == 0) { - distance = 0.1 * Math.random(); - dx = distance; - } + /** + * Disables the default callout shown when you touch and hold a touch target. + * On iOS, when you touch and hold a touch target such as a link, Safari displays + * a callout containing information about the link. This property allows you to disable that callout. + * @type {String} + * @default 'none' + */ + touchCallout: 'none', - if (distance < 2 * nodeDistance) { - if (distance < 0.5 * nodeDistance) { - repulsingForce = 1; - } else { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) - } - repulsingForce = repulsingForce / distance; + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - fx = dx * repulsingForce; - fy = dy * repulsingForce; + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; - } - } - } - }, - writable: true, - configurable: true + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in iOS. This property obeys the alpha value, if specified. + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' } - }); + }; - return RepulsionSolver; - })(); + var STOP = 1; + var FORCED_STOP = 2; - module.exports = RepulsionSolver; + /** + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor + */ + function Manager(element, options) { + options = options || {}; -/***/ }, -/* 89 */ -/***/ function(module, exports, __webpack_require__) { + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - "use strict"; + this.handlers = {}; + this.session = {}; + this.recognizers = []; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + toggleCssProps(this, true); - /** - * Created by Alex on 2/23/2015. - */ + each(options.recognizers, function(item) { + var recognizer = this.add(new (item[0])(item[1])); + item[2] && recognizer.recognizeWith(item[2]); + item[3] && recognizer.requireFailure(item[3]); + }, this); + } - var HierarchicalRepulsionSolver = (function () { - function HierarchicalRepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalRepulsionSolver); + Manager.prototype = { + /** + * set options + * @param {Object} options + * @returns {Manager} + */ + set: function(options) { + extend(this.options, options); - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + // Options that need a little more setup + if (options.touchAction) { + this.touchAction.update(); + } + if (options.inputTarget) { + // Clean up existing event listeners and reinitialize + this.input.destroy(); + this.input.target = options.inputTarget; + this.input.init(); + } + return this; + }, - _prototypeProperties(HierarchicalRepulsionSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true + /** + * stop recognizing for this session. + * This session will be discarded, when a new [input]start event is fired. + * When forced, the recognizer cycle is stopped immediately. + * @param {Boolean} [force] + */ + stop: function(force) { + this.session.stopped = force ? FORCED_STOP : STOP; }, - solve: { - /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * - * @private - */ - value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; + /** + * run the recognizers! + * called by the inputHandler function on every movement of the pointers (touches) + * it walks through all the recognizers and tries to detect the gesture that is being made + * @param {Object} inputData + */ + recognize: function(inputData) { + var session = this.session; + if (session.stopped) { + return; + } - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; + var recognizer; + var recognizers = this.recognizers; - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; + // this holds the recognizer that is being recognized. + // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED + // if no recognizer is detecting a thing, it is set to `null` + var curRecognizer = session.curRecognizer; - // nodes only affect nodes on their level - if (node1.level == node2.level) { - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + // reset when the last recognizer is recognized + // or when we're in a new session + if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { + curRecognizer = session.curRecognizer = null; + } - var steepness = 0.05; - if (distance < nodeDistance) { - repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); - } else { - repulsingForce = 0; - } - // normalize force with - if (distance == 0) { - distance = 0.01; - } else { - repulsingForce = repulsingForce / distance; - } - fx = dx * repulsingForce; - fy = dy * repulsingForce; + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; + // find out if we are allowed try to recognize the input for this one. + // 1. allow if the session is NOT forced stopped (see the .stop() method) + // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one + // that is being recognized. + // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. + // this can be setup with the `recognizeWith()` method on the recognizer. + if (session.stopped !== FORCED_STOP && ( // 1 + !curRecognizer || recognizer == curRecognizer || // 2 + recognizer.canRecognizeWith(curRecognizer))) { // 3 + recognizer.recognize(inputData); + } else { + recognizer.reset(); } - } - } - }, - writable: true, - configurable: true - } - }); - return HierarchicalRepulsionSolver; - })(); + // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the + // current active recognizer. but only if we don't already have an active recognizer + if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) { + curRecognizer = session.curRecognizer = recognizer; + } + i++; + } + }, - module.exports = HierarchicalRepulsionSolver; + /** + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} + */ + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; + } -/***/ }, -/* 90 */ -/***/ function(module, exports, __webpack_require__) { + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } + } + return null; + }, - "use strict"; + /** + * add a recognizer to the manager + * existing recognizers with the same event name will be removed + * @param {Recognizer} recognizer + * @returns {Recognizer|Manager} + */ + add: function(recognizer) { + if (invokeArrayArg(recognizer, 'add', this)) { + return this; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + this.recognizers.push(recognizer); + recognizer.manager = this; - /** - * Created by Alex on 2/23/2015. - */ + this.touchAction.update(); + return recognizer; + }, - var SpringSolver = (function () { - function SpringSolver(body, physicsBody, options) { - _classCallCheck(this, SpringSolver); + /** + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} + */ + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - _prototypeProperties(SpringSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true + this.touchAction.update(); + return this; }, - solve: { - - /** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * - * @private - */ - value: function solve() { - var edgeLength, edge; - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var edges = this.body.edges; - - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - // only calculate forces if nodes are in the same sector - if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { - if (edge.edgeType.via !== undefined) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - var node1 = edge.to; - var node2 = edge.edgeType.via; - var node3 = edge.from; + /** + * bind event + * @param {String} events + * @param {Function} handler + * @returns {EventEmitter} this + */ + on: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + handlers[event] = handlers[event] || []; + handlers[event].push(handler); + }); + return this; + }, - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } else { - // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use - // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. - edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; - this._calculateSpringForce(edge.from, edge.to, edgeLength); - } + /** + * unbind event, leave emit blank to remove all handlers + * @param {String} events + * @param {Function} [handler] + * @returns {EventEmitter} this + */ + off: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + if (!handler) { + delete handlers[event]; + } else { + handlers[event].splice(inArray(handlers[event], handler), 1); } - } - } - }, - writable: true, - configurable: true + }); + return this; }, - _calculateSpringForce: { - - /** - * This is the code actually performing the calculation for the function above. - * - * @param node1 - * @param node2 - * @param edgeLength - * @private - */ - value: function _calculateSpringForce(node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; - - dx = node1.x - node2.x; - dy = node1.y - node2.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance == 0 ? 0.01 : distance; + /** + * emit event to the listeners + * @param {String} event + * @param {Object} data + */ + emit: function(event, data) { + // we also want to trigger dom events + if (this.options.domEvents) { + triggerDomEvent(event, data); + } - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; + } - fx = dx * springForce; - fy = dy * springForce; + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; - // handle the case where one node is not part of the physcis - if (this.physicsBody.forces[node1.id] !== undefined) { - this.physicsBody.forces[node1.id].x += fx; - this.physicsBody.forces[node1.id].y += fy; + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; } + }, - if (this.physicsBody.forces[node2.id] !== undefined) { - this.physicsBody.forces[node2.id].x -= fx; - this.physicsBody.forces[node2.id].y -= fy; - } - }, - writable: true, - configurable: true + /** + * destroy the manager and unbinds all events + * it doesn't unbind dom events, that is the user own responsibility + */ + destroy: function() { + this.element && toggleCssProps(this, false); + + this.handlers = {}; + this.session = {}; + this.input.destroy(); + this.element = null; } - }); + }; - return SpringSolver; - })(); + /** + * add/remove the css properties as defined in manager.options.cssProps + * @param {Manager} manager + * @param {Boolean} add + */ + function toggleCssProps(manager, add) { + var element = manager.element; + each(manager.options.cssProps, function(value, name) { + element.style[prefixed(element.style, name)] = add ? value : ''; + }); + } - module.exports = SpringSolver; + /** + * trigger dom event + * @param {String} event + * @param {Object} data + */ + function triggerDomEvent(event, data) { + var gestureEvent = document.createEvent('Event'); + gestureEvent.initEvent(event, true, true); + gestureEvent.gesture = data; + data.target.dispatchEvent(gestureEvent); + } -/***/ }, -/* 91 */ -/***/ function(module, exports, __webpack_require__) { + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, - "use strict"; + STATE_POSSIBLE: STATE_POSSIBLE, + STATE_BEGAN: STATE_BEGAN, + STATE_CHANGED: STATE_CHANGED, + STATE_ENDED: STATE_ENDED, + STATE_RECOGNIZED: STATE_RECOGNIZED, + STATE_CANCELLED: STATE_CANCELLED, + STATE_FAILED: STATE_FAILED, - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + DIRECTION_NONE: DIRECTION_NONE, + DIRECTION_LEFT: DIRECTION_LEFT, + DIRECTION_RIGHT: DIRECTION_RIGHT, + DIRECTION_UP: DIRECTION_UP, + DIRECTION_DOWN: DIRECTION_DOWN, + DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, + DIRECTION_VERTICAL: DIRECTION_VERTICAL, + DIRECTION_ALL: DIRECTION_ALL, - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + Manager: Manager, + Input: Input, + TouchAction: TouchAction, - /** - * Created by Alex on 2/25/2015. - */ + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, - var HierarchicalSpringSolver = (function () { - function HierarchicalSpringSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalSpringSolver); + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); - _prototypeProperties(HierarchicalSpringSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true - }, - solve: { + if ("function" == TYPE_FUNCTION && __webpack_require__(99)) { + !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { + return Hammer; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof module != 'undefined' && module.exports) { + module.exports = Hammer; + } else { + window[exportName] = Hammer; + } - /** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * - * @private - */ - value: function solve() { - var edgeLength, edge; - var dx, dy, fx, fy, springForce, distance; - var edges = this.body.edges; - var factor = 0.5; + })(window, document, 'Hammer'); - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; - // initialize the spring force counters - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].springFx = 0; - forces[nodeId].springFy = 0; - } +/***/ }, +/* 65 */ +/***/ function(module, exports, __webpack_require__) { + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; + (function (factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + window.propagating = factory(); + } + }(function () { + // will contain the target element where the gesture started + var _firstTarget = null; // singleton - dx = edge.from.x - edge.to.x; - dy = edge.from.y - edge.to.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance == 0 ? 0.01 : distance; + /** + * Extend an Hammer.js instance with event propagation. + * + * Features: + * - Events emitted by hammer will propagate in order from child to parent + * elements. + * - Events are extended with a function `event.stopPropagation()` to stop + * propagation to parent elements. + * + * Usage: + * var hammer = propagatingHammer(new Hammer(element)); + * + * @param {Hammer.Manager} hammer An hammer instance. + * @return {Hammer.Manager} Returns the same hammer instance with extended + * functionality + */ + return function propagating(hammer) { + if (hammer.Manager) { + // This looks like the Hammer constructor. + // Overload the constructors with our own. + var Hammer = hammer; - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; + var PropagatingHammer = function(element, options) { + return propagating(new Hammer(element, options)); + }; + Hammer.extend(PropagatingHammer, Hammer); + PropagatingHammer.Manager = function (element, options) { + return propagating(new Hammer.Manager(element, options)); + }; - fx = dx * springForce; - fy = dy * springForce; + return PropagatingHammer; + } + + // attach to DOM element + var element = hammer.element; + element.hammer = hammer; - if (edge.to.level != edge.from.level) { - forces[edge.toId].springFx -= fx; - forces[edge.toId].springFy -= fy; - forces[edge.fromId].springFx += fx; - forces[edge.fromId].springFy += fy; - } else { - forces[edge.toId].x -= factor * fx; - forces[edge.toId].y -= factor * fy; - forces[edge.fromId].x += factor * fx; - forces[edge.fromId].y += factor * fy; - } - } - } + // move the original functions that we will wrap + hammer._on = hammer.on; + hammer._off = hammer.off; + hammer._emit = hammer.emit; + hammer._destroy = hammer.destroy; - // normalize spring forces - var springForce = 1; - var springFx, springFy; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); - springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); + /** @type {Object.>} */ + hammer._handlers = {}; - forces[nodeId].x += springFx; - forces[nodeId].y += springFy; - } + // register an event to catch the start of a gesture and store the + // target in a singleton + hammer._on('hammer.input', function (event) { + if (event.isFirst) { + _firstTarget = event.target; + } + }); - // retain energy balance - var totalFx = 0; - var totalFy = 0; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - totalFx += forces[nodeId].x; - totalFy += forces[nodeId].y; - } - var correctionFx = totalFx / nodeIndices.length; - var correctionFy = totalFy / nodeIndices.length; + /** + * Register a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} handler A callback function, called as handler(event) + * @returns {Hammer.Manager} Returns the hammer instance + */ + hammer.on = function (events, handler) { + // register the handler + split(events).forEach(function (event) { + var _handlers = hammer._handlers[event]; + if (!_handlers) { + hammer._handlers[event] = _handlers = []; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].x -= correctionFx; - forces[nodeId].y -= correctionFy; + // register the static, propagated handler + hammer._on(event, propagatedHandler); } - }, - writable: true, - configurable: true - } - }); - - return HierarchicalSpringSolver; - })(); + _handlers.push(handler); + }); - module.exports = HierarchicalSpringSolver; + return hammer; + }; -/***/ }, -/* 92 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Unregister a handler for one or multiple events + * @param {String} events A space separated string with events + * @param {function} [handler] Optional. The registered handler. If not + * provided, all handlers for given events + * are removed. + * @returns {Hammer.Manager} Returns the hammer instance + */ + hammer.off = function (events, handler) { + // unregister the handler + split(events).forEach(function (event) { + var _handlers = hammer._handlers[event]; + if (_handlers) { + _handlers = handler ? _handlers.filter(function (h) { + return h !== handler; + }) : []; - "use strict"; + if (_handlers.length > 0) { + hammer._handlers[event] = _handlers; + } + else { + // remove static, propagated handler + hammer._off(event, propagatedHandler); + delete hammer._handlers[event]; + } + } + }); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + return hammer; + }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event + */ + hammer.emit = function(eventType, event) { + _firstTarget = event.target; + hammer._emit(eventType, event); + }; - /** - * Created by Alex on 2/23/2015. - */ + hammer.destroy = function () { + // Detach from DOM element + var element = hammer.element; + delete element.hammer; - var CentralGravitySolver = (function () { - function CentralGravitySolver(body, physicsBody, options) { - _classCallCheck(this, CentralGravitySolver); + // clear all handlers + hammer._handlers = {}; - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + // call original hammer destroy + hammer._destroy(); + }; - _prototypeProperties(CentralGravitySolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true - }, - solve: { - value: function solve() { - var dx, dy, distance, node, i; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + // split a string with space separated words + function split(events) { + return events.match(/[^ ]+/g); + } + /** + * A static event handler, applying event propagation. + * @param {Object} event + */ + function propagatedHandler(event) { + // let only a single hammer instance handle this event + if (event.type !== 'hammer.input') { + if (event.srcEvent._handled && event.srcEvent._handled[event.type]) { + return; + } + else { + // it is possible that the same srcEvent is used with multiple hammer events + event.srcEvent._handled = {}; + event.srcEvent._handled[event.type] = true; + } + } - var gravity = this.options.centralGravity; - var gravityForce = 0; + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - for (i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - node = nodes[nodeId]; - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); + // attach firstTarget property to the event + event.firstTarget = _firstTarget; - gravityForce = distance == 0 ? 0 : gravity / distance; - forces[nodeId].x = dx * gravityForce; - forces[nodeId].y = dy * gravityForce; + // propagate over all elements (until stopped) + var elem = _firstTarget; + while (elem && !stopped) { + var _handlers = elem.hammer && elem.hammer._handlers[event.type]; + if (_handlers) { + for (var i = 0; i < _handlers.length && !stopped; i++) { + _handlers[i](event); + } } - }, - writable: true, - configurable: true + + elem = elem.parentNode; + } } - }); - return CentralGravitySolver; - })(); + return hammer; + }; + })); - module.exports = CentralGravitySolver; /***/ }, -/* 93 */ +/* 66 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -31236,1053 +31089,938 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - /** - * Created by Alex on 24-Feb-15. - */ - var util = __webpack_require__(1); - var Cluster = _interopRequire(__webpack_require__(94)); - - var ClusterEngine = (function () { - function ClusterEngine(body) { - _classCallCheck(this, ClusterEngine); - this.body = body; - this.clusteredNodes = {}; - } - - _prototypeProperties(ClusterEngine, null, { - setOptions: { - value: function setOptions(options) {}, - writable: true, - configurable: true - }, - clusterByConnectionCount: { + var Label = _interopRequire(__webpack_require__(80)); - /** - * - * @param hubsize - * @param options - */ - value: function clusterByConnectionCount(hubsize, options) { - if (hubsize === undefined) { - hubsize = this._getHubSize(); - } else if (tyepof(hubsize) == "object") { - options = this._checkOptions(hubsize); - hubsize = this._getHubSize(); - } + var Box = _interopRequire(__webpack_require__(81)); - var nodesToCluster = []; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.edges.length >= hubsize) { - nodesToCluster.push(node.id); - } - } + var Circle = _interopRequire(__webpack_require__(82)); - for (var i = 0; i < nodesToCluster.length; i++) { - var node = this.body.nodes[nodesToCluster[i]]; - this.clusterByConnection(node, options, {}, {}, false); - } - this.body.emitter.emit("_dataChanged"); - }, - writable: true, - configurable: true - }, - clusterByNodeData: { + var CircularImage = _interopRequire(__webpack_require__(83)); + var Database = _interopRequire(__webpack_require__(84)); - /** - * loop over all nodes, check if they adhere to the condition and cluster if needed. - * @param options - * @param refreshData - */ - value: function clusterByNodeData() { - var options = arguments[0] === undefined ? {} : arguments[0]; - var refreshData = arguments[1] === undefined ? true : arguments[1]; - if (options.joinCondition === undefined) { - throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); - } + var Diamond = _interopRequire(__webpack_require__(85)); - // check if the options object is fine, append if needed - options = this._checkOptions(options); + var Dot = _interopRequire(__webpack_require__(86)); - var childNodesObj = {}; - var childEdgesObj = {}; + var Ellipse = _interopRequire(__webpack_require__(87)); - // collect the nodes that will be in the cluster - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - var clonedOptions = this._cloneOptions(nodeId); - if (options.joinCondition(clonedOptions) == true) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - } - } + var Icon = _interopRequire(__webpack_require__(88)); - this._cluster(childNodesObj, childEdgesObj, options, refreshData); - }, - writable: true, - configurable: true - }, - clusterOutliers: { + var Image = _interopRequire(__webpack_require__(89)); + var Square = _interopRequire(__webpack_require__(90)); - /** - * Cluster all nodes in the network that have only 1 edge - * @param options - * @param refreshData - */ - value: function clusterOutliers(options) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; - options = this._checkOptions(options); - var clusters = []; + var Star = _interopRequire(__webpack_require__(91)); - // collect the nodes that will be in the cluster - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var childNodesObj = {}; - var childEdgesObj = {}; - var nodeId = this.body.nodeIndices[i]; - if (this.body.nodes[nodeId].edges.length == 1) { - var edge = this.body.nodes[nodeId].edges[0]; - var childNodeId = this._getConnectedId(edge, nodeId); - if (childNodeId != nodeId) { - if (options.joinCondition === undefined) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } else { - var clonedOptions = this._cloneOptions(nodeId); - if (options.joinCondition(clonedOptions) == true) { - childNodesObj[nodeId] = this.body.nodes[nodeId]; - } - clonedOptions = this._cloneOptions(childNodeId); - if (options.joinCondition(clonedOptions) == true) { - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } - } - clusters.push({ nodes: childNodesObj, edges: childEdgesObj }); - } - } - } + var Text = _interopRequire(__webpack_require__(92)); - for (var i = 0; i < clusters.length; i++) { - this._cluster(clusters[i].nodes, clusters[i].edges, options, false); - } + var Triangle = _interopRequire(__webpack_require__(93)); - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - clusterByConnection: { + var TriangleDown = _interopRequire(__webpack_require__(94)); - /** - * - * @param nodeId - * @param options - * @param refreshData - */ - value: function clusterByConnection(nodeId, options) { - var refreshData = arguments[2] === undefined ? true : arguments[2]; - // kill conditions - if (nodeId === undefined) { - throw new Error("No nodeId supplied to clusterByConnection!"); - } - if (this.body.nodes[nodeId] === undefined) { - throw new Error("The nodeId given to clusterByConnection does not exist!"); - } + /** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} options An object containing options for the node. All + * options are optional, except for the id. + * {number} id Id of the node. Required + * {string} label Text label for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} shape Node shape, available: + * "database", "circle", "ellipse", + * "box", "image", "text", "dot", + * "star", "triangle", "triangleDown", + * "square", "icon" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Network.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Network.Groups} grouplist A list with groups. Needed for + * retrieving group options + * @param {Object} constants An object with default values for + * example for the color + * + */ + var Node = (function () { + function Node(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Node); - var node = this.body.nodes[nodeId]; - options = this._checkOptions(options, node); - if (options.clusterNodeProperties.x === undefined) { - options.clusterNodeProperties.x = node.x; - } - if (options.clusterNodeProperties.y === undefined) { - options.clusterNodeProperties.y = node.y; - } - if (options.clusterNodeProperties.fixed === undefined) { - options.clusterNodeProperties.fixed = {}; - options.clusterNodeProperties.fixed.x = node.options.fixed.x; - options.clusterNodeProperties.fixed.y = node.options.fixed.y; - } + this.options = util.bridgeObject(globalOptions); + this.body = body; + this.edges = []; // all edges connected to this node - var childNodesObj = {}; - var childEdgesObj = {}; - var parentNodeId = node.id; - var parentClonedOptions = this._cloneOptions(parentNodeId); - childNodesObj[parentNodeId] = node; + // set defaults for the options + this.id = undefined; + this.imagelist = imagelist; + this.grouplist = grouplist; - // collect the nodes that will be in the cluster - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - var childNodeId = this._getConnectedId(edge, parentNodeId); + // state options + this.x = undefined; + this.y = undefined; + this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate + this.selected = false; + this.hover = false; - if (childNodeId !== parentNodeId) { - if (options.joinCondition === undefined) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } else { - // clone the options and insert some additional parameters that could be interesting. - var childClonedOptions = this._cloneOptions(childNodeId); - if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { - childEdgesObj[edge.id] = edge; - childNodesObj[childNodeId] = this.body.nodes[childNodeId]; - } - } - } else { - childEdgesObj[edge.id] = edge; - } - } + this.labelModule = new Label(this.body, this.options); + this.setOptions(options); + } - this._cluster(childNodesObj, childEdgesObj, options, refreshData); - }, - writable: true, - configurable: true - }, - _cloneOptions: { + _prototypeProperties(Node, null, { + attachEdge: { /** - * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. - * @param objId - * @param type - * @returns {{}} - * @private - */ - value: function _cloneOptions(objId, type) { - var clonedOptions = {}; - if (type === undefined || type == "node") { - util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); - util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); - clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; - } else { - util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); + * Attach a edge to the node + * @param {Edge} edge + */ + value: function attachEdge(edge) { + if (this.edges.indexOf(edge) == -1) { + this.edges.push(edge); } - return clonedOptions; }, writable: true, configurable: true }, - _createClusterEdges: { + detachEdge: { /** - * This function creates the edges that will be attached to the cluster. - * - * @param childNodesObj - * @param childEdgesObj - * @param newEdges - * @param options - * @private - */ - value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { - var edge, childNodeId, childNode; - - var childKeys = Object.keys(childNodesObj); - for (var i = 0; i < childKeys.length; i++) { - childNodeId = childKeys[i]; - childNode = childNodesObj[childNodeId]; - - // mark all edges for removal from global and construct new edges from the cluster to others - for (var j = 0; j < childNode.edges.length; j++) { - edge = childNode.edges[j]; - childEdgesObj[edge.id] = edge; - - var otherNodeId = edge.toId; - var otherOnTo = true; - if (edge.toId != childNodeId) { - otherNodeId = edge.toId; - otherOnTo = true; - } else if (edge.fromId != childNodeId) { - otherNodeId = edge.fromId; - otherOnTo = false; - } - - if (childNodesObj[otherNodeId] === undefined) { - var clonedOptions = this._cloneOptions(edge.id, "edge"); - util.deepExtend(clonedOptions, options.clusterEdgeProperties); - if (otherOnTo === true) { - clonedOptions.from = options.clusterNodeProperties.id; - clonedOptions.to = otherNodeId; - } else { - clonedOptions.from = otherNodeId; - clonedOptions.to = options.clusterNodeProperties.id; - } - clonedOptions.id = "clusterEdge:" + util.randomUUID(); - newEdges.push(this.body.functions.createEdge(clonedOptions)); - } - } + * Detach a edge from the node + * @param {Edge} edge + */ + value: function detachEdge(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); } }, writable: true, configurable: true }, - _checkOptions: { - + togglePhysics: { /** - * This function checks the options that can be supplied to the different cluster functions - * for certain fields and inserts defaults if needed - * @param options - * @returns {*} - * @private - */ - value: function _checkOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; - if (options.clusterEdgeProperties === undefined) { - options.clusterEdgeProperties = {}; - } - if (options.clusterNodeProperties === undefined) { - options.clusterNodeProperties = {}; - } - - return options; + * Enable or disable the physics. + * @param status + */ + value: function togglePhysics(status) { + this.options.physics = status; }, writable: true, configurable: true }, - _cluster: { + setOptions: { + /** - * - * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node - * @param {Object} childEdgesObj | object with edge objects, id as keys - * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} - * @param {Boolean} refreshData | when true, do not wrap up - * @private - */ - value: function _cluster(childNodesObj, childEdgesObj, options) { - var refreshData = arguments[3] === undefined ? true : arguments[3]; - // kill condition: no children so cant cluster - if (Object.keys(childNodesObj).length == 0) { + * Set or overwrite options for the node + * @param {Object} options an object with options + * @param {Object} constants and object with default, global options + */ + value: function setOptions(options) { + if (!options) { return; } - // check if we have an unique id; - if (options.clusterNodeProperties.id === undefined) { - options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); - } - var clusterId = options.clusterNodeProperties.id; - - // create the new edges that will connect to the cluster - var newEdges = []; - this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + var fields = ["borderWidth", "borderWidthSelected", "brokenImage", "customScalingFunction", "font", "hidden", "icon", "id", "image", "label", "level", "physics", "shape", "size", "title", "value", "x", "y"]; + util.selectiveDeepExtend(fields, this.options, options); - // construct the clusterNodeProperties - var clusterNodeProperties = options.clusterNodeProperties; - if (options.processProperties !== undefined) { - // get the childNode options - var childNodesOptions = []; - for (var nodeId in childNodesObj) { - var clonedOptions = this._cloneOptions(nodeId); - childNodesOptions.push(clonedOptions); - } + // basic options + if (options.id !== undefined) { + this.id = options.id; + } - // get clusterproperties based on childNodes - var childEdgesOptions = []; - for (var edgeId in childEdgesObj) { - var clonedOptions = this._cloneOptions(edgeId, "edge"); - childEdgesOptions.push(clonedOptions); - } + if (this.id === undefined) { + throw "Node must have an id"; + } - clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); - if (!clusterNodeProperties) { - throw new Error("The processClusterProperties function does not return properties!"); - } + if (options.x !== undefined) { + this.x = options.x;this.predefinedPosition = true; } - if (clusterNodeProperties.label === undefined) { - clusterNodeProperties.label = "cluster"; + if (options.y !== undefined) { + this.y = options.y;this.predefinedPosition = true; + } + if (options.value !== undefined) { + this.value = options.value; } - - // give the clusterNode a postion if it does not have one. - var pos = undefined; - if (clusterNodeProperties.x === undefined) { - pos = this._getClusterPosition(childNodesObj); - clusterNodeProperties.x = pos.x; - clusterNodeProperties.allowedToMoveX = true; + // copy group options + if (typeof options.group === "number" || typeof options.group === "string" && options.group != "") { + var groupObj = this.grouplist.get(options.group); + util.deepExtend(this.options, groupObj); + // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. + this.options.color = util.parseColor(this.options.color); } - if (clusterNodeProperties.x === undefined) { - if (pos === undefined) { - pos = this._getClusterPosition(childNodesObj); - } - clusterNodeProperties.y = pos.y; - clusterNodeProperties.allowedToMoveY = true; + // individual shape options + if (options.color !== undefined) { + this.options.color = util.parseColor(options.color); } - - // force the ID to remain the same - clusterNodeProperties.id = clusterId; - - - // create the clusterNode - var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); - clusterNode.isCluster = true; - clusterNode.containedNodes = childNodesObj; - clusterNode.containedEdges = childEdgesObj; - - - // disable the childEdges - for (var edgeId in childEdgesObj) { - if (childEdgesObj.hasOwnProperty(edgeId)) { - if (this.body.edges[edgeId] !== undefined) { - var edge = this.body.edges[edgeId]; - edge.togglePhysics(false); - edge.options.hidden = true; - } + if (this.options.image !== undefined && this.options.image != "") { + if (this.imagelist) { + this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage); + } else { + throw "No imagelist provided"; } } - - // disable the childNodes - for (var nodeId in childNodesObj) { - if (childNodesObj.hasOwnProperty(nodeId)) { - this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; - this.body.nodes[nodeId].togglePhysics(false); - this.body.nodes[nodeId].options.hidden = true; + if (options.fixed !== undefined) { + if (typeof options.fixed == "boolean") { + this.options.fixed.x = true; + this.options.fixed.y = true; + } else { + if (options.fixed.x !== undefined && typeof options.fixed.x == "boolean") { + this.options.fixed.x = options.fixed.x; + } + if (options.fixed.y !== undefined && typeof options.fixed.y == "boolean") { + this.options.fixed.y = options.fixed.y; + } } } + // choose draw method depending on the shape + switch (this.options.shape) { - // finally put the cluster node into global - this.body.nodes[clusterNodeProperties.id] = clusterNode; - - - // push new edges to global - for (var i = 0; i < newEdges.length; i++) { - this.body.edges[newEdges[i].id] = newEdges[i]; - this.body.edges[newEdges[i].id].connect(); + case "box": + this.shape = new Box(this.options, this.body, this.labelModule); + break; + case "circle": + this.shape = new Circle(this.options, this.body, this.labelModule); + break; + case "circularImage": + this.shape = new CircularImage(this.options, this.body, this.labelModule, this.imageObj); + break; + case "database": + this.shape = new Database(this.options, this.body, this.labelModule); + break; + case "diamond": + this.shape = new Diamond(this.options, this.body, this.labelModule); + break; + case "dot": + this.shape = new Dot(this.options, this.body, this.labelModule); + break; + case "ellipse": + this.shape = new Ellipse(this.options, this.body, this.labelModule); + break; + case "icon": + this.shape = new Icon(this.options, this.body, this.labelModule); + break; + case "image": + this.shape = new Image(this.options, this.body, this.labelModule, this.imageObj); + break; + case "square": + this.shape = new Square(this.options, this.body, this.labelModule); + break; + case "star": + this.shape = new Star(this.options, this.body, this.labelModule); + break; + case "text": + this.shape = new Text(this.options, this.body, this.labelModule); + break; + case "triangle": + this.shape = new Triangle(this.options, this.body, this.labelModule); + break; + case "triangleDown": + this.shape = new TriangleDown(this.options, this.body, this.labelModule); + break; + default: + this.shape = new Ellipse(this.options, this.body, this.labelModule); + break; } - // set ID to undefined so no duplicates arise - clusterNodeProperties.id = undefined; - + this.labelModule.setOptions(this.options, options); - // wrap up - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } + // reset the size of the node, this can be changed + this._reset(); }, writable: true, configurable: true }, - isCluster: { + select: { /** - * Check if a node is a cluster. - * @param nodeId - * @returns {*} - */ - value: function isCluster(nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].isCluster === true; - } else { - console.log("Node does not exist."); - return false; - } + * select this node + */ + value: function select() { + this.selected = true; + this._reset(); }, writable: true, configurable: true }, - _getClusterPosition: { + unselect: { + /** - * get the position of the cluster node based on what's inside - * @param {object} childNodesObj | object with node objects, id as keys - * @returns {{x: number, y: number}} - * @private - */ - value: function _getClusterPosition(childNodesObj) { - var childKeys = Object.keys(childNodesObj); - var minX = childNodesObj[childKeys[0]].x; - var maxX = childNodesObj[childKeys[0]].x; - var minY = childNodesObj[childKeys[0]].y; - var maxY = childNodesObj[childKeys[0]].y; - var node; - for (var i = 0; i < childKeys.lenght; i++) { - node = childNodesObj[childKeys[0]]; - minX = node.x < minX ? node.x : minX; - maxX = node.x > maxX ? node.x : maxX; - minY = node.y < minY ? node.y : minY; - maxY = node.y > maxY ? node.y : maxY; - } - return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; + * unselect this node + */ + value: function unselect() { + this.selected = false; + this._reset(); }, writable: true, configurable: true }, - openCluster: { - - - /** - * Open a cluster by calling this function. - * @param {String} clusterNodeId | the ID of the cluster node - * @param {Boolean} refreshData | wrap up afterwards if not true - */ - value: function openCluster(clusterNodeId) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; - // kill conditions - if (clusterNodeId === undefined) { - throw new Error("No clusterNodeId supplied to openCluster."); - } - if (this.body.nodes[clusterNodeId] === undefined) { - throw new Error("The clusterNodeId supplied to openCluster does not exist."); - } - if (this.body.nodes[clusterNodeId].containedNodes === undefined) { - console.log("The node:" + clusterNodeId + " is not a cluster.");return; - }; + _reset: { - var clusterNode = this.body.nodes[clusterNodeId]; - var containedNodes = clusterNode.containedNodes; - var containedEdges = clusterNode.containedEdges; - // release nodes - for (var nodeId in containedNodes) { - if (containedNodes.hasOwnProperty(nodeId)) { - var containedNode = this.body.nodes[nodeId]; - containedNode = containedNodes[nodeId]; - // inherit position - containedNode.x = clusterNode.x; - containedNode.y = clusterNode.y; - // inherit speed - containedNode.vx = clusterNode.vx; - containedNode.vy = clusterNode.vy; + /** + * Reset the calculated size of the node, forces it to recalculate its size + * @private + */ + value: function _reset() { + this.shape.width = undefined; + this.shape.height = undefined; + }, + writable: true, + configurable: true + }, + getTitle: { - containedNode.options.hidden = false; - containedNode.togglePhysics(true); - delete this.clusteredNodes[nodeId]; - } - } + /** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ + value: function getTitle() { + return typeof this.options.title === "function" ? this.options.title() : this.options.title; + }, + writable: true, + configurable: true + }, + distanceToBorder: { - // release edges - for (var edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - var edge = this.body.edges[edgeId]; - edge.options.hidden = false; - edge.togglePhysics(true); - } - } - // remove all temporary edges - for (var i = 0; i < clusterNode.edges.length; i++) { - var edgeId = clusterNode.edges[i].id; - var viaId = this.body.edges[edgeId].via.id; - if (viaId) { - this.body.edges[edgeId].via = undefined; - delete this.body.nodes[viaId]; - } - // this removes the edge from node.edges, which is why edgeIds is formed - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; - } + /** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ + value: function distanceToBorder(ctx, angle) { + return this.shape.distanceToBorder(ctx, angle); + }, + writable: true, + configurable: true + }, + isFixed: { - // remove clusterNode - delete this.body.nodes[clusterNodeId]; - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } + /** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ + value: function isFixed() { + return this.options.fixed.x && this.options.fixed.y; }, writable: true, configurable: true }, - _connectEdge: { - + isSelected: { /** - * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to - * is currently residing in cluster B - * @param edge - * @param nodeId - * @param from - * @private - */ - value: function _connectEdge(edge, nodeId, from) { - var clusterStack = this._getClusterStack(nodeId); - if (from == true) { - edge.from = clusterStack[clusterStack.length - 1]; - edge.fromId = clusterStack[clusterStack.length - 1].id; - clusterStack.pop(); - edge.fromArray = clusterStack; - } else { - edge.to = clusterStack[clusterStack.length - 1]; - edge.toId = clusterStack[clusterStack.length - 1].id; - clusterStack.pop(); - edge.toArray = clusterStack; - } - edge.connect(); + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; }, writable: true, configurable: true }, - _getClusterStack: { + getValue: { - /** - * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node - * @param nodeId - * @returns {Array} - * @private - */ - value: function _getClusterStack(nodeId) { - var stack = []; - var max = 100; - var counter = 0; - while (this.clusteredNodes[nodeId] !== undefined && counter < max) { - stack.push(this.clusteredNodes[nodeId].node); - nodeId = this.clusteredNodes[nodeId].clusterId; - counter++; - } - stack.push(this.body.nodes[nodeId]); - return stack; + /** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.value; }, writable: true, configurable: true }, - _getConnectedId: { + setValueRange: { /** - * Get the Id the node is connected to - * @param edge - * @param nodeId - * @returns {*} - * @private - */ - value: function _getConnectedId(edge, nodeId) { - if (edge.toId != nodeId) { - return edge.toId; - } else if (edge.fromId != nodeId) { - return edge.fromId; - } else { - return edge.fromId; + * Adjust the value range of the node. The node will adjust it's size + * based on its value. + * @param {Number} min + * @param {Number} max + */ + value: function setValueRange(min, max, total) { + if (this.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); + var sizeDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled == true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + } + this.options.size = this.options.scaling.min + scale * sizeDiff; } }, writable: true, configurable: true }, - _getHubSize: { + draw: { - /** - * We determine how many connections denote an important hub. - * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) - * - * @private - */ - value: function _getHubSize() { - var average = 0; - var averageSquared = 0; - var hubCounter = 0; - var largestHub = 0; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var node = this.body.nodes[this.body.nodeIndices[i]]; - if (node.edges.length > largestHub) { - largestHub = node.edges.length; - } - average += node.edges.length; - averageSquared += Math.pow(node.edges.length, 2); - hubCounter += 1; - } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; + /** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function draw(ctx) { + this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); + }, + writable: true, + configurable: true + }, + resize: { - var variance = averageSquared - Math.pow(average, 2); - var standardDeviation = Math.sqrt(variance); - var hubThreshold = Math.floor(average + 2 * standardDeviation); + /** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function resize(ctx) { + this.shape.resize(ctx); + }, + writable: true, + configurable: true + }, + isOverlappingWith: { - // always have at least one to cluster - if (hubThreshold > largestHub) { - hubThreshold = largestHub; - } - return hubThreshold; + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ + value: function isOverlappingWith(obj) { + return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; }, writable: true, configurable: true } }); - return ClusterEngine; + return Node; })(); - module.exports = ClusterEngine; + module.exports = Node; /***/ }, -/* 94 */ +/* 67 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var Node = _interopRequire(__webpack_require__(60)); - - /** - * - */ - var Cluster = (function (Node) { - function Cluster(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Cluster); - - _get(Object.getPrototypeOf(Cluster.prototype), "constructor", this).call(this, options, body, imagelist, grouplist, globalOptions); + var util = __webpack_require__(1); - this.isCluster = true; - this.containedNodes = {}; - this.containedEdges = {}; - } - _inherits(Cluster, Node); + var Label = _interopRequire(__webpack_require__(80)); - return Cluster; - })(Node); + var BezierEdgeDynamic = _interopRequire(__webpack_require__(95)); - module.exports = Cluster; + var BezierEdgeStatic = _interopRequire(__webpack_require__(96)); -/***/ }, -/* 95 */ -/***/ function(module, exports, __webpack_require__) { + var StraightEdge = _interopRequire(__webpack_require__(97)); - "use strict"; + /** + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with options. Must contain + * At least options from and to. + * Available options: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color + */ + var Edge = (function () { + function Edge(options, body, globalOptions) { + _classCallCheck(this, Edge); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + if (body === undefined) { + throw "No body provided"; + } + this.options = util.bridgeObject(globalOptions); + this.body = body; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.value = undefined; + this.selected = false; + this.hover = false; + this.labelDirty = true; + this.colorDirty = true; - /** - * Created by Alex on 26-Feb-15. - */ + this.from = undefined; // a node + this.to = undefined; // a node - if (typeof window !== "undefined") { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; - } + this.edgeType = undefined; - var util = __webpack_require__(1); + this.connected = false; + this.labelModule = new Label(this.body, this.options); - var CanvasRenderer = (function () { - function CanvasRenderer(body, canvas) { - var _this = this; - _classCallCheck(this, CanvasRenderer); + this.setOptions(options); - this.body = body; - this.canvas = canvas; + this.controlNodesEnabled = false; + this.controlNodes = { from: undefined, to: undefined, positions: {} }; + this.connectedNode = undefined; + } - this.redrawRequested = false; - this.renderTimer = false; - this.requiresTimeout = true; - this.renderingActive = false; - this.renderRequests = 0; - this.pixelRatio = undefined; + _prototypeProperties(Edge, null, { + setOptions: { - // redefined in this._redraw - this.canvasTopLeft = { x: 0, y: 0 }; - this.canvasBottomRight = { x: 0, y: 0 }; - this.dragging = false; + /** + * Set or overwrite options for the edge + * @param {Object} options an object with options + * @param doNotEmit + */ + value: function setOptions(options) { + if (!options) { + return; + } + this.colorDirty = true; - this.body.emitter.on("dragStart", function () { - _this.dragging = true; - }); - this.body.emitter.on("dragEnd", function () { - return _this.dragging = false; - }); - this.body.emitter.on("_redraw", function () { - if (_this.renderingActive === false) { - _this._redraw(); - } - }); - this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); - this.body.emitter.on("_startRendering", function () { - _this.renderRequests += 1;_this.renderingActive = true;_this.startRendering(); - }); - this.body.emitter.on("_stopRendering", function () { - _this.renderRequests -= 1;_this.renderingActive = _this.renderRequests > 0; - }); + var fields = ["id", "font", "from", "hidden", "hoverWidth", "label", "length", "line", "opacity", "physics", "scaling", "selfReferenceSize", "to", "title", "value", "width", "widthMin", "widthMax", "widthSelectionMultiplier"]; + util.selectiveDeepExtend(fields, this.options, options); - this.options = {}; - this.defaultOptions = { - hideEdgesOnDrag: false, - hideNodesOnDrag: false - }; - util.extend(this.options, this.defaultOptions); + util.mergeOptions(this.options, options, "smooth"); + util.mergeOptions(this.options, options, "dashes"); - this._determineBrowserMethod(); - } + if (options.id !== undefined) { + this.id = options.id; + } + if (options.from !== undefined) { + this.fromId = options.from; + } + if (options.to !== undefined) { + this.toId = options.to; + } + if (options.title !== undefined) { + this.title = options.title; + } + if (options.value !== undefined) { + this.value = options.value; + } - _prototypeProperties(CanvasRenderer, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); + // hanlde multiple input cases for arrows + if (options.arrows !== undefined) { + if (typeof options.arrows === "string") { + var arrows = options.arrows.toLowerCase(); + if (arrows.indexOf("to") != -1) { + this.options.arrows.to.enabled = true; + } + if (arrows.indexOf("middle") != -1) { + this.options.arrows.middle.enabled = true; + } + if (arrows.indexOf("from") != -1) { + this.options.arrows.from.enabled = true; + } + } else if (typeof options.arrows === "object") { + util.mergeOptions(this.options.arrows, options.arrows, "to"); + util.mergeOptions(this.options.arrows, options.arrows, "middle"); + util.mergeOptions(this.options.arrows, options.arrows, "from"); + } else { + throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); + } } - }, - writable: true, - configurable: true - }, - startRendering: { - value: function startRendering() { - if (this.renderingActive === true) { - if (!this.renderTimer) { - if (this.requiresTimeout == true) { - this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function - } else { - this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function + + // hanlde multiple input cases for color + if (options.color !== undefined) { + if (util.isString(options.color)) { + util.assignAllKeys(this.options.color, options.color); + this.options.color.inherit.enabled = false; + } else { + util.extend(this.options.color, options.color); + if (options.color.inherit === undefined) { + this.options.color.inherit.enabled = false; } } - } else {} + util.mergeOptions(this.options.color, options.color, "inherit"); + } + + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); + + this.labelModule.setOptions(this.options); + + var dataChanged = this.updateEdgeType(); + return dataChanged; }, writable: true, configurable: true }, - renderStep: { - value: function renderStep() { - // reset the renderTimer so a new scheduled animation step can be set - this.renderTimer = undefined; + updateEdgeType: { + value: function updateEdgeType() { + var dataChanged = false; + var changeInType = true; + if (this.edgeType !== undefined) { + if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { + changeInType = false; + } + if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false) { + changeInType = false; + } + if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) { + changeInType = false; + } - if (this.requiresTimeout == true) { - // this schedules a new simulation step - this.startRendering(); + if (changeInType == true) { + dataChanged = this.edgeType.cleanup(); + } } - this._redraw(); - - if (this.requiresTimeout == false) { - // this schedules a new simulation step - this.startRendering(); + if (changeInType === true) { + if (this.options.smooth.enabled === true) { + if (this.options.smooth.dynamic === true) { + dataChanged = true; + this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); + } else { + this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); + } + } else { + this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); + } + } else { + // if nothing changes, we just set the options. + this.edgeType.setOptions(this.options); } + + return dataChanged; }, writable: true, configurable: true }, - redraw: { + togglePhysics: { + /** - * Redraw the network with the current data - * chart will be resized too. + * Enable or disable the physics. + * @param status */ - value: function redraw() { - this.setSize(this.constants.width, this.constants.height); - this._redraw(); + value: function togglePhysics(status) { + if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { + if (this.via === undefined) { + this.via.pptions.physics = status; + } + } + this.options.physics = status; }, writable: true, configurable: true }, - _requestRedraw: { + connect: { /** - * Redraw the network with the current data - * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. - * @private + * Connect an edge to its nodes */ - value: function _requestRedraw() { - if (this.redrawRequested !== true && this.renderingActive === false) { - this.redrawRequested = true; - if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, false), 0); - } else { - window.requestAnimationFrame(this._redraw.bind(this, false)); + value: function connect() { + this.disconnect(); + + this.from = this.body.nodes[this.fromId] || undefined; + this.to = this.body.nodes[this.toId] || undefined; + this.connected = this.from !== undefined && this.to !== undefined; + + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); + } + if (this.to) { + this.to.detachEdge(this); } } }, writable: true, configurable: true }, - _redraw: { - value: function _redraw() { - var hidden = arguments[0] === undefined ? false : arguments[0]; - this.body.emitter.emit("initRedraw"); + disconnect: { - this.redrawRequested = false; - var ctx = this.canvas.frame.canvas.getContext("2d"); - if (this.pixelRation === undefined) { - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + /** + * Disconnect an edge from its nodes + */ + value: function disconnect() { + if (this.from) { + this.from.detachEdge(this); + this.from = undefined; + } + if (this.to) { + this.to.detachEdge(this); + this.to = undefined; } - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - - // clear the canvas - var w = this.canvas.frame.canvas.clientWidth; - var h = this.canvas.frame.canvas.clientHeight; - ctx.clearRect(0, 0, w, h); - - this.body.emitter.emit("beforeDrawing", ctx); + this.connected = false; + }, + writable: true, + configurable: true + }, + getTitle: { - // set scaling and translation - ctx.save(); - ctx.translate(this.body.view.translation.x, this.body.view.translation.y); - ctx.scale(this.body.view.scale, this.body.view.scale); - this.canvasTopLeft = this.canvas.DOMtoCanvas({ x: 0, y: 0 }); - this.canvasBottomRight = this.canvas.DOMtoCanvas({ x: this.canvas.frame.canvas.clientWidth, y: this.canvas.frame.canvas.clientHeight }); + /** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ + value: function getTitle() { + return typeof this.title === "function" ? this.title() : this.title; + }, + writable: true, + configurable: true + }, + isSelected: { - if (hidden === false) { - if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { - this._drawEdges(ctx); - } - } - if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { - this._drawNodes(ctx, hidden); - } + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + }, + writable: true, + configurable: true + }, + getValue: { - if (this.controlNodesActive === true) { - this._drawControlNodes(ctx); - } - //this.physics.nodesSolver._debug(ctx,"#F00F0F"); - this.body.emitter.emit("afterDrawing", ctx); + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.value; + }, + writable: true, + configurable: true + }, + setValueRange: { - // restore original scaling and translation - ctx.restore(); - if (hidden === true) { - ctx.clearRect(0, 0, w, h); + /** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + * @param total + */ + value: function setValueRange(min, max, total) { + if (this.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); + var widthDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled == true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + } + this.options.width = this.options.scaling.min + scale * widthDiff; } - }, writable: true, configurable: true }, - _drawNodes: { + draw: { /** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * 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 - * @param {Boolean} [alwaysShow] - * @private */ - value: function _drawNodes(ctx) { - var alwaysShow = arguments[1] === undefined ? false : arguments[1]; - var nodes = this.body.nodes; - var nodeIndices = this.body.nodeIndices; - var node; - var selected = []; + value: function draw(ctx) { + var via = this.edgeType.drawLine(ctx, this.selected, this.hover); + this.drawArrows(ctx, via); + this.drawLabel(ctx, via); + }, + writable: true, + configurable: true + }, + drawArrows: { + value: function drawArrows(ctx, viaNode) { + if (this.options.arrows.from.enabled === true) { + this.edgeType.drawArrowHead(ctx, "from", viaNode, this.selected, this.hover); + } + if (this.options.arrows.middle.enabled === true) { + this.edgeType.drawArrowHead(ctx, "middle", viaNode, this.selected, this.hover); + } + if (this.options.arrows.to.enabled === true) { + this.edgeType.drawArrowHead(ctx, "to", viaNode, this.selected, this.hover); + } + }, + writable: true, + configurable: true + }, + drawLabel: { + value: function drawLabel(ctx, viaNode) { + if (this.options.label !== undefined) { + // set style + var node1 = this.from; + var node2 = this.to; + var selected = this.from.selected || this.to.selected || this.selected; + if (node1.id != node2.id) { + var point = this.edgeType.getPoint(0.5, viaNode); + ctx.save(); - // draw unselected nodes; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - // set selected nodes aside - if (node.isSelected()) { - selected.push(nodeIndices[i]); - } else { - if (alwaysShow === true) { - node.draw(ctx); + // if the label has to be rotated: + if (this.options.font.align !== "horizontal") { + this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); + ctx.translate(point.x, this.labelModule.size.yLine); + this._rotateForLabelAlignment(ctx); } - // todo: replace check - //else if (node.inArea() === true) { - node.draw(ctx); - //} - } - } - // draw the selected nodes on top - for (var i = 0; i < selected.length; i++) { - node = nodes[selected[i]]; - node.draw(ctx); + // draw the label + this.labelModule.draw(ctx, point.x, point.y, selected); + ctx.restore(); + } else { + var x, y; + var radius = this.options.selfReferenceSize; + if (node1.width > node1.height) { + x = node1.x + node1.width * 0.5; + y = node1.y - radius; + } else { + x = node1.x + radius; + y = node1.y - node1.height * 0.5; + } + point = this._pointOnCircle(x, y, radius, 0.125); + + this.labelModule.draw(ctx, point.x, point.y, selected); + } } }, writable: true, configurable: true }, - _drawEdges: { + isOverlappingWith: { /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge */ - value: function _drawEdges(ctx) { - var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge; + value: function isOverlappingWith(obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edge.draw(ctx); - } + var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return dist < distMax; + } else { + return false; } }, writable: true, configurable: true }, - _drawControlNodes: { + _rotateForLabelAlignment: { + /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _drawControlNodes(ctx) { - var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge; + value: function _rotateForLabelAlignment(ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - edge._drawControlNodes(ctx); + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; } + + ctx.rotate(angleInDegrees); }, writable: true, configurable: true }, - _determineBrowserMethod: { + _pointOnCircle: { + /** - * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because - * some implementations (safari and IE9) did not support requestAnimationFrame + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point * @private */ - value: function _determineBrowserMethod() { - if (typeof window !== "undefined") { - var browserType = navigator.userAgent.toLowerCase(); - this.requiresTimeout = false; - if (browserType.indexOf("msie 9.0") != -1) { - // IE 9 - this.requiresTimeout = true; - } else if (browserType.indexOf("safari") != -1) { - // safari - if (browserType.indexOf("chrome") <= -1) { - this.requiresTimeout = true; - } - } - } else { - this.requiresTimeout = true; - } + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; + }, + writable: true, + configurable: true + }, + select: { + value: function select() { + this.selected = true; + }, + writable: true, + configurable: true + }, + unselect: { + value: function unselect() { + this.selected = false; }, writable: true, configurable: true } }); - return CanvasRenderer; + return Edge; })(); - module.exports = CanvasRenderer; + module.exports = Edge; /***/ }, -/* 96 */ +/* 68 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32291,310 +32029,605 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var Hammer = __webpack_require__(19); - var hammerUtil = __webpack_require__(24); - - var util = __webpack_require__(1); - /** - * 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 + * Created by Alex on 2/23/2015. */ - var Canvas = (function () { - function Canvas(body) { - var _this = this; - _classCallCheck(this, Canvas); - - this.body = body; - - this.options = {}; - this.defaultOptions = { - width: "100%", - height: "100%" - }; - util.extend(this.options, this.defaultOptions); - this.body.emitter.once("resize", function (obj) { - _this.body.view.translation.x = obj.width * 0.5;_this.body.view.translation.y = obj.height * 0.5; - }); - this.body.emitter.on("destroy", function () { - return _this.hammer.destroy(); - }); + var BarnesHutSolver = (function () { + function BarnesHutSolver(body, physicsBody, options) { + _classCallCheck(this, BarnesHutSolver); - this.pixelRatio = 1; + this.body = body; + this.physicsBody = physicsBody; + this.barnesHutTree; + this.setOptions(options); } - _prototypeProperties(Canvas, null, { + _prototypeProperties(BarnesHutSolver, null, { setOptions: { value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); - } + this.options = options; }, writable: true, configurable: true }, - create: { - value: function create() { - // remove all elements from the container element. - while (this.body.container.hasChildNodes()) { - this.body.container.removeChild(this.body.container.firstChild); - } + solve: { - this.frame = document.createElement("div"); - this.frame.className = "vis network-frame"; - this.frame.style.position = "relative"; - this.frame.style.overflow = "hidden"; - this.frame.tabIndex = 900; - ////////////////////////////////////////////////////////////////// + /** + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. + * + * @private + */ + value: function solve() { + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var nodeCount = nodeIndices.length; - this.frame.canvas = document.createElement("canvas"); - this.frame.canvas.style.position = "relative"; - this.frame.appendChild(this.frame.canvas); + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); - 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); - } else { - var ctx = this.frame.canvas.getContext("2d"); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + // for debugging + this.barnesHutTree = barnesHutTree; - this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); + } + } } + }, + writable: true, + configurable: true + }, + _getForceContribution: { - // add the frame to the container element - this.body.container.appendChild(this.frame); - this.body.view.scale = 1; - this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; + /** + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node + * @private + */ + value: function _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; - this._bindHammer(); + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // BarnesHutSolver condition + // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.options.thetaInverted) { + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount == 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } else { + // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { + // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } + } + } + } }, writable: true, configurable: true }, - _bindHammer: { + _formBarnesHutTree: { /** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices * @private */ - value: function _bindHammer() { - var _this = this; - if (this.hammer !== undefined) { - this.hammer.destroy(); + value: function _formBarnesHutTree(nodes, nodeIndices) { + var node; + var nodeCount = nodeIndices.length; + + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; + + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } } - this.drag = {}; - this.pinch = {}; + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize - // init hammer - this.hammer = new Hammer(this.frame.canvas); - this.hammer.get("pinch").set({ enable: true }); - hammerUtil.onTouch(this.hammer, function (event) { - _this.body.eventListeners.onTouch(event); - }); - this.hammer.on("tap", function (event) { - _this.body.eventListeners.onTap(event); - }); - this.hammer.on("doubletap", function (event) { - _this.body.eventListeners.onDoubleTap(event); - }); - this.hammer.on("press", function (event) { - _this.body.eventListeners.onHold(event); - }); - this.hammer.on("panstart", function (event) { - _this.body.eventListeners.onDragStart(event); - }); - this.hammer.on("panmove", function (event) { - _this.body.eventListeners.onDrag(event); - }); - this.hammer.on("panend", function (event) { - _this.body.eventListeners.onDragEnd(event); - }); - this.hammer.on("pinch", function (event) { - _this.body.eventListeners.onPinch(event); - }); + var minimumTreeSize = 0.00001; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), + centerY = 0.5 * (minY + maxY); - // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? - this.frame.canvas.addEventListener("mousewheel", function (event) { - _this.body.eventListeners.onMouseWheel(event); - }); - this.frame.canvas.addEventListener("DOMMouseScroll", function (event) { - _this.body.eventListeners.onMouseWheel(event); - }); + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: { data: null }, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); - this.frame.canvas.addEventListener("mousemove", function (event) { - _this.body.eventListeners.onMouseMove(event); - }); + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); + } + } - this.hammerFrame = new Hammer(this.frame); - hammerUtil.onRelease(this.hammerFrame, function (event) { - _this.body.eventListeners.onRelease(event); - }); + // make global + return barnesHutTree; }, writable: true, configurable: true }, - setSize: { + _updateBranchMass: { /** - * 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%') + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node + * @private */ - value: function setSize() { - var width = arguments[0] === undefined ? this.options.width : arguments[0]; - var height = arguments[1] === undefined ? this.options.height : arguments[1]; - var emitEvent = false; - var oldWidth = this.frame.canvas.width; - var oldHeight = this.frame.canvas.height; - if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { - this.frame.style.width = width; - this.frame.style.height = height; + value: function _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; - this.frame.canvas.style.width = "100%"; - this.frame.canvas.style.height = "100%"; + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; - this.options.width = width; - this.options.height = height; + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + }, + writable: true, + configurable: true + }, + _placeInTree: { - emitEvent = true; - } else { - // this would adapt the width of the canvas to the width from 100% if and only if - // there is a change. - if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - emitEvent = true; + /** + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private + */ + value: function _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } + + if (parentBranch.children.NW.range.maxX > node.x) { + // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { + // in NW + this._placeInRegion(parentBranch, node, "NW"); + } else { + // in SW + this._placeInRegion(parentBranch, node, "SW"); } - if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - emitEvent = true; + } else { + // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { + // in NE + this._placeInRegion(parentBranch, node, "NE"); + } else { + // in SE + this._placeInRegion(parentBranch, node, "SE"); } } + }, + writable: true, + configurable: true + }, + _placeInRegion: { - if (emitEvent === true) { - this.body.emitter.emit("resize", { width: this.frame.canvas.width / this.pixelRatio, height: this.frame.canvas.height / this.pixelRatio, oldWidth: oldWidth / this.pixelRatio, oldHeight: oldHeight / this.pixelRatio }); + + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + value: function _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: + // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); + break; + case 1: + // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x == node.x && parentBranch.children[region].children.data.y == node.y) { + node.x += Math.random(); + node.y += Math.random(); + } else { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); + } + break; + case 4: + // place in branch + this._placeInTree(parentBranch.children[region], node); + break; } }, writable: true, configurable: true }, - _XconvertDOMtoCanvas: { + _splitBranch: { /** - * 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} + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch * @private */ - value: function _XconvertDOMtoCanvas(x) { - return (x - this.body.view.translation.x) / this.body.view.scale; + value: function _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount == 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; + } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); + + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); + } }, writable: true, configurable: true }, - _XconvertCanvasToDOM: { + _insertRegion: { + /** - * 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} + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange * @private */ - value: function _XconvertCanvasToDOM(x) { - return x * this.body.view.scale + this.body.view.translation.x; + value: function _insertRegion(parentBranch, region) { + var minX, maxX, minY, maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } + + + parentBranch.children[region] = { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: { data: null }, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; }, writable: true, configurable: true }, - _YconvertDOMtoCanvas: { + _debug: { + + + + + //--------------------------- DEBUGGING BELOW ---------------------------// + /** - * 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} + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color * @private */ - value: function _YconvertDOMtoCanvas(y) { - return (y - this.body.view.translation.y) / this.body.view.scale; + value: function _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { + ctx.lineWidth = 1; + + this._drawBranch(this.barnesHutTree.root, ctx, color); + } }, writable: true, configurable: true }, - _YconvertCanvasToDOM: { + _drawBranch: { + /** - * 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} + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color * @private */ - value: function _YconvertCanvasToDOM(y) { - return y * this.body.view.scale + this.body.view.translation.y; + value: function _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; + } + + if (branch.childrenCount == 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); + + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ }, writable: true, configurable: true - }, - canvasToDOM: { + } + }); + return BarnesHutSolver; + })(); - /** - * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor - */ - value: function canvasToDOM(pos) { - return { x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y) }; + module.exports = BarnesHutSolver; + +/***/ }, +/* 69 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 2/23/2015. + */ + + var RepulsionSolver = (function () { + function RepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, RepulsionSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(RepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - DOMtoCanvas: { - + solve: { /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. * - * @param {object} pos = {x: number, y: number} - * @returns {{x: number, y: number}} - * @constructor + * @private */ - value: function DOMtoCanvas(pos) { - return { x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y) }; + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2; + + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // approximation constants + var a = -2 / 3 / nodeDistance; + var b = 4 / 3; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (var i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (var j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + + if (distance < 2 * nodeDistance) { + if (distance < 0.5 * nodeDistance) { + repulsingForce = 1; + } else { + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) + } + repulsingForce = repulsingForce / distance; + + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } + } }, writable: true, configurable: true } }); - return Canvas; + return RepulsionSolver; })(); - module.exports = Canvas; + module.exports = RepulsionSolver; /***/ }, -/* 97 */ +/* 70 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32604,392 +32637,385 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Created by Alex on 26-Feb-15. + * Created by Alex on 2/23/2015. */ - var util = __webpack_require__(1); - - var View = (function () { - function View(body, canvas) { - var _this = this; - _classCallCheck(this, View); + var HierarchicalRepulsionSolver = (function () { + function HierarchicalRepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalRepulsionSolver); this.body = body; - this.canvas = canvas; - - this.animationSpeed = 1 / this.renderRefreshRate; - this.animationEasingFunction = "easeInOutQuint"; - this.easingTime = 0; - this.sourceScale = 0; - this.targetScale = 0; - this.sourceTranslation = 0; - this.targetTranslation = 0; - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; - this.touchTime = 0; - - this.viewFunction = undefined; - - this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); - this.body.emitter.on("animationFinished", function () { - _this.body.emitter.emit("_stopRendering"); - }); - this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); + this.physicsBody = physicsBody; + this.setOptions(options); } - _prototypeProperties(View, null, { + _prototypeProperties(HierarchicalRepulsionSolver, null, { setOptions: { - value: function setOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; + value: function setOptions(options) { this.options = options; }, writable: true, configurable: true }, - _getRange: { - + solve: { - // zoomExtent /** - * Find the center position of the network + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * * @private */ - value: function _getRange() { - var specificNodes = arguments[0] === undefined ? [] : arguments[0]; - var minY = 1000000000, - maxY = -1000000000, - minX = 1000000000, - maxX = -1000000000, - node; - if (specificNodes.length > 0) { - for (var i = 0; i < specificNodes.length; i++) { - node = this.body.nodes[specificNodes[i]]; - if (minX > node.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; - } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; - } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.boundingBox.bottom; - } // top is negative, bottom is positive - } - } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (minX > node.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; + + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; + + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; + + // nodes only affect nodes on their level + if (node1.level == node2.level) { + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); + } else { + repulsingForce = 0; } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; + // normalize force with + if (distance == 0) { + distance = 0.01; + } else { + repulsingForce = repulsingForce / distance; } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.boundingBox.bottom; - } // top is negative, bottom is positive + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; } } } - - if (minX == 1000000000 && maxX == -1000000000 && minY == 1000000000 && maxY == -1000000000) { - minY = 0, maxY = 0, minX = 0, maxX = 0; - } - return { minX: minX, maxX: maxX, minY: minY, maxY: maxY }; }, writable: true, configurable: true - }, - _findCenter: { + } + }); + return HierarchicalRepulsionSolver; + })(); - /** - * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; - * @returns {{x: number, y: number}} - * @private - */ - value: function _findCenter(range) { - return { x: 0.5 * (range.maxX + range.minX), - y: 0.5 * (range.maxY + range.minY) }; + module.exports = HierarchicalRepulsionSolver; + +/***/ }, +/* 71 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 2/23/2015. + */ + + var SpringSolver = (function () { + function SpringSolver(body, physicsBody, options) { + _classCallCheck(this, SpringSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(SpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - zoomExtent: { - + solve: { /** - * This function zooms out to fit all data on screen based on amount of nodes - * @param {Object} - * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; - * @param {Boolean} [disableStart] | If true, start is not called. + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private */ - value: function zoomExtent() { - var options = arguments[0] === undefined ? { nodes: [] } : arguments[0]; - var initialZoom = arguments[1] === undefined ? false : arguments[1]; - var range; - var zoomLevel; - - if (initialZoom === true) { - // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. - var positionDefined = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (node.predefinedPosition == true) { - positionDefined += 1; - } - } - } - if (positionDefined > 0.5 * this.body.nodeIndices.length) { - this.zoomExtent(options, false); - return; - } - - range = this._getRange(options.nodes); - - var numberOfNodes = this.body.nodeIndices.length; - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + value: function solve() { + var edgeLength, edge; + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var edges = this.body.edges; - // correct for larger canvasses. - var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); - zoomLevel *= factor; - } else { - this.body.emitter.emit("_redraw", true); - range = this._getRange(options.nodes); - var xDistance = Math.abs(range.maxX - range.minX) * 1.1; - var yDistance = Math.abs(range.maxY - range.minY) * 1.1; + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + // only calculate forces if nodes are in the same sector + if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { + if (edge.edgeType.via !== undefined) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; + var node1 = edge.to; + var node2 = edge.edgeType.via; + var node3 = edge.from; - var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; - zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; - } - if (zoomLevel > 1) { - zoomLevel = 1; + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } else { + // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use + // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. + edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; + this._calculateSpringForce(edge.from, edge.to, edgeLength); + } + } + } } - - var center = this._findCenter(range); - var animationOptions = { position: center, scale: zoomLevel, animation: options }; - this.moveTo(animationOptions); }, writable: true, configurable: true }, - focusOnNode: { + _calculateSpringForce: { - // animation /** - * Center a node in view. + * This is the code actually performing the calculation for the function above. * - * @param {Number} nodeId - * @param {Number} [options] + * @param node1 + * @param node2 + * @param edgeLength + * @private */ - value: function focusOnNode(nodeId) { - var options = arguments[1] === undefined ? {} : arguments[1]; - if (this.body.nodes[nodeId] !== undefined) { - var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; - options.position = nodePosition; - options.lockedOnNode = nodeId; + value: function _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; - this.moveTo(options); - } else { - console.log("Node: " + nodeId + " cannot be found."); + dx = node1.x - node2.x; + dy = node1.y - node2.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + // handle the case where one node is not part of the physcis + if (this.physicsBody.forces[node1.id] !== undefined) { + this.physicsBody.forces[node1.id].x += fx; + this.physicsBody.forces[node1.id].y += fy; + } + + if (this.physicsBody.forces[node2.id] !== undefined) { + this.physicsBody.forces[node2.id].x -= fx; + this.physicsBody.forces[node2.id].y -= fy; } }, writable: true, configurable: true - }, - moveTo: { + } + }); - /** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.scale = Number // scale to move to - * | options.position = {x:Number, y:Number} // position to move to - * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to - */ - value: function moveTo(options) { - if (options === undefined) { - options = {}; - return; - } - if (options.offset === undefined) { - options.offset = { x: 0, y: 0 }; - } - if (options.offset.x === undefined) { - options.offset.x = 0; - } - if (options.offset.y === undefined) { - options.offset.y = 0; - } - if (options.scale === undefined) { - options.scale = this.body.view.scale; - } - if (options.position === undefined) { - options.position = this.body.view.translation; - } - if (options.animation === undefined) { - options.animation = { duration: 0 }; - } - if (options.animation === false) { - options.animation = { duration: 0 }; - } - if (options.animation === true) { - options.animation = {}; - } - if (options.animation.duration === undefined) { - options.animation.duration = 1000; - } // default duration - if (options.animation.easingFunction === undefined) { - options.animation.easingFunction = "easeInOutQuad"; - } // default easing function + return SpringSolver; + })(); - this.animateView(options); + module.exports = SpringSolver; + +/***/ }, +/* 72 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 2/25/2015. + */ + + var HierarchicalSpringSolver = (function () { + function HierarchicalSpringSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalSpringSolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(HierarchicalSpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - animateView: { + solve: { /** + * This function calculates the springforces on the nodes, accounting for the support nodes. * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.time = Number // animation time in milliseconds - * | options.scale = Number // scale to animate to - * | options.position = {x:Number, y:Number} // position to animate to - * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, - * // easeInCubic, easeOutCubic, easeInOutCubic, - * // easeInQuart, easeOutQuart, easeInOutQuart, - * // easeInQuint, easeOutQuint, easeInOutQuint + * @private */ - value: function animateView(options) { - if (options === undefined) { - return; - } - this.animationEasingFunction = options.animation.easingFunction; - // release if something focussed on the node - this.releaseNode(); - if (options.locked == true) { - this.lockedOnNodeId = options.lockedOnNode; - this.lockedOnNodeOffset = options.offset; - } + value: function solve() { + var edgeLength, edge; + var dx, dy, fx, fy, springForce, distance; + var edges = this.body.edges; + var factor = 0.5; - // forcefully complete the old animation if it was still running - if (this.easingTime != 0) { - this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + // initialize the spring force counters + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].springFx = 0; + forces[nodeId].springFy = 0; } - this.sourceScale = this.body.view.scale; - this.sourceTranslation = this.body.view.translation; - this.targetScale = options.scale; - // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw - // but at least then we'll have the target transition - this.body.view.scale = this.targetScale; - var viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - options.position.x, - y: viewCenter.y - options.position.y - }; - this.targetTranslation = { - x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, - y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y - }; + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - // if the time is set to 0, don't do an animation - if (options.animation.duration == 0) { - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - } else { - this.body.view.scale = this.targetScale; - this.body.view.translation = this.targetTranslation; - this.body.emitter.emit("_requestRedraw"); + dx = edge.from.x - edge.to.x; + dy = edge.from.y - edge.to.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + if (edge.to.level != edge.from.level) { + forces[edge.toId].springFx -= fx; + forces[edge.toId].springFy -= fy; + forces[edge.fromId].springFx += fx; + forces[edge.fromId].springFy += fy; + } else { + forces[edge.toId].x -= factor * fx; + forces[edge.toId].y -= factor * fy; + forces[edge.fromId].x += factor * fx; + forces[edge.fromId].y += factor * fy; + } } - } else { - this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's - this.animationEasingFunction = options.animation.easingFunction; + } + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); + springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); - this.viewFunction = this._transitionRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); + forces[nodeId].x += springFx; + forces[nodeId].y += springFy; } - }, - writable: true, - configurable: true - }, - _lockedRedraw: { - /** - * used to animate smoothly by hijacking the redraw function. - * @private - */ - value: function _lockedRedraw() { - var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; - var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - nodePosition.x, - y: viewCenter.y - nodePosition.y - }; - var sourceTranslation = this.body.view.translation; - var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y - }; + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + totalFx += forces[nodeId].x; + totalFy += forces[nodeId].y; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; - this.body.view.translation = targetTranslation; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].x -= correctionFx; + forces[nodeId].y -= correctionFy; + } }, writable: true, configurable: true - }, - releaseNode: { - value: function releaseNode() { - if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; - } + } + }); + + return HierarchicalSpringSolver; + })(); + + module.exports = HierarchicalSpringSolver; + +/***/ }, +/* 73 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 2/23/2015. + */ + + var CentralGravitySolver = (function () { + function CentralGravitySolver(body, physicsBody, options) { + _classCallCheck(this, CentralGravitySolver); + + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } + + _prototypeProperties(CentralGravitySolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - _transitionRedraw: { + solve: { + value: function solve() { + var dx, dy, distance, node, i; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - /** - * - * @param easingTime - * @private - */ - value: function _transitionRedraw() { - var finished = arguments[0] === undefined ? false : arguments[0]; - this.easingTime += this.animationSpeed; - this.easingTime = finished === true ? 1 : this.easingTime; - var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); + var gravity = this.options.centralGravity; + var gravityForce = 0; - this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress; - this.body.view.translation = { - x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, - y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress - }; + for (i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + node = nodes[nodeId]; + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); - // cleanup - if (this.easingTime >= 1) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.easingTime = 0; - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - } - this.body.emitter.emit("animationFinished"); + gravityForce = distance == 0 ? 0 : gravity / distance; + forces[nodeId].x = dx * gravityForce; + forces[nodeId].y = dy * gravityForce; } }, writable: true, @@ -32997,2447 +33023,2751 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return View; + return CentralGravitySolver; })(); - module.exports = View; + module.exports = CentralGravitySolver; /***/ }, -/* 98 */ +/* 74 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var Node = _interopRequire(__webpack_require__(66)); + /** - * Created by Alex on 2/27/2015. * */ + var Cluster = (function (Node) { + function Cluster(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Cluster); - var util = __webpack_require__(1); + _get(Object.getPrototypeOf(Cluster.prototype), "constructor", this).call(this, options, body, imagelist, grouplist, globalOptions); + + this.isCluster = true; + this.containedNodes = {}; + this.containedEdges = {}; + } - var NavigationHandler = _interopRequire(__webpack_require__(99)); + _inherits(Cluster, Node); - var Popup = _interopRequire(__webpack_require__(100)); + return Cluster; + })(Node); - var InteractionHandler = (function () { - function InteractionHandler(body, canvas, selectionHandler) { - _classCallCheck(this, InteractionHandler); + module.exports = Cluster; + +/***/ }, +/* 75 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var util = __webpack_require__(1); + var Hammer = __webpack_require__(41); + var hammerUtil = __webpack_require__(43); + var keycharm = __webpack_require__(78); + + var NavigationHandler = (function () { + function NavigationHandler(body, canvas) { + var _this = this; + _classCallCheck(this, NavigationHandler); this.body = body; this.canvas = canvas; - this.selectionHandler = selectionHandler; - this.navigationHandler = new NavigationHandler(body, canvas); - - // bind the events from hammer to functions in this object - this.body.eventListeners.onTap = this.onTap.bind(this); - this.body.eventListeners.onTouch = this.onTouch.bind(this); - this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); - this.body.eventListeners.onHold = this.onHold.bind(this); - this.body.eventListeners.onDragStart = this.onDragStart.bind(this); - this.body.eventListeners.onDrag = this.onDrag.bind(this); - this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); - this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); - this.body.eventListeners.onPinch = this.onPinch.bind(this); - this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); - this.body.eventListeners.onRelease = this.onRelease.bind(this); + this.iconsCreated = false; + this.navigationHammers = []; + this.boundFunctions = {}; this.touchTime = 0; - this.drag = {}; - this.pinch = {}; - this.hoverObj = { nodes: {}, edges: {} }; - this.popup = undefined; - this.popupObj = undefined; - this.popupTimer = undefined; + this.activated = false; - this.body.functions.getPointer = this.getPointer.bind(this); + this.body.emitter.on("release", this._stopMovement.bind(this)); + this.body.emitter.on("activate", function () { + _this.activated = true;_this.configureKeyboardBindings(); + }); + this.body.emitter.on("deactivate", function () { + _this.activated = false;_this.configureKeyboardBindings(); + }); + this.body.emitter.on("destroy", function () { + if (_this.keycharm !== undefined) { + _this.keycharm.destroy(); + } + }); this.options = {}; - this.defaultOptions = { - dragNodes: true, - dragView: true, - zoomView: true, - hoverEnabled: false, - showNavigationIcons: false, - tooltip: { - delay: 300, - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - color: { - border: "#666", - background: "#FFFFC6" - } - }, - keyboard: { - enabled: false, - speed: { x: 10, y: 10, zoom: 0.02 }, - bindToWindow: true - } - }; - util.extend(this.options, this.defaultOptions); } - _prototypeProperties(InteractionHandler, null, { + _prototypeProperties(NavigationHandler, null, { setOptions: { value: function setOptions(options) { if (options !== undefined) { - // extend all but the values in fields - var fields = ["keyboard", "tooltip"]; - util.selectiveNotDeepExtend(fields, this.options, options); - - // merge the keyboard options in. - util.mergeOptions(this.options, options, "keyboard"); - - if (options.tooltip) { - util.extend(this.options.tooltip, options.tooltip); - if (options.tooltip.color) { - this.options.tooltip.color = util.parseColor(options.tooltip.color); - } - } + this.options = options; + this.create(); } - - this.navigationHandler.setOptions(this.options); }, writable: true, configurable: true }, - getPointer: { - - - /** - * Get the pointer location from a touch location - * @param {{x: Number, y: Number}} touch - * @return {{x: Number, y: Number}} pointer - * @private - */ - value: function getPointer(touch) { - return { - x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), - y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) - }; + create: { + value: function create() { + if (this.options.showNavigationIcons === true) { + if (this.iconsCreated === false) { + this.loadNavigationElements(); + } + } else if (this.iconsCreated === true) { + this.cleanNavigation(); + } + + this.configureKeyboardBindings(); }, writable: true, configurable: true }, - onTouch: { + cleanNavigation: { + value: function cleanNavigation() { + // clean hammer bindings + if (this.navigationHammers.length != 0) { + for (var i = 0; i < this.navigationHammers.length; i++) { + this.navigationHammers[i].destroy(); + } + this.navigationHammers = []; + } + this._navigationReleaseOverload = function () {}; - /** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ - value: function onTouch(event) { - if (new Date().valueOf() - this.touchTime > 100) { - this.drag.pointer = this.getPointer(event.center); - this.drag.pinched = false; - this.pinch.scale = this.body.view.scale; - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { + this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); } + + this.iconsCreated = false; }, writable: true, configurable: true }, - onTap: { + loadNavigationElements: { /** - * handle tap/click event: select/unselect a node + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * * @private */ - value: function onTap(event) { - var pointer = this.getPointer(event.center); + value: function loadNavigationElements() { + this.cleanNavigation(); - var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; - var selected = this.selectionHandler.selectOnPoint(pointer); + this.navigationDOM = {}; + var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; + var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - if (selected === true || previouslySelected == true && selected === false) { - // select or unselect - this.body.emitter.emit("select", this.selectionHandler.getSelection()); + this.navigationDOM.wrapper = document.createElement("div"); + this.canvas.frame.appendChild(this.navigationDOM.wrapper); + + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDOM[navigationDivs[i]] = document.createElement("div"); + this.navigationDOM[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; + this.navigationDOM.wrapper.appendChild(this.navigationDOM[navigationDivs[i]]); + + var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); + if (navigationDivActions[i] == "_zoomExtent") { + hammerUtil.onTouch(hammer, this._zoomExtent.bind(this)); + } else { + hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); + } + + this.navigationHammers.push(hammer); } - this.selectionHandler._generateClickEvent("click", pointer); + this.iconsCreated = true; }, writable: true, configurable: true }, - onDoubleTap: { - - - /** - * handle doubletap event - * @private - */ - value: function onDoubleTap(event) { - var pointer = this.getPointer(event.center); - this.selectionHandler._generateClickEvent("doubleClick", pointer); + bindToRedraw: { + value: function bindToRedraw(action) { + if (this.boundFunctions[action] === undefined) { + this.boundFunctions[action] = this[action].bind(this); + this.body.emitter.on("initRedraw", this.boundFunctions[action]); + this.body.emitter.emit("_startRendering"); + } }, writable: true, configurable: true }, - onHold: { - - - - /** - * handle long tap event: multi select nodes - * @private - */ - value: function onHold(event) { - var pointer = this.getPointer(event.center); - - var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); - - if (selectionChanged === true) { - // select or longpress - this.body.emitter.emit("select", this.selectionHandler.getSelection()); + unbindFromRedraw: { + value: function unbindFromRedraw(action) { + if (this.boundFunctions[action] !== undefined) { + this.body.emitter.off("initRedraw", this.boundFunctions[action]); + this.body.emitter.emit("_stopRendering"); + delete this.boundFunctions[action]; } - - this.selectionHandler._generateClickEvent("click", pointer); }, writable: true, configurable: true }, - onRelease: { - + _zoomExtent: { /** - * handle the release of the screen + * this stops all movement induced by the navigation buttons * * @private */ - value: function onRelease(event) { - this.body.emitter.emit("release", event); + value: function _zoomExtent() { + if (new Date().valueOf() - this.touchTime > 700) { + // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) + this.body.emitter.emit("zoomExtent", { duration: 700 }); + this.touchTime = new Date().valueOf(); + } }, writable: true, configurable: true }, - onDragStart: { - + _stopMovement: { /** - * This function is called by onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. + * this stops all movement induced by the navigation buttons * * @private */ - value: function onDragStart(event) { - //in case the touch event was triggered on an external div, do the initial touch now. - if (this.drag.pointer === undefined) { - this.onTouch(event); - } - - // note: drag.pointer is set in onTouch to get the initial touch location - var node = this.selectionHandler.getNodeAt(this.drag.pointer); - - this.drag.dragging = true; - this.drag.selection = []; - this.drag.translation = util.extend({}, this.body.view.translation); // copy the object - this.drag.nodeId = undefined; - - this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); - - if (node !== undefined && this.options.dragNodes === true) { - this.drag.nodeId = node.id; - // select the clicked node if not yet selected - if (node.isSelected() === false) { - this.selectionHandler.unselectAll(); - this.selectionHandler.selectObject(node); - } - - var selection = this.selectionHandler.selectionObj.nodes; - // create an array with the selected nodes and their original location and status - for (var nodeId in selection) { - if (selection.hasOwnProperty(nodeId)) { - var object = selection[nodeId]; - var s = { - id: object.id, - node: object, - - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.options.fixed.x, - yFixed: object.options.fixed.y - }; - - object.options.fixed.x = true; - object.options.fixed.y = true; - - this.drag.selection.push(s); - } + value: function _stopMovement() { + for (var boundAction in this.boundFunctions) { + if (this.boundFunctions.hasOwnProperty(boundAction)) { + this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]); + this.body.emitter.emit("_stopRendering"); } } + this.boundFunctions = {}; }, writable: true, configurable: true }, - onDrag: { + _moveUp: { + value: function _moveUp() { + this.body.view.translation.y += this.options.keyboard.speed.y; + }, + writable: true, + configurable: true + }, + _moveDown: { + value: function _moveDown() { + this.body.view.translation.y -= this.options.keyboard.speed.y; + }, + writable: true, + configurable: true + }, + _moveLeft: { + value: function _moveLeft() { + this.body.view.translation.x += this.options.keyboard.speed.x; + }, + writable: true, + configurable: true + }, + _moveRight: { + value: function _moveRight() { + this.body.view.translation.x -= this.options.keyboard.speed.x; + }, + writable: true, + configurable: true + }, + _zoomIn: { + value: function _zoomIn() { + this.body.view.scale += this.options.keyboard.speed.zoom; + }, + writable: true, + configurable: true + }, + _zoomOut: { + value: function _zoomOut() { + this.body.view.scale -= this.options.keyboard.speed.zoom; + }, + writable: true, + configurable: true + }, + configureKeyboardBindings: { /** - * handle drag event - * @private + * bind all keys using keycharm. */ - value: function onDrag(event) { - var _this = this; - if (this.drag.pinched === true) { - return; + value: function configureKeyboardBindings() { + if (this.keycharm !== undefined) { + this.keycharm.destroy(); } - // remove the focus on node if it is focussed on by the focusOnNode - this.body.emitter.emit("unlockNode"); - - var pointer = this.getPointer(event.center); - var selection = this.drag.selection; - if (selection && selection.length && this.options.dragNodes === true) { - (function () { - // calculate delta's and new location - var deltaX = pointer.x - _this.drag.pointer.x; - var deltaY = pointer.y - _this.drag.pointer.y; + if (this.options.keyboard.enabled === true) { + if (this.options.keyboard.bindToWindow === true) { + this.keycharm = keycharm({ container: window, preventDefault: false }); + } else { + this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: false }); + } - // update position of all selected nodes - selection.forEach(function (selection) { - var node = selection.node; - // only move the node if it was not fixed initially - if (selection.xFixed === false) { - node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); - } - // only move the node if it was not fixed initially - if (selection.yFixed === false) { - node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); - } - }); + this.keycharm.reset(); - // start the simulation of the physics - _this.body.emitter.emit("startSimulation"); - })(); - } else { - // move the network - if (this.options.dragView === true) { - // if the drag was not started properly because the click started outside the network div, start it now. - if (this.drag.pointer === undefined) { - this._handleDragStart(event); - return; - } - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; + if (this.activated === true) { + this.keycharm.bind("up", this.bindToRedraw.bind(this, "_moveUp"), "keydown"); + this.keycharm.bind("down", this.bindToRedraw.bind(this, "_moveDown"), "keydown"); + this.keycharm.bind("left", this.bindToRedraw.bind(this, "_moveLeft"), "keydown"); + this.keycharm.bind("right", this.bindToRedraw.bind(this, "_moveRight"), "keydown"); + this.keycharm.bind("=", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("num+", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("num-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("[", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("]", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("pageup", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("pagedown", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; - this.body.emitter.emit("_redraw"); + this.keycharm.bind("up", this.unbindFromRedraw.bind(this, "_moveUp"), "keyup"); + this.keycharm.bind("down", this.unbindFromRedraw.bind(this, "_moveDown"), "keyup"); + this.keycharm.bind("left", this.unbindFromRedraw.bind(this, "_moveLeft"), "keyup"); + this.keycharm.bind("right", this.unbindFromRedraw.bind(this, "_moveRight"), "keyup"); + this.keycharm.bind("=", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("num+", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("num-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("[", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("]", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("pageup", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("pagedown", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); } } }, writable: true, configurable: true - }, - onDragEnd: { - - - /** - * handle drag start event - * @private - */ - value: function onDragEnd(event) { - this.drag.dragging = false; - var selection = this.drag.selection; - if (selection && selection.length) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.options.fixed.x = s.xFixed; - s.node.options.fixed.y = s.yFixed; - }); - this.body.emitter.emit("startSimulation"); - } else { - this.body.emitter.emit("_requestRedraw"); - } + } + }); - this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); - }, - writable: true, - configurable: true - }, - onPinch: { + return NavigationHandler; + })(); + module.exports = NavigationHandler; +/***/ }, +/* 76 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Handle pinch event - * @param event - * @private - */ - value: function onPinch(event) { - var pointer = this.getPointer(event.center); + "use strict"; - this.drag.pinched = true; - if (this.pinch.scale === undefined) { - this.pinch.scale = 1; - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.scale; - this.zoom(scale, pointer); - }, - writable: true, - configurable: true - }, - zoom: { + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + * @param {Object} [style] An object containing borderColor, + * backgroundColor, etc. + */ + var Popup = (function () { + function Popup(container, x, y, text, style) { + _classCallCheck(this, Popup); - /** - * 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 - */ - value: function zoom(scale, pointer) { - if (this.options.zoomView === true) { - var scaleOld = this.body.view.scale; - if (scale < 0.00001) { - scale = 0.00001; - } - if (scale > 10) { - scale = 10; - } + if (container) { + this.container = container; + } else { + this.container = document.body; + } - var preScaleDragPointer = undefined; - if (this.drag !== undefined) { - if (this.drag.dragging === true) { - preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); - } + // x, y and text are optional, see if a style object was passed in their place + if (style === undefined) { + if (typeof x === "object") { + style = x; + x = undefined; + } else if (typeof text === "object") { + style = text; + text = undefined; + } else { + // for backwards compatibility, in case clients other than Network are creating Popup directly + style = { + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" } - // + this.canvas.frame.canvas.clientHeight / 2 - var translation = this.body.view.translation; + }; + } + } - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + this.x = 0; + this.y = 0; + this.padding = 5; + this.hidden = false; - this.body.view.scale = scale; - this.body.view.translation = { x: tx, y: ty }; + if (x !== undefined && y !== undefined) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } - if (preScaleDragPointer != undefined) { - var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; - } + // create the frame + this.frame = document.createElement("div"); + this.frame.className = "network-tooltip"; + this.frame.style.color = style.fontColor; + this.frame.style.backgroundColor = style.color.background; + this.frame.style.borderColor = style.color.border; + this.frame.style.fontSize = style.fontSize + "px"; + this.frame.style.fontFamily = style.fontFace; + this.container.appendChild(this.frame); + } - this.body.emitter.emit("_requestRedraw"); + _prototypeProperties(Popup, null, { + setPosition: { - if (scaleOld < scale) { - this.body.emitter.emit("zoom", { direction: "+" }); - } else { - this.body.emitter.emit("zoom", { direction: "-" }); - } - } + /** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ + value: function setPosition(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); }, writable: true, configurable: true }, - onMouseWheel: { - - - /** - * 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 - */ - value: function onMouseWheel(event) { - // retrieve delta - var delta = 0; - if (event.wheelDelta) { - /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { - /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; - } - - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta !== 0) { - // calculate the new scale - var scale = this.body.view.scale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= 1 + zoom; - - // calculate the pointer location - var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + setText: { - // apply the new scale - this.zoom(scale, pointer); + /** + * Set the content for the popup window. This can be HTML code or text. + * @param {string | Element} content + */ + value: function setText(content) { + if (content instanceof Element) { + this.frame.innerHTML = ""; + this.frame.appendChild(content); + } else { + this.frame.innerHTML = content; // string containing text or HTML } - - // Prevent default actions caused by mouse wheel. - event.preventDefault(); }, writable: true, configurable: true }, - onMouseMove: { - + show: { /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private + * Show the popup window + * @param {boolean} show Optional. Show or hide the window */ - value: function onMouseMove(event) { - var _this = this; - var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); - var popupVisible = false; - - // check if the previously selected node is still selected - if (this.popup !== undefined) { - if (this.popup.hidden === false) { - this._checkHidePopup(pointer); - } - - // if the popup was not hidden above - if (this.popup.hidden === false) { - popupVisible = true; - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.show(); - } + value: function show(show) { + if (show === undefined) { + show = true; } - // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over. - if (this.options.keyboard.bindToWindow == false && this.options.keyboard.enabled === true) { - this.canvas.frame.focus(); - } + if (show === true) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; - // start a timeout that will check if the mouse is positioned above an element - if (popupVisible === false) { - if (this.popupTimer !== undefined) { - clearInterval(this.popupTimer); // stop any running calculationTimer - this.popupTimer = undefined; - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(function () { - return _this._checkShowPopup(pointer); - }, this.options.tooltip.delay); + var top = this.y - height; + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; } - } - - /** - * Adding hover highlights - */ - if (this.options.hoverEnabled === 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]; - } + if (top < this.padding) { + top = this.padding; } - // adding hover highlights - var obj = this.selectionHandler.getNodeAt(pointer); - if (obj == undefined) { - obj = this.selectionHandler.getEdgeAt(pointer); + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; } - if (obj != undefined) { - this.selectionHandler.hoverObject(obj); + if (left < this.padding) { + left = this.padding; } - // 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 == undefined) { - this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } - } - } - this.body.emitter.emit("_requestRedraw"); + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + this.hidden = false; + } else { + this.hide(); } }, writable: true, configurable: true }, - _checkShowPopup: { - - + hide: { /** - * 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 + * Hide the popup window */ - value: function _checkShowPopup(pointer) { - var x = this.canvas._XconvertDOMtoCanvas(pointer.x); - var y = this.canvas._YconvertDOMtoCanvas(pointer.y); - var pointerObj = { - left: x, - top: y, - right: x, - bottom: y - }; + value: function hide() { + this.hidden = true; + this.frame.style.visibility = "hidden"; + }, + writable: true, + configurable: true + } + }); - var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; - var nodeUnderCursor = false; - var popupType = "node"; + return Popup; + })(); - // check if a node is under the cursor. - if (this.popupObj === undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodeIndices = this.body.nodeIndices; - var nodes = this.body.nodes; - var node = undefined; - var overlappingNodes = []; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - if (node.isOverlappingWith(pointerObj) === true) { - if (node.getTitle() !== undefined) { - overlappingNodes.push(nodeIndices[i]); - } - } - } + module.exports = Popup; - if (overlappingNodes.length > 0) { - // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others - this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; - // if you hover over a node, the title of the edge is not supposed to be shown. - nodeUnderCursor = true; - } - } +/***/ }, +/* 77 */ +/***/ function(module, exports, __webpack_require__) { - if (this.popupObj === undefined && nodeUnderCursor == false) { - // search the edges for overlap - var edgeIndices = this.body.edgeIndices; - var edges = this.body.edges; - var edge = undefined; - var overlappingEdges = []; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.isOverlappingWith(pointerObj) === true) { - if (edge.connected === true && edge.getTitle() !== undefined) { - overlappingEdges.push(edgeIndices[i]); - } - } - } + "use strict"; - if (overlappingEdges.length > 0) { - this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; - popupType = "edge"; - } - } + // English + exports.en = { + edit: "Edit", + del: "Delete selected", + back: "Back", + addNode: "Add Node", + addEdge: "Add Edge", + editNode: "Edit Node", + editEdge: "Edit Edge", + addDescription: "Click in an empty space to place a new node.", + edgeDescription: "Click on a node and drag the edge to another node to connect them.", + editEdgeDescription: "Click on the control points and drag them to a node to connect to it.", + createEdgeError: "Cannot link edges to a cluster.", + deleteClusterError: "Clusters cannot be deleted.", + editClusterError: "Clusters cannot be edited." + }; + exports.en_EN = exports.en; + exports.en_US = exports.en; - if (this.popupObj !== undefined) { - // show popup message window - if (this.popupObj.id != previousPopupObjId) { - if (this.popup === undefined) { - this.popup = new Popup(this.frame, this.options.tooltip); - } + // Dutch + exports.nl = { + edit: "Wijzigen", + del: "Selectie verwijderen", + back: "Terug", + addNode: "Node toevoegen", + addEdge: "Link toevoegen", + editNode: "Node wijzigen", + editEdge: "Link wijzigen", + addDescription: "Klik op een leeg gebied om een nieuwe node te maken.", + edgeDescription: "Klik op een node en sleep de link naar een andere node om ze te verbinden.", + editEdgeDescription: "Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.", + createEdgeError: "Kan geen link maken naar een cluster.", + deleteClusterError: "Clusters kunnen niet worden verwijderd.", + editClusterError: "Clusters kunnen niet worden aangepast." + }; + exports.nl_NL = exports.nl; + exports.nl_BE = exports.nl; - this.popup.popupTargetType = popupType; - this.popup.popupTargetId = this.popupObj.id; +/***/ }, +/* 78 */ +/***/ function(module, exports, __webpack_require__) { - // 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 - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.setText(this.popupObj.getTitle()); - this.popup.show(); + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; + /** + * Created by Alex on 11/6/2014. + */ + + // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 + // if the module has no dependencies, the above pattern can be simplified to + (function (root, factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.keycharm = factory(); + } + }(this, function () { + + function keycharm(options) { + var preventDefault = options && options.preventDefault || false; + + var container = options && options.container || window; + var _exportFunctions = {}; + var _bound = {keydown:{}, keyup:{}}; + var _keys = {}; + var i; + + // a - z + for (i = 97; i <= 122; i++) {_keys[String.fromCharCode(i)] = {code:65 + (i - 97), shift: false};} + // A - Z + for (i = 65; i <= 90; i++) {_keys[String.fromCharCode(i)] = {code:i, shift: true};} + // 0 - 9 + for (i = 0; i <= 9; i++) {_keys['' + i] = {code:48 + i, shift: false};} + // F1 - F12 + for (i = 1; i <= 12; i++) {_keys['F' + i] = {code:111 + i, shift: false};} + // num0 - num9 + for (i = 0; i <= 9; i++) {_keys['num' + i] = {code:96 + i, shift: false};} + + // numpad misc + _keys['num*'] = {code:106, shift: false}; + _keys['num+'] = {code:107, shift: false}; + _keys['num-'] = {code:109, shift: false}; + _keys['num/'] = {code:111, shift: false}; + _keys['num.'] = {code:110, shift: false}; + // arrows + _keys['left'] = {code:37, shift: false}; + _keys['up'] = {code:38, shift: false}; + _keys['right'] = {code:39, shift: false}; + _keys['down'] = {code:40, shift: false}; + // extra keys + _keys['space'] = {code:32, shift: false}; + _keys['enter'] = {code:13, shift: false}; + _keys['shift'] = {code:16, shift: undefined}; + _keys['esc'] = {code:27, shift: false}; + _keys['backspace'] = {code:8, shift: false}; + _keys['tab'] = {code:9, shift: false}; + _keys['ctrl'] = {code:17, shift: false}; + _keys['alt'] = {code:18, shift: false}; + _keys['delete'] = {code:46, shift: false}; + _keys['pageup'] = {code:33, shift: false}; + _keys['pagedown'] = {code:34, shift: false}; + // symbols + _keys['='] = {code:187, shift: false}; + _keys['-'] = {code:189, shift: false}; + _keys[']'] = {code:221, shift: false}; + _keys['['] = {code:219, shift: false}; + + + + var down = function(event) {handleEvent(event,'keydown');}; + var up = function(event) {handleEvent(event,'keyup');}; + + // handle the actualy bound key with the event + var handleEvent = function(event,type) { + if (_bound[type][event.keyCode] !== undefined) { + var bound = _bound[type][event.keyCode]; + for (var i = 0; i < bound.length; i++) { + if (bound[i].shift === undefined) { + bound[i].fn(event); } - } else { - if (this.popup) { - this.popup.hide(); + else if (bound[i].shift == true && event.shiftKey == true) { + bound[i].fn(event); + } + else if (bound[i].shift == false && event.shiftKey == false) { + bound[i].fn(event); } } - }, - writable: true, - configurable: true - }, - _checkHidePopup: { + if (preventDefault == true) { + event.preventDefault(); + } + } + }; - /** - * Check if the popup must be hidden, which is the case when the mouse is no - * longer hovering on the object - * @param {{x:Number, y:Number}} pointer - * @private - */ - value: function _checkHidePopup(pointer) { - var x = this.canvas._XconvertDOMtoCanvas(pointer.x); - var y = this.canvas._YconvertDOMtoCanvas(pointer.y); - var pointerObj = { - left: x, - top: y, - right: x, - bottom: y - }; + // bind a key to a callback + _exportFunctions.bind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (_bound[type][_keys[key].code] === undefined) { + _bound[type][_keys[key].code] = []; + } + _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); + }; - var stillOnObj = false; - if (this.popup.popupTargetType == "node") { - if (this.body.nodes[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); - // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. - // we initially only check stillOnObj because this is much faster. - if (stillOnObj === true) { - var overNode = this.selectionHandler.getNodeAt(pointer); - stillOnObj = overNode.id == this.popup.popupTargetId; - } + // bind all keys to a call back (demo purposes) + _exportFunctions.bindAll = function(callback, type) { + if (type === undefined) { + type = 'keydown'; + } + for (var key in _keys) { + if (_keys.hasOwnProperty(key)) { + _exportFunctions.bind(key,callback,type); + } + } + }; + + // get the key label from an event + _exportFunctions.getKey = function(event) { + for (var key in _keys) { + if (_keys.hasOwnProperty(key)) { + if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { + return key; } - } else { - if (this.selectionHandler.getNodeAt(pointer) === undefined) { - if (this.body.edges[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { + return key; + } + else if (event.keyCode == _keys[key].code && key == 'shift') { + return key; + } + } + } + return "unknown key, currently not supported"; + }; + + // unbind either a specific callback from a key or all of them (by leaving callback undefined) + _exportFunctions.unbind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (callback !== undefined) { + var newBindings = []; + var bound = _bound[type][_keys[key].code]; + if (bound !== undefined) { + for (var i = 0; i < bound.length; i++) { + if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { + newBindings.push(_bound[type][_keys[key].code][i]); } } } + _bound[type][_keys[key].code] = newBindings; + } + else { + _bound[type][_keys[key].code] = []; + } + }; + // reset all bound variables. + _exportFunctions.reset = function() { + _bound = {keydown:{}, keyup:{}}; + }; + + // unbind all listeners and reset all variables. + _exportFunctions.destroy = function() { + _bound = {keydown:{}, keyup:{}}; + container.removeEventListener('keydown', down, true); + container.removeEventListener('keyup', up, true); + }; + + // create listeners. + container.addEventListener('keydown',down,true); + container.addEventListener('keyup',up,true); + + // return the public functions. + return _exportFunctions; + } + + return keycharm; + })); - if (stillOnObj === false) { - this.popupObj = undefined; - this.popup.hide(); - } - }, - writable: true, - configurable: true - } - }); - return InteractionHandler; - })(); - module.exports = InteractionHandler; /***/ }, -/* 99 */ +/* 79 */ +/***/ function(module, exports, __webpack_require__) { + + function webpackContext(req) { + throw new Error("Cannot find module '" + req + "'."); + } + webpackContext.keys = function() { return []; }; + webpackContext.resolve = webpackContext; + module.exports = webpackContext; + webpackContext.id = 79; + + +/***/ }, +/* 80 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var util = __webpack_require__(1); - var Hammer = __webpack_require__(19); - var hammerUtil = __webpack_require__(24); - var keycharm = __webpack_require__(39); - - var NavigationHandler = (function () { - function NavigationHandler(body, canvas) { - var _this = this; - _classCallCheck(this, NavigationHandler); - this.body = body; - this.canvas = canvas; + /** + * Created by Alex on 3/17/2015. + */ - this.iconsCreated = false; - this.navigationHammers = []; - this.boundFunctions = {}; - this.touchTime = 0; - this.activated = false; + var Label = (function () { + function Label(body, options) { + _classCallCheck(this, Label); + this.body = body; - this.body.emitter.on("release", this._stopMovement.bind(this)); - this.body.emitter.on("activate", function () { - _this.activated = true;_this.configureKeyboardBindings(); - }); - this.body.emitter.on("deactivate", function () { - _this.activated = false;_this.configureKeyboardBindings(); - }); - this.body.emitter.on("destroy", function () { - if (_this.keycharm !== undefined) { - _this.keycharm.destroy(); - } - }); + this.fontOptions = {}; + this.defaultOptions = { + color: "#343434", + size: 14, // px + face: "arial", + background: "none", + stroke: 0, // px + strokeColor: "white", + align: "horizontal" + }; + util.extend(this.fontOptions, this.defaultOptions); - this.options = {}; + this.setOptions(options); + this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached } - _prototypeProperties(NavigationHandler, null, { + _prototypeProperties(Label, null, { setOptions: { value: function setOptions(options) { - if (options !== undefined) { - this.options = options; - this.create(); + this.options = options; + if (options.label !== undefined) { + this.labelDirty = true; + } + if (options.font) { + if (typeof options.font === "string") { + var optionsArray = options.font.split(" "); + this.fontOptions.size = optionsArray[0].replace("px", ""); + this.fontOptions.face = optionsArray[1]; + this.fontOptions.color = optionsArray[2]; + } else if (typeof options.font == "object") { + util.protoExtend(this.fontOptions, options.font); + } + this.fontOptions.size = Number(this.fontOptions.size); } }, writable: true, configurable: true }, - create: { - value: function create() { - if (this.options.showNavigationIcons === true) { - if (this.iconsCreated === false) { - this.loadNavigationElements(); - } - } else if (this.iconsCreated === true) { - this.cleanNavigation(); - } + draw: { - this.configureKeyboardBindings(); + + /** + * Main function. This is called from anything that wants to draw a label. + * @param ctx + * @param x + * @param y + * @param selected + * @param baseline + */ + value: function draw(ctx, x, y, selected) { + var baseline = arguments[4] === undefined ? "middle" : arguments[4]; + // if no label, return + if (this.options.label === undefined) { + return; + } // check if we have to render the label + var viewFontSize = this.fontOptions.size * this.body.view.scale; + if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) { + return; + } // update the size cache if required + this.calculateLabelSize(ctx, selected, x, y, baseline); + + // create the fontfill background + this._drawBackground(ctx); + // draw text + this._drawText(ctx, selected, x, y, baseline); }, writable: true, configurable: true }, - cleanNavigation: { - value: function cleanNavigation() { - // clean hammer bindings - if (this.navigationHammers.length != 0) { - for (var i = 0; i < this.navigationHammers.length; i++) { - this.navigationHammers[i].destroy(); - } - this.navigationHammers = []; - } + _drawBackground: { - this._navigationReleaseOverload = function () {}; + /** + * Draws the label background + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawBackground(ctx) { + if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") { + ctx.fillStyle = this.fontOptions.background; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { - this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); - } + var lineMargin = 2; - this.iconsCreated = false; + switch (this.fontOptions.align) { + case "middle": + ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); + break; + case "top": + ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); + break; + case "bottom": + ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); + break; + default: + ctx.fillRect(this.size.left, this.size.top, this.size.width, this.size.height); + break; + } + } }, writable: true, configurable: true }, - loadNavigationElements: { + _drawText: { + /** - * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation - * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent - * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. - * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. * + * @param ctx + * @param x + * @param baseline * @private */ - value: function loadNavigationElements() { - this.cleanNavigation(); + value: function _drawText(ctx, selected, x, y) { + var baseline = arguments[4] === undefined ? "middle" : arguments[4]; + var fontSize = this.fontOptions.size; + var viewFontSize = fontSize * this.body.view.scale; + // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) + if (viewFontSize >= this.options.scaling.label.maxVisible) { + fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; + } - this.navigationDOM = {}; - var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; - var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; + var yLine = this.size.yLine; + var _getColor = this._getColor(viewFontSize); - this.navigationDOM.wrapper = document.createElement("div"); - this.canvas.frame.appendChild(this.navigationDOM.wrapper); + var _getColor2 = _slicedToArray(_getColor, 2); - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDOM[navigationDivs[i]] = document.createElement("div"); - this.navigationDOM[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; - this.navigationDOM.wrapper.appendChild(this.navigationDOM[navigationDivs[i]]); + var fontColor = _getColor2[0]; + var strokeColor = _getColor2[1]; + var _ref = this._setAlignment(ctx, x, yLine, baseline); - var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); - if (navigationDivActions[i] == "_zoomExtent") { - hammerUtil.onTouch(hammer, this._zoomExtent.bind(this)); - } else { - hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); - } + var _ref2 = _slicedToArray(_ref, 2); - this.navigationHammers.push(hammer); + x = _ref2[0]; + yLine = _ref2[1]; + + + // configure context for drawing the text + ctx.font = (selected ? "bold " : "") + fontSize + "px " + this.fontOptions.face; + ctx.fillStyle = fontColor; + ctx.textAlign = "center"; + + // set the strokeWidth + if (this.fontOptions.stroke > 0) { + ctx.lineWidth = this.fontOptions.stroke; + ctx.strokeStyle = strokeColor; + ctx.lineJoin = "round"; } - this.iconsCreated = true; + // draw the text + for (var i = 0; i < this.lineCount; i++) { + if (this.fontOptions.stroke > 0) { + ctx.strokeText(this.lines[i], x, yLine); + } + ctx.fillText(this.lines[i], x, yLine); + yLine += fontSize; + } }, writable: true, configurable: true }, - bindToRedraw: { - value: function bindToRedraw(action) { - if (this.boundFunctions[action] === undefined) { - this.boundFunctions[action] = this[action].bind(this); - this.body.emitter.on("initRedraw", this.boundFunctions[action]); - this.body.emitter.emit("_startRendering"); + _setAlignment: { + value: function _setAlignment(ctx, x, yLine, baseline) { + // check for label alignment (for edges) + // TODO: make alignment for nodes + if (this.fontOptions.align !== "horizontal") { + x = 0; + yLine = 0; + + var lineMargin = 2; + if (this.fontOptions.align === "top") { + ctx.textBaseline = "alphabetic"; + yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers + } else if (this.fontOptions.align === "bottom") { + ctx.textBaseline = "hanging"; + yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers + } else { + ctx.textBaseline = "middle"; + } + } else { + ctx.textBaseline = baseline; } + + return [x, yLine]; }, writable: true, configurable: true }, - unbindFromRedraw: { - value: function unbindFromRedraw(action) { - if (this.boundFunctions[action] !== undefined) { - this.body.emitter.off("initRedraw", this.boundFunctions[action]); - this.body.emitter.emit("_stopRendering"); - delete this.boundFunctions[action]; + _getColor: { + + /** + * fade in when relative scale is between threshold and threshold - 1. + * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. + * + * @param viewFontSize + * @returns {*[]} + * @private + */ + value: function _getColor(viewFontSize) { + var fontColor = this.fontOptions.color || "#000000"; + var strokeColor = this.fontOptions.strokeColor || "#ffffff"; + if (viewFontSize <= this.options.scaling.label.drawThreshold) { + var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); + fontColor = util.overrideOpacity(fontColor, opacity); + strokeColor = util.overrideOpacity(strokeColor, opacity); } + return [fontColor, strokeColor]; }, writable: true, configurable: true }, - _zoomExtent: { + getTextSize: { + /** - * this stops all movement induced by the navigation buttons * - * @private + * @param ctx + * @param selected + * @returns {{width: number, height: number}} */ - value: function _zoomExtent() { - if (new Date().valueOf() - this.touchTime > 700) { - // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) - this.body.emitter.emit("zoomExtent", { duration: 700 }); - this.touchTime = new Date().valueOf(); - } + value: function getTextSize(ctx) { + var selected = arguments[1] === undefined ? false : arguments[1]; + var size = { + width: this._processLabel(ctx, selected), + height: this.fontOptions.size * this.lineCount + }; + return size; }, writable: true, configurable: true }, - _stopMovement: { + calculateLabelSize: { + /** - * this stops all movement induced by the navigation buttons * + * @param ctx + * @param selected + * @param x + * @param y + * @param baseline + */ + value: function calculateLabelSize(ctx, selected) { + var x = arguments[2] === undefined ? 0 : arguments[2]; + var y = arguments[3] === undefined ? 0 : arguments[3]; + var baseline = arguments[4] === undefined ? "middle" : arguments[4]; + if (this.labelDirty === true) { + this.size.width = this._processLabel(ctx, selected); + } + this.size.height = this.fontOptions.size * this.lineCount; + this.size.left = x - this.size.width * 0.5; + this.size.top = y - this.size.height * 0.5; + this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; + if (baseline == "hanging") { + this.size.top += 0.5 * this.fontOptions.size; + this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers + this.size.yLine += 4; // distance from node + } + + this.labelDirty = false; + }, + writable: true, + configurable: true + }, + _processLabel: { + + + /** + * This calculates the width as well as explodes the label string and calculates the amount of lines. + * @param ctx + * @param selected + * @returns {number} * @private */ - value: function _stopMovement() { - for (var boundAction in this.boundFunctions) { - if (this.boundFunctions.hasOwnProperty(boundAction)) { - this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]); - this.body.emitter.emit("_stopRendering"); + value: function _processLabel(ctx, selected) { + var width = 0; + var lines = [""]; + var lineCount = 0; + if (this.options.label !== undefined) { + lines = String(this.options.label).split("\n"); + lineCount = lines.length; + ctx.font = (selected ? "bold " : "") + this.fontOptions.size + "px " + this.fontOptions.face; + width = ctx.measureText(lines[0]).width; + for (var i = 1; i < lineCount; i++) { + var lineWidth = ctx.measureText(lines[i]).width; + width = lineWidth > width ? lineWidth : width; } } - this.boundFunctions = {}; + this.lines = lines; + this.lineCount = lineCount; + + return width; }, writable: true, configurable: true - }, - _moveUp: { - value: function _moveUp() { - this.body.view.translation.y += this.options.keyboard.speed.y; + } + }); + + return Label; + })(); + + module.exports = Label; + +/***/ }, +/* 81 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var NodeBase = _interopRequire(__webpack_require__(100)); + + var Box = (function (NodeBase) { + function Box(options, body, labelModule) { + _classCallCheck(this, Box); + + _get(Object.getPrototypeOf(Box.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Box, NodeBase); + + _prototypeProperties(Box, null, { + resize: { + value: function resize(ctx) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, this.selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } }, writable: true, configurable: true }, - _moveDown: { - value: function _moveDown() { - this.body.view.translation.y -= this.options.keyboard.speed.y; + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + + ctx.roundRect(this.left, this.top, this.width, this.height, this.options.size); + ctx.fill(); + ctx.stroke(); + + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; + + this.labelModule.draw(ctx, x, y, selected); }, writable: true, configurable: true }, - _moveLeft: { - value: function _moveLeft() { - this.body.view.translation.x += this.options.keyboard.speed.x; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); }, writable: true, configurable: true - }, - _moveRight: { - value: function _moveRight() { - this.body.view.translation.x -= this.options.keyboard.speed.x; + } + }); + + return Box; + })(NodeBase); + + module.exports = Box; + +/***/ }, +/* 82 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var CircleImageBase = _interopRequire(__webpack_require__(101)); + + var Circle = (function (CircleImageBase) { + function Circle(options, body, labelModule) { + _classCallCheck(this, Circle); + + _get(Object.getPrototypeOf(Circle.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Circle, CircleImageBase); + + _prototypeProperties(Circle, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.options.size = diameter / 2; + + this.width = diameter; + this.height = diameter; + } }, writable: true, configurable: true }, - _zoomIn: { - value: function _zoomIn() { - this.body.view.scale += this.options.keyboard.speed.zoom; + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); + + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + + this.labelModule.draw(ctx, x, y, selected); }, writable: true, configurable: true }, - _zoomOut: { - value: function _zoomOut() { - this.body.view.scale -= this.options.keyboard.speed.zoom; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + }, + writable: true, + configurable: true + } + }); + + return Circle; + })(CircleImageBase); + + module.exports = Circle; + +/***/ }, +/* 83 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var CircleImageBase = _interopRequire(__webpack_require__(101)); + + var CircularImage = (function (CircleImageBase) { + function CircularImage(options, body, labelModule, imageObj) { + _classCallCheck(this, CircularImage); + + _get(Object.getPrototypeOf(CircularImage.prototype), "constructor", this).call(this, options, body, labelModule); + this.imageObj = imageObj; + } + + _inherits(CircularImage, CircleImageBase); + + _prototypeProperties(CircularImage, null, { + resize: { + value: function resize(ctx) { + if (this.imageObj.src !== undefined || this.imageObj.width !== undefined || this.imageObj.height !== undefined) { + if (!this.width) { + var diameter = this.options.size * 2; + this.width = diameter; + this.height = diameter; + this._swapToImageResizeWhenImageLoaded = true; + } + } else { + if (this._swapToImageResizeWhenImageLoaded) { + this.width = 0; + this.height = 0; + delete this._swapToImageResizeWhenImageLoaded; + } + this._resizeImage(ctx); + } }, writable: true, configurable: true }, - configureKeyboardBindings: { + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - /** - * bind all keys using keycharm. - */ - value: function configureKeyboardBindings() { - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } + var size = Math.abs(this.height / 2); + this._drawRawCircle(ctx, x, y, selected, hover, size); - if (this.options.keyboard.enabled === true) { - if (this.options.keyboard.bindToWindow === true) { - this.keycharm = keycharm({ container: window, preventDefault: false }); - } else { - this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: false }); - } + ctx.save(); + ctx.circle(x, y, size); + ctx.stroke(); + ctx.clip(); - this.keycharm.reset(); + this._drawImageAtPosition(ctx); - if (this.activated === true) { - this.keycharm.bind("up", this.bindToRedraw.bind(this, "_moveUp"), "keydown"); - this.keycharm.bind("down", this.bindToRedraw.bind(this, "_moveDown"), "keydown"); - this.keycharm.bind("left", this.bindToRedraw.bind(this, "_moveLeft"), "keydown"); - this.keycharm.bind("right", this.bindToRedraw.bind(this, "_moveRight"), "keydown"); - this.keycharm.bind("=", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("num+", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("num-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("[", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("]", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("pageup", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("pagedown", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + ctx.restore(); - this.keycharm.bind("up", this.unbindFromRedraw.bind(this, "_moveUp"), "keyup"); - this.keycharm.bind("down", this.unbindFromRedraw.bind(this, "_moveDown"), "keyup"); - this.keycharm.bind("left", this.unbindFromRedraw.bind(this, "_moveLeft"), "keyup"); - this.keycharm.bind("right", this.unbindFromRedraw.bind(this, "_moveRight"), "keyup"); - this.keycharm.bind("=", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("num+", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("num-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("[", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("]", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("pageup", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("pagedown", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - } - } + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + + this._drawImageLabel(ctx, x, y, selected); + + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); }, writable: true, configurable: true } }); - return NavigationHandler; - })(); + return CircularImage; + })(CircleImageBase); - module.exports = NavigationHandler; + module.exports = CircularImage; /***/ }, -/* 100 */ +/* 84 */ /***/ function(module, exports, __webpack_require__) { + /** + * Created by Alex on 3/18/2015. + */ "use strict"; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * Popup is a class to create a popup window with some text - * @param {Element} container The container object. - * @param {Number} [x] - * @param {Number} [y] - * @param {String} [text] - * @param {Object} [style] An object containing borderColor, - * backgroundColor, etc. - */ - var Popup = (function () { - function Popup(container, x, y, text, style) { - _classCallCheck(this, Popup); + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - if (container) { - this.container = container; - } else { - this.container = document.body; - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - // x, y and text are optional, see if a style object was passed in their place - if (style === undefined) { - if (typeof x === "object") { - style = x; - x = undefined; - } else if (typeof text === "object") { - style = text; - text = undefined; - } else { - // for backwards compatibility, in case clients other than Network are creating Popup directly - style = { - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - color: { - border: "#666", - background: "#FFFFC6" - } - }; - } - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - this.x = 0; - this.y = 0; - this.padding = 5; - this.hidden = false; + var NodeBase = _interopRequire(__webpack_require__(100)); - if (x !== undefined && y !== undefined) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); - } + var Database = (function (NodeBase) { + function Database(options, body, labelModule) { + _classCallCheck(this, Database); - // create the frame - this.frame = document.createElement("div"); - this.frame.className = "network-tooltip"; - this.frame.style.color = style.fontColor; - this.frame.style.backgroundColor = style.color.background; - this.frame.style.borderColor = style.color.border; - this.frame.style.fontSize = style.fontSize + "px"; - this.frame.style.fontFamily = style.fontFace; - this.container.appendChild(this.frame); + _get(Object.getPrototypeOf(Database.prototype), "constructor", this).call(this, options, body, labelModule); } - _prototypeProperties(Popup, null, { - setPosition: { - - /** - * @param {number} x Horizontal position of the popup window - * @param {number} y Vertical position of the popup window - */ - value: function setPosition(x, y) { - this.x = parseInt(x); - this.y = parseInt(y); - }, - writable: true, - configurable: true - }, - setText: { + _inherits(Database, NodeBase); - /** - * Set the content for the popup window. This can be HTML code or text. - * @param {string | Element} content - */ - value: function setText(content) { - if (content instanceof Element) { - this.frame.innerHTML = ""; - this.frame.appendChild(content); - } else { - this.frame.innerHTML = content; // string containing text or HTML + _prototypeProperties(Database, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; } }, writable: true, configurable: true }, - show: { + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - /** - * Show the popup window - * @param {boolean} show Optional. Show or hide the window - */ - value: function show(show) { - if (show === undefined) { - show = true; - } + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - if (show === true) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - var top = this.y - height; - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; - } + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; - } - if (left < this.padding) { - left = this.padding; - } + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - this.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; - this.hidden = false; - } else { - this.hide(); - } + this.labelModule.draw(ctx, x, y, selected); }, writable: true, configurable: true }, - hide: { - - /** - * Hide the popup window - */ - value: function hide() { - this.hidden = true; - this.frame.style.visibility = "hidden"; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); }, writable: true, configurable: true } }); - return Popup; - })(); + return Database; + })(NodeBase); - module.exports = Popup; + module.exports = Database; /***/ }, -/* 101 */ +/* 85 */ /***/ function(module, exports, __webpack_require__) { + /** + * Created by Alex on 3/18/2015. + */ "use strict"; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * Created by Alex on 2/27/2015. - */ + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - var Node = __webpack_require__(60); - var util = __webpack_require__(1); + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var SelectionHandler = (function () { - function SelectionHandler(body, canvas) { - var _this = this; - _classCallCheck(this, SelectionHandler); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - this.body = body; - this.canvas = canvas; - this.selectionObj = { nodes: [], edges: [] }; - this.forceSelectEdges = false; + var ShapeBase = _interopRequire(__webpack_require__(102)); - this.options = {}; - this.defaultOptions = { - select: true, - selectConnectedEdges: true - }; - util.extend(this.options, this.defaultOptions); + var Diamond = (function (ShapeBase) { + function Diamond(options, body, labelModule) { + _classCallCheck(this, Diamond); - this.body.emitter.on("_dataChanged", function () { - _this.updateSelection(); - }); + _get(Object.getPrototypeOf(Diamond.prototype), "constructor", this).call(this, options, body, labelModule); } - _prototypeProperties(SelectionHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); - } + _inherits(Diamond, ShapeBase); + + _prototypeProperties(Diamond, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); }, writable: true, configurable: true }, - selectOnPoint: { - - - - /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private - */ - value: function selectOnPoint(pointer) { - var selected = false; - if (this.options.select === true) { - this.unselectAll(); - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - if (obj !== undefined) { - selected = this.selectObject(obj); - } - this.body.emitter.emit("_requestRedraw"); - } - return selected; + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "diamond", 4, x, y, selected, hover); }, writable: true, configurable: true }, - selectAdditionalOnPoint: { - value: function selectAdditionalOnPoint(pointer) { - var selectionChanged = false; - if (this.options.select === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - - if (obj !== undefined) { - selectionChanged = true; - if (obj.isSelected() === true) { - this.deselectObject(obj); - } else { - this.selectObject(obj); - } - - this.body.emitter.emit("_requestRedraw"); - } - } - return selectionChanged; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); }, writable: true, configurable: true - }, - _generateClickEvent: { - value: function _generateClickEvent(eventType, pointer) { - var properties = this.getSelection(); - properties.pointer = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: this.canvas.DOMtoCanvas(pointer) - }; - this.body.emitter.emit(eventType, properties); + } + }); + + return Diamond; + })(ShapeBase); + + module.exports = Diamond; + +/***/ }, +/* 86 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var ShapeBase = _interopRequire(__webpack_require__(102)); + + var Dot = (function (ShapeBase) { + function Dot(options, body, labelModule) { + _classCallCheck(this, Dot); + + _get(Object.getPrototypeOf(Dot.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Dot, ShapeBase); + + _prototypeProperties(Dot, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); }, writable: true, configurable: true }, - selectObject: { - value: function selectObject(obj) { - if (obj !== undefined) { - if (obj instanceof Node) { - if (this.options.selectConnectedEdges === true || this.forceSelectEdges === true) { - this._selectConnectedEdges(obj); - } - } - obj.select(); - this._addToSelection(obj); - return true; - } - return false; + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "circle", 2, x, y, selected, hover); }, writable: true, configurable: true }, - deselectObject: { - value: function deselectObject(obj) { - if (obj.isSelected() === true) { - obj.selected = false; - this._removeFromSelection(obj); - } + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this.options.size + this.options.borderWidth; }, writable: true, configurable: true - }, - _getAllNodesOverlappingWith: { + } + }); + return Dot; + })(ShapeBase); + module.exports = Dot; - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getAllNodesOverlappingWith(object) { - var overlappingNodes = []; - var nodes = this.body.nodes; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); +/***/ }, +/* 87 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var NodeBase = _interopRequire(__webpack_require__(100)); + + var Ellipse = (function (NodeBase) { + function Ellipse(options, body, labelModule) { + _classCallCheck(this, Ellipse); + + _get(Object.getPrototypeOf(Ellipse.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Ellipse, NodeBase); + + _prototypeProperties(Ellipse, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var textSize = this.labelModule.getTextSize(ctx, selected); + + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; } } - return overlappingNodes; }, writable: true, configurable: true }, - _pointerToPositionObject: { + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - value: function _pointerToPositionObject(pointer) { - var canvasPos = this.canvas.DOMtoCanvas(pointer); - return { - left: canvasPos.x - 1, - top: canvasPos.y + 1, - right: canvasPos.x + 1, - bottom: canvasPos.y - 1 - }; - }, - writable: true, - configurable: true - }, - getNodeAt: { + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | undefined} node - * @private - */ - value: function getNodeAt(pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.ellipse(this.left, this.top, this.width, this.height); + ctx.fill(); + ctx.stroke(); - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } else { - return undefined; - } - }, - writable: true, - configurable: true - }, - _getEdgesOverlappingWith: { + this.boundingBox.left = this.left; + this.boundingBox.top = this.top; + this.boundingBox.bottom = this.top + this.height; + this.boundingBox.right = this.left + this.width; - /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getEdgesOverlappingWith(object, overlappingEdges) { - var edges = this.body.edges; - for (var i = 0; i < this.body.edgeIndices.length; i++) { - var edgeId = this.body.edgeIndices[i]; - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); - } - } + this.labelModule.draw(ctx, x, y, selected); }, writable: true, configurable: true }, - _getAllEdgesOverlappingWith: { - - - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getAllEdgesOverlappingWith(object) { - var overlappingEdges = []; - this._getEdgesOverlappingWith(object, overlappingEdges); - return overlappingEdges; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); }, writable: true, configurable: true - }, - getEdgeAt: { + } + }); + return Ellipse; + })(NodeBase); - /** - * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call - * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. - * - * @param pointer - * @returns {undefined} - * @private - */ - value: function getEdgeAt(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); + module.exports = Ellipse; - if (overlappingEdges.length > 0) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; - } else { - return undefined; - } - }, - writable: true, - configurable: true - }, - _addToSelection: { +/***/ }, +/* 88 */ +/***/ function(module, exports, __webpack_require__) { + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - value: function _addToSelection(obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } else { - this.selectionObj.edges[obj.id] = obj; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var NodeBase = _interopRequire(__webpack_require__(100)); + + var Icon = (function (NodeBase) { + function Icon(options, body, labelModule) { + _classCallCheck(this, Icon); + + _get(Object.getPrototypeOf(Icon.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Icon, NodeBase); + + _prototypeProperties(Icon, null, { + resize: { + value: function resize(ctx) { + if (this.width === undefined) { + var margin = 5; + var iconSize = { + width: Number(this.options.icon.size), + height: Number(this.options.icon.size) + }; + this.width = iconSize.width + 2 * margin; + this.height = iconSize.height + 2 * margin; } }, writable: true, configurable: true }, - _addToHover: { + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.options.icon.size = this.options.icon.size || 50; - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - value: function _addToHover(obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; - } else { - this.hoverObj.edges[obj.id] = obj; + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; + this._icon(ctx, x, y, selected); + + + this.boundingBox.top = y - this.options.icon.size * 0.5; + this.boundingBox.left = x - this.options.icon.size * 0.5; + this.boundingBox.right = x + this.options.icon.size * 0.5; + this.boundingBox.bottom = y + this.options.icon.size * 0.5; + + if (this.options.label !== undefined) { + var iconTextSpacing = 5; + this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); } }, writable: true, configurable: true }, - _removeFromSelection: { + _icon: { + value: function _icon(ctx, x, y, selected) { + var iconSize = Number(this.options.icon.size); + var relativeIconSize = iconSize * this.body.view.scale; + if (this.options.icon.code && relativeIconSize > this.options.scaling.label.drawThreshold - 1) { + ctx.font = (selected ? "bold " : "") + iconSize + "px " + this.options.icon.face; - /** - * Remove a single option from selection. - * - * @param {Object} obj - * @private - */ - value: function _removeFromSelection(obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } else { - delete this.selectionObj.edges[obj.id]; + // draw icon + ctx.fillStyle = this.options.icon.color || "black"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.options.icon.code, x, y); } }, writable: true, configurable: true }, - unselectAll: { - - /** - * Unselect all. The selectionObj is useful for this. - * - * @private - */ - value: function unselectAll() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); - } - } - - this.selectionObj = { nodes: {}, edges: {} }; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + this._distanceToBorder(angle); }, writable: true, configurable: true - }, - _getSelectedNodeCount: { + } + }); + return Icon; + })(NodeBase); - /** - * return the number of selected nodes - * - * @returns {number} - * @private - */ - value: function _getSelectedNodeCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; + module.exports = Icon; + +/***/ }, +/* 89 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var CircleImageBase = _interopRequire(__webpack_require__(101)); + + var Image = (function (CircleImageBase) { + function Image(options, body, labelModule, imageObj) { + _classCallCheck(this, Image); + + _get(Object.getPrototypeOf(Image.prototype), "constructor", this).call(this, options, body, labelModule); + this.imageObj = imageObj; + } + + _inherits(Image, CircleImageBase); + + _prototypeProperties(Image, null, { + resize: { + value: function resize() { + if (!this.width || !this.height) { + // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + if (scale !== undefined) { + width = this.options.size || this.imageObj.width; + height = this.options.size * scale || this.imageObj.height; + } else { + width = 0; + height = 0; + } + } else { + width = this.imageObj.width; + height = this.imageObj.height; } + this.width = width; + this.height = height; } - return count; }, writable: true, configurable: true }, - _getSelectedNode: { + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - /** - * return the selected node - * - * @returns {number} - * @private - */ - value: function _getSelectedNode() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } - } - return undefined; + this._drawImageAtPosition(ctx); + + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; + + this._drawImageLabel(ctx, x, y, selected || hover); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); }, writable: true, configurable: true }, - _getSelectedEdge: { - - /** - * return the selected edge - * - * @returns {number} - * @private - */ - value: function _getSelectedEdge() { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; - } - } - return undefined; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); }, writable: true, configurable: true - }, - _getSelectedEdgeCount: { + } + }); + return Image; + })(CircleImageBase); - /** - * return the number of selected edges - * - * @returns {number} - * @private - */ - value: function _getSelectedEdgeCount() { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } - } - return count; + module.exports = Image; + +/***/ }, +/* 90 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var ShapeBase = _interopRequire(__webpack_require__(102)); + + var Square = (function (ShapeBase) { + function Square(options, body, labelModule) { + _classCallCheck(this, Square); + + _get(Object.getPrototypeOf(Square.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Square, ShapeBase); + + _prototypeProperties(Square, null, { + resize: { + value: function resize() { + this._resizeShape(); }, writable: true, configurable: true }, - _getSelectedObjectCount: { - - - /** - * return the number of selected objects. - * - * @returns {number} - * @private - */ - value: function _getSelectedObjectCount() { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } - } - return count; + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "square", 2, x, y, selected, hover); }, writable: true, configurable: true }, - _selectionIsEmpty: { - - /** - * Check if anything is selected - * - * @returns {boolean} - * @private - */ - value: function _selectionIsEmpty() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; - } - } - return true; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); }, writable: true, configurable: true - }, - _clusterInSelection: { + } + }); + + return Square; + })(ShapeBase); + + module.exports = Square; + +/***/ }, +/* 91 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - value: function _clusterInSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; - } - } - } - return false; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var ShapeBase = _interopRequire(__webpack_require__(102)); + + var Star = (function (ShapeBase) { + function Star(options, body, labelModule) { + _classCallCheck(this, Star); + + _get(Object.getPrototypeOf(Star.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Star, ShapeBase); + + _prototypeProperties(Star, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); }, writable: true, configurable: true }, - _selectConnectedEdges: { - - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _selectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.select(); - this._addToSelection(edge); - } + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "star", 4, x, y, selected, hover); }, writable: true, configurable: true }, - _hoverConnectedEdges: { - - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _hoverConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.hover = true; - this._addToHover(edge); - } + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); }, writable: true, configurable: true - }, - _unselectConnectedEdges: { + } + }); + return Star; + })(ShapeBase); - /** - * unselect the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - value: function _unselectConnectedEdges(node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.unselect(); - this._removeFromSelection(edge); - } - }, - writable: true, - configurable: true - }, - blurObject: { + module.exports = Star; + +/***/ }, +/* 92 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - value: function blurObject(object) { - if (object.hover == true) { - object.hover = false; - this.body.emitter.emit("blurNode", { node: object.id }); + var NodeBase = _interopRequire(__webpack_require__(100)); + + var Text = (function (NodeBase) { + function Text(options, body, labelModule) { + _classCallCheck(this, Text); + + _get(Object.getPrototypeOf(Text.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Text, NodeBase); + + _prototypeProperties(Text, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; } }, writable: true, configurable: true }, - hoverObject: { + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected || hover); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @private - */ - value: function hoverObject(object) { - if (object.hover == false) { - object.hover = true; - this._addToHover(object); - if (object instanceof Node) { - this.body.emitter.emit("hoverNode", { node: object.id }); - } - } - if (object instanceof Node) { - this._hoverConnectedEdges(object); - } + this.labelModule.draw(ctx, x, y, selected || hover); + + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; }, writable: true, configurable: true }, - getSelection: { + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true + } + }); + return Text; + })(NodeBase); + module.exports = Text; +/***/ }, +/* 93 */ +/***/ function(module, exports, __webpack_require__) { - /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection - */ - value: function getSelection() { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return { nodes: nodeIds, edges: edgeIds }; + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var ShapeBase = _interopRequire(__webpack_require__(102)); + + var Triangle = (function (ShapeBase) { + function Triangle(options, body, labelModule) { + _classCallCheck(this, Triangle); + + _get(Object.getPrototypeOf(Triangle.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Triangle, ShapeBase); + + _prototypeProperties(Triangle, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); }, writable: true, configurable: true }, - getSelectedNodes: { - - /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. - */ - value: function getSelectedNodes() { - var idArray = []; - if (this.options.select == true) { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); - } - } - } - return idArray; + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "triangle", 3, x, y, selected, hover); }, writable: true, configurable: true }, - getSelectedEdges: { - - /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. - */ - value: function getSelectedEdges() { - var idArray = []; - if (this.options.select == true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } - } - } - return idArray; + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); }, writable: true, configurable: true - }, - selectNodes: { + } + }); + return Triangle; + })(ShapeBase); - /** - * select zero or more nodes with the option to highlight edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - * @param {boolean} [highlightEdges] - */ - value: function selectNodes(selection, highlightEdges) { - var i, iMax, id; + module.exports = Triangle; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; +/***/ }, +/* 94 */ +/***/ function(module, exports, __webpack_require__) { - // first unselect any selected node - this.unselectAll(true); + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var node = this.body.nodes[id]; - if (!node) { - throw new RangeError("Node with id \"" + id + "\" not found"); - } - this._selectObject(node, true, true, highlightEdges, true); - } - this.redraw(); - }, - writable: true, - configurable: true - }, - selectEdges: { + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - value: function selectEdges(selection) { - var i, iMax, id; + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // first unselect any selected node - this.unselectAll(true); + var ShapeBase = _interopRequire(__webpack_require__(102)); - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var TriangleDown = (function (ShapeBase) { + function TriangleDown(options, body, labelModule) { + _classCallCheck(this, TriangleDown); - var edge = this.body.edges[id]; - if (!edge) { - throw new RangeError("Edge with id \"" + id + "\" not found"); - } - this._selectObject(edge, true, true, false, true); - } - this.redraw(); + _get(Object.getPrototypeOf(TriangleDown.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(TriangleDown, ShapeBase); + + _prototypeProperties(TriangleDown, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); }, writable: true, configurable: true }, - updateSelection: { - - /** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ - value: function updateSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.body.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; - } - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.body.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; - } - } - } + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "triangleDown", 3, x, y, selected, hover); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); }, writable: true, configurable: true } }); - return SelectionHandler; - })(); + return TriangleDown; + })(ShapeBase); - module.exports = SelectionHandler; + module.exports = TriangleDown; /***/ }, -/* 102 */ +/* 95 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Created by Alex on 3/3/2015. + * Created by Alex on 3/20/2015. */ - var util = __webpack_require__(1); - - var LayoutEngine = (function () { - function LayoutEngine(body) { - var _this = this; - _classCallCheck(this, LayoutEngine); - - this.body = body; - - this.initialRandomSeed = Math.round(Math.random() * 1000000); - this.randomSeed = this.initialRandomSeed; - this.options = {}; - this.defaultOptions = { - randomSeed: undefined, - hierarchical: { - enabled: false, - levelSeparation: 150, - direction: "UD", // UD, DU, LR, RL - sortMethod: "hubsize" // hubsize, directed - } - }; - util.extend(this.options, this.defaultOptions); + var BezierEdgeBase = _interopRequire(__webpack_require__(103)); - this.hierarchicalLevels = {}; + var BezierEdgeDynamic = (function (BezierEdgeBase) { + function BezierEdgeDynamic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeDynamic); - this.body.emitter.on("_dataChanged", function () { - _this.setupHierarchicalLayout(); - }); + this.via = undefined; + _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); // --> this calls the setOptions below } - _prototypeProperties(LayoutEngine, null, { - setOptions: { - value: function setOptions(options, allOptions) { - if (options !== undefined) { - util.mergeOptions(this.options, options, "hierarchical"); - if (options.randomSeed !== undefined) { - this.randomSeed = options.randomSeed; - } - - if (this.options.hierarchical.enabled === true) { - // make sure the level seperation is the right way up - if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "DU") { - if (this.options.hierarchical.levelSeparation > 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } else { - if (this.options.hierarchical.levelSeparation < 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } + _inherits(BezierEdgeDynamic, BezierEdgeBase); - // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. - return this.adaptAllOptions(allOptions); - } + _prototypeProperties(BezierEdgeDynamic, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; + this.setupSupportNode(); + }, + writable: true, + configurable: true + }, + cleanup: { + value: function cleanup() { + if (this.via !== undefined) { + delete this.body.nodes[this.via.id]; + this.via = undefined; + return true; } - return allOptions; + return false; }, writable: true, configurable: true }, - adaptAllOptions: { - value: function adaptAllOptions(allOptions) { - if (this.options.hierarchical.enabled === true) { - // set the physics - if (allOptions.physics === undefined || allOptions.physics === true) { - allOptions.physics = { solver: "hierarchicalRepulsion" }; - } else if (options.physics !== false) { - allOptions.physics.solver = "hierarchicalRepulsion"; - } - - // get the type of static smooth curve in case it is required - var type = "horizontal"; - if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "LR") { - type = "vertical"; - } - - // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. - if (allOptions.edges === undefined) { - allOptions.edges = { smooth: false }; - } else if (allOptions.edges.smooth === undefined) { - allOptions.edges.smooth = false; - } else { - allOptions.edges.smooth = { enabled: true, dynamic: false, type: type }; - } + setupSupportNode: { - // force all edges into static smooth curves. - this.body.emitter.emit("_forceDisableDynamicCurves", type); + /** + * 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. + * + * The changed data is not called, if needed, it is returned by the main edge constructor. + * @private + */ + value: function setupSupportNode() { + if (this.via === undefined) { + var nodeId = "edgeId:" + this.id; + var node = this.body.functions.createNode({ + id: nodeId, + mass: 1, + shape: "circle", + image: "", + physics: true, + hidden: true + }); + this.body.nodes[nodeId] = node; + this.via = node; + this.via.parentEdgeId = this.id; + this.positionBezierNode(); + } + }, + writable: true, + configurable: true + }, + positionBezierNode: { + value: function positionBezierNode() { + if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); + } else if (this.via !== undefined) { + this.via.x = 0; + this.via.y = 0; } - return allOptions; }, writable: true, configurable: true }, - seededRandom: { - value: function seededRandom() { - var x = Math.sin(this.randomSeed++) * 10000; - return x - Math.floor(x); + _line: { + + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); + ctx.stroke(); + return this.via; }, writable: true, configurable: true }, - positionInitially: { - value: function positionInitially(nodesArray) { - if (this.options.hierarchical.enabled !== true) { - for (var i = 0; i < nodesArray.length; i++) { - var node = nodesArray[i]; - if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { - var radius = 10 * 0.1 * nodesArray.length + 10; - var angle = 2 * Math.PI * this.seededRandom(); + getPoint: { - if (node.options.fixed.x == false) { - node.x = radius * Math.cos(angle); - } - if (node.options.fixed.x == false) { - node.y = radius * Math.sin(angle); - } - } - } - } + + /** + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} + * @private + */ + value: function getPoint(percentage) { + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + + return { x: x, y: y }; }, writable: true, configurable: true }, - getSeed: { - value: function getSeed() { - return this.initialRandomSeed; + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + return this._findBorderPositionBezier(nearNode, ctx, this.via); }, writable: true, configurable: true }, - setupHierarchicalLayout: { + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); + }, + writable: true, + configurable: true + } + }); - /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * - * @private - */ - value: function setupHierarchicalLayout() { - if (this.options.hierarchical.enabled == true && this.body.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var node = undefined, - nodeId = undefined; - var definedLevel = false; - var undefinedLevel = false; - this.hierarchicalLevels = {}; - this.nodeSpacing = 100; + return BezierEdgeDynamic; + })(BezierEdgeBase); - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.options.level !== undefined) { - definedLevel = true; - this.hierarchicalLevels[nodeId] = node.options.level; - } else { - undefinedLevel = true; - } - } - } + module.exports = BezierEdgeDynamic; - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - return; - } else { - // setup the system to use hierarchical method. - //this._changeConstants(); +/***/ }, +/* 96 */ +/***/ function(module, exports, __webpack_require__) { - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - if (this.options.hierarchical.sortMethod == "hubsize") { - this._determineLevelsByHubsize(); - } else if (this.options.hierarchical.sortMethod == "directed" || "direction") { - this._determineLevelsDirected(); - } - } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); + "use strict"; - // place the nodes on the canvas. - this._placeNodesByHierarchy(distribution); - } - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 3/20/2015. + */ + + var BezierEdgeBase = _interopRequire(__webpack_require__(103)); + + var BezierEdgeStatic = (function (BezierEdgeBase) { + function BezierEdgeStatic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeStatic); + + _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(BezierEdgeStatic, BezierEdgeBase); + + _prototypeProperties(BezierEdgeStatic, null, { + cleanup: { + value: function cleanup() { + return false; }, writable: true, configurable: true }, - _placeNodesByHierarchy: { - + _line: { /** - * This function places the nodes on the canvas based on the hierarchial distribution. - * - * @param {Object} distribution | obtained by the function this._getDistribution() + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _placeNodesByHierarchy(distribution) { - var nodeId = undefined, - node = undefined; - this.positionedNodes = {}; - - // start placing all the level 0 nodes first. Then recursively position their branches. - for (var level in distribution) { - if (distribution.hasOwnProperty(level)) { - for (nodeId in distribution[level].nodes) { - if (distribution[level].nodes.hasOwnProperty(nodeId)) { - node = distribution[level].nodes[nodeId]; - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - if (node.x === undefined) { - node.x = distribution[level].distance; - } - distribution[level].distance = node.x + this.nodeSpacing; - } else { - if (node.y === undefined) { - node.y = distribution[level].distance; - } - distribution[level].distance = node.y + this.nodeSpacing; - } + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + var via = this._getViaCoordinates(); - this.positionedNodes[nodeId] = true; - this._placeBranchNodes(node.edges, node.id, distribution, level); - } - } - } + // fallback to normal straight edges + if (via.x === undefined) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return undefined; + } else { + ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); + ctx.stroke(); + return via; } }, writable: true, configurable: true }, - _getDistribution: { + _getViaCoordinates: { + value: function _getViaCoordinates() { + var xVia = undefined; + var yVia = undefined; + var factor = this.options.smooth.roundness; + var type = this.options.smooth.type; + 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; + } + } + } 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; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } 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 { + xVia = this.to.x + (1 - factor) * dx; + } + 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; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } else if (type == "curvedCW") { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); - /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} - * @private - */ - value: function _getDistribution() { - var distribution = {}; - var nodeId = undefined, - node = undefined; + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else if (type == "curvedCCW") { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - node.y = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; - node.options.fixed.y = true; - } else { - node.x = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; - node.options.fixed.x = true; + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); + + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } 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) { + 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) { + 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) { + 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) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + xVia = this.to.x > xVia ? this.to.x : xVia; + } } - if (distribution[this.hierarchicalLevels[nodeId]] === undefined) { - distribution[this.hierarchicalLevels[nodeId]] = { amount: 0, nodes: {}, distance: 0 }; + } 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; + yVia = this.to.y > yVia ? this.to.y : yVia; + } else if (this.from.x > this.to.x) { + 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.y < this.to.y) { + if (this.from.x < this.to.x) { + 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) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + yVia = this.to.y < yVia ? this.to.y : yVia; + } } - distribution[this.hierarchicalLevels[nodeId]].amount += 1; - distribution[this.hierarchicalLevels[nodeId]].nodes[nodeId] = node; } } - return distribution; + return { x: xVia, y: yVia }; }, writable: true, configurable: true }, - _getHubSize: { - + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + var options = arguments[2] === undefined ? {} : arguments[2]; + return this._findBorderPositionBezier(nearNode, ctx, options.via); + }, + writable: true, + configurable: true + }, + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); + }, + writable: true, + configurable: true + }, + getPoint: { /** - * Get the hubsize from all remaining unlevelled nodes. - * - * @returns {number} + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} * @private */ - value: function _getHubSize() { - var hubSize = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (this.hierarchicalLevels[nodeId] === undefined) { - hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; - } - } - } - return hubSize; + value: function getPoint(percentage) { + var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; + + return { x: x, y: y }; }, writable: true, configurable: true - }, - _determineLevelsByHubsize: { + } + }); + return BezierEdgeStatic; + })(BezierEdgeBase); - /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. - * - * @param hubsize - * @private - */ - value: function _determineLevelsByHubsize() { - var nodeId = undefined, - node = undefined; - var hubSize = 1; + module.exports = BezierEdgeStatic; - while (hubSize > 0) { - // determine hubs - hubSize = this._getHubSize(); - if (hubSize == 0) break; +/***/ }, +/* 97 */ +/***/ function(module, exports, __webpack_require__) { - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.edges.length == hubSize) { - this._setLevel(0, node); - } - } - } - } + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 3/20/2015. + */ + + var EdgeBase = _interopRequire(__webpack_require__(104)); + + var StraightEdge = (function (EdgeBase) { + function StraightEdge(options, body, labelModule) { + _classCallCheck(this, StraightEdge); + + _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(StraightEdge, EdgeBase); + + _prototypeProperties(StraightEdge, null, { + cleanup: { + value: function cleanup() { + return false; }, writable: true, configurable: true }, - _setLevel: { - - + _line: { /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. - * - * @param level - * @param edges - * @param parentId + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _setLevel(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) { - return; - }var childNode = undefined; - this.hierarchicalLevels[node.id] = level; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId == node.id) { - childNode = node.edges[i].from; - } else { - childNode = node.edges[i].to; - } - this._setLevel(level + 1, childNode); - } + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return undefined; }, writable: true, configurable: true }, - _determineLevelsDirected: { - - + getPoint: { /** - * this function allocates nodes in levels based on the direction of the edges - * - * @param hubsize + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} * @private */ - value: function _determineLevelsDirected() { - var nodeId = undefined, - node = undefined; - var minLevel = 10000; - - // set first node to source - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - this._setLevelDirected(minLevel, node); - } - } - - // get the minimum level - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; - } - } - - // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.hierarchicalLevels[nodeId] -= minLevel; - } - } + value: function getPoint(percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + }; }, writable: true, configurable: true }, - _setLevelDirected: { + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + var node1 = this.to; + var node2 = this.from; + if (nearNode.id === this.from.id) { + node1 = this.from; + node2 = this.to; + } + var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + var dx = node1.x - node2.x; + var dy = node1.y - node2.y; + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = nearNode.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - /** - * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction - * - * @param level - * @param edges - * @param parentId - * @private - */ - value: function _setLevelDirected(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) { - return; - }var childNode = undefined; - this.hierarchicalLevels[node.id] = level; + var borderPos = {}; + borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; + borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId == node.id) { - childNode = node.edges[i].from; - this._setLevelDirected(level - 1, childNode); - } else { - childNode = node.edges[i].to; - this._setLevelDirected(level + 1, childNode); - } - } + return borderPos; }, writable: true, configurable: true }, - _placeBranchNodes: { - - - - /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. - * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel - * @private - */ - value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = undefined; - var parentNode = undefined; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - parentNode = edges[i].to; - } else { - childNode = edges[i].to; - parentNode = edges[i].from; - } - var childNodeLevel = this.hierarchicalLevels[childNode.id]; - if (this.positionedNodes[childNode.id] === undefined) { - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - if (childNodeLevel > parentLevel) { - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - if (childNode.x === undefined) { - childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); - } - distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; - this.positionedNodes[childNode.id] = true; - } else { - if (childNode.y === undefined) { - childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); - } - distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; - } - this.positionedNodes[childNode.id] = true; - - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); - } - } - } - } + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); }, writable: true, configurable: true } }); - return LayoutEngine; - })(); + return StraightEdge; + })(EdgeBase); - module.exports = LayoutEngine; + module.exports = StraightEdge; /***/ }, -/* 103 */ +/* 98 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = function(module) { + if(!module.webpackPolyfill) { + module.deprecate = function() {}; + module.paths = []; + // module.parent = undefined by default + module.children = []; + module.webpackPolyfill = 1; + } + return module; + } + + +/***/ }, +/* 99 */ +/***/ function(module, exports, __webpack_require__) { + + /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; + + /* WEBPACK VAR INJECTION */}.call(exports, {})) + +/***/ }, +/* 100 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -35446,1169 +35776,896 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var util = __webpack_require__(1); - var Hammer = __webpack_require__(19); - var hammerUtil = __webpack_require__(24); - var locales = __webpack_require__(104); - /** - * clears the toolbar div element of children - * - * @private + * Created by Alex on 3/19/2015. */ - var ManipulationSystem = (function () { - function ManipulationSystem(body, canvas, selectionHandler) { - _classCallCheck(this, ManipulationSystem); - - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; - this.editMode = false; - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; - this.boundFunction = undefined; - this.manipulationHammers = []; - this.cachedFunctions = {}; - this.touchTime = 0; - this.temporaryIds = { nodes: [], edges: [] }; - this.guiEnabled = false; - this.selectedControlNode = undefined; + var NodeBase = (function () { + function NodeBase(options, body, labelModule) { + _classCallCheck(this, NodeBase); - this.options = {}; - this.defaultOptions = { - enabled: false, - initiallyVisible: false, - locale: "en", - locales: locales, - functionality: { - addNode: true, - addEdge: true, - editNode: true, - editEdge: true, - deleteNode: true, - deleteEdge: true - }, - handlerFunctions: { - addNode: undefined, - addEdge: undefined, - editNode: undefined, - editEdge: undefined, - deleteNode: undefined, - deleteEdge: undefined - } - }; - util.extend(this.options, this.defaultOptions); + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.top = undefined; + this.left = undefined; + this.height = undefined; + this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; } - _prototypeProperties(ManipulationSystem, null, { + _prototypeProperties(NodeBase, null, { setOptions: { value: function setOptions(options) { - if (options !== undefined) { - if (typeof options == "boolean") { - this.options.enabled = options; - } else { - this.options.enabled = true; - for (var prop in options) { - if (options.hasOwnProperty(prop)) { - this.options[prop] = options[prop]; - } - } - } - if (this.options.initiallyVisible === true) { - this.editMode = true; - } - this.init(); - } + this.options = options; }, writable: true, configurable: true }, - init: { - value: function init() { - if (this.options.enabled === true) { - // Enable the GUI - this.guiEnabled = true; - - // remove override - this.selectionHandler.forceSelectEdges = true; - - this.createWrappers(); - if (this.editMode === false) { - this.createEditButton(); - } else { - this.createManipulatorBar(); - } - } else { - this.removeManipulationDOM(); - - // disable the gui - this.guiEnabled = false; - } + _distanceToBorder: { + value: function _distanceToBorder(angle) { + var borderWidth = 1; + return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; }, writable: true, configurable: true - }, - createWrappers: { - value: function createWrappers() { - // load the manipulator HTML elements. All styling done in css. - if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement("div"); - this.manipulationDiv.className = "network-manipulationDiv"; - if (this.editMode === true) { - this.manipulationDiv.style.display = "block"; - } else { - this.manipulationDiv.style.display = "none"; - } - this.canvas.frame.appendChild(this.manipulationDiv); - } + } + }); - if (this.editModeDiv === undefined) { - this.editModeDiv = document.createElement("div"); - this.editModeDiv.className = "network-manipulation-editMode"; - if (this.editMode === true) { - this.editModeDiv.style.display = "none"; - } else { - this.editModeDiv.style.display = "block"; - } - this.canvas.frame.appendChild(this.editModeDiv); - } + return NodeBase; + })(); - if (this.closeDiv === undefined) { - this.closeDiv = document.createElement("div"); - this.closeDiv.className = "network-manipulation-closeDiv"; - this.closeDiv.style.display = this.manipulationDiv.style.display; - this.canvas.frame.appendChild(this.closeDiv); - } - }, - writable: true, - configurable: true - }, - createEditButton: { + module.exports = NodeBase; +/***/ }, +/* 101 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Create the edit button - */ - value: function createEditButton() { - // restore everything to it's original state (if applicable) - this._clean(); + "use strict"; - // reset the manipulationDOM - this.manipulationDOM = {}; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // empty the editModeDiv - util.recursiveDOMDelete(this.editModeDiv); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // create the contents for the editMode button - var locale = this.options.locales[this.options.locale]; - var button = this.createButton("editMode", "network-manipulationUI edit editmode", locale.edit); - this.editModeDiv.appendChild(button); + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - // bind a hammer listener to the button, calling the function toggleEditMode. - this.bindHammerToDiv(button, "toggleEditMode"); - }, - writable: true, - configurable: true - }, - removeManipulationDOM: { - value: function removeManipulationDOM() { - // removes all the bindings and overloads - this._clean(); + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - // empty the manipulation divs - util.recursiveDOMDelete(this.manipulationDiv); - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.closeDiv); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // remove the manipulation divs - this.canvas.frame.removeChild(this.manipulationDiv); - this.canvas.frame.removeChild(this.editModeDiv); - this.canvas.frame.removeChild(this.closeDiv); + /** + * Created by Alex on 3/19/2015. + */ + var NodeBase = _interopRequire(__webpack_require__(100)); - // set the references to undefined - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + var CircleImageBase = (function (NodeBase) { + function CircleImageBase(options, body, labelModule) { + _classCallCheck(this, CircleImageBase); - // remove override - this.selectionHandler.forceSelectEdges = false; - }, - writable: true, - configurable: true - }, - _cleanManipulatorHammers: { + _get(Object.getPrototypeOf(CircleImageBase.prototype), "constructor", this).call(this, options, body, labelModule); + } - //clearManipulatorBar() { - // util._recursiveDOMDelete(this.manipulationDiv); - // this.manipulationDOM = {}; - // this._cleanManipulatorHammers(); - // this._manipulationReleaseOverload(); - //} + _inherits(CircleImageBase, NodeBase); + + _prototypeProperties(CircleImageBase, null, { + _drawRawCircle: { + value: function _drawRawCircle(ctx, x, y, selected, hover, size) { + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - value: function _cleanManipulatorHammers() { - // _clean hammer bindings - if (this.manipulationHammers.length != 0) { - for (var i = 0; i < this.manipulationHammers.length; i++) { - this.manipulationHammers[i].destroy(); - } - this.manipulationHammers = []; - } + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.circle(x, y, size); + ctx.fill(); + ctx.stroke(); }, writable: true, configurable: true }, - _restoreOverloadedFunctions: { - - /** - * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore - * these functions to their original functionality, we saved them in this.cachedFunctions. - * This function restores these functions to their original function. - * - * @private - */ - value: function _restoreOverloadedFunctions() { - for (var functionName in this.cachedFunctions) { - if (this.cachedFunctions.hasOwnProperty(functionName)) { - this.body.eventListeners[functionName] = this.cachedFunctions[functionName]; - delete this.cachedFunctions[functionName]; - } + _drawImageAtPosition: { + value: function _drawImageAtPosition(ctx) { + if (this.imageObj.width != 0) { + // draw the image + ctx.globalAlpha = 1; + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); } - this.cachedFunctions = {}; }, writable: true, configurable: true }, - toggleEditMode: { + _drawImageLabel: { + value: function _drawImageLabel(ctx, x, y, selected) { + var yLabel; + var offset = 0; - /** - * Enable or disable edit-mode. - * - * @private - */ - value: function toggleEditMode() { - this.editMode = !this.editMode; - var toolbar = this.manipulationDiv; - var closeDiv = this.closeDiv; - var editModeDiv = this.editModeDiv; - if (this.editMode === true) { - toolbar.style.display = "block"; - closeDiv.style.display = "block"; - editModeDiv.style.display = "none"; - this.bindHammerToDiv(closeDiv, "toggleEditMode"); - this.createManipulatorBar(); - } else { - toolbar.style.display = "none"; - closeDiv.style.display = "none"; - editModeDiv.style.display = "block"; - this.createEditButton(); + if (this.height !== undefined) { + offset = this.height * 0.5; + var labelDimensions = this.labelModule.getTextSize(ctx); + + if (labelDimensions.lineCount >= 1) { + offset += labelDimensions.height / 2; + offset += 3; + } } + + yLabel = y + offset; + this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); }, writable: true, configurable: true - }, - _clean: { - value: function _clean() { - // _clean the divs - if (this.guiEnabled === true) { - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.manipulationDiv); + } + }); - // removes all the bindings and overloads - this._cleanManipulatorHammers(); - } + return CircleImageBase; + })(NodeBase); - // remove temporary nodes and edges - this._cleanupTemporaryNodesAndEdges(); + module.exports = CircleImageBase; - // restore overloaded UI functions - this._restoreOverloadedFunctions(); +/***/ }, +/* 102 */ +/***/ function(module, exports, __webpack_require__) { - // remove the boundFunction - if (this.boundFunction !== undefined) { - this.body.emitter.off(this.boundFunction.event, this.boundFunction.fn); - } - this.boundFunction = undefined; - }, - writable: true, - configurable: true - }, - createSeperator: { - value: function createSeperator() { - var index = arguments[0] === undefined ? 1 : arguments[0]; - this.manipulationDOM["seperatorLineDiv" + index] = document.createElement("div"); - this.manipulationDOM["seperatorLineDiv" + index].className = "network-seperatorLine"; - this.manipulationDiv.appendChild(this.manipulationDOM["seperatorLineDiv" + index]); - }, - writable: true, - configurable: true - }, - createAddNodeButton: { - value: function createAddNodeButton(locale) { - var button = this.createButton("addNode", "network-manipulationUI add", locale.addNode); - this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, "addNodeMode"); - }, - writable: true, - configurable: true - }, - createAddEdgeButton: { - value: function createAddEdgeButton(locale) { - var button = this.createButton("addEdge", "network-manipulationUI connect", locale.addEdge); - this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, "addEdgeMode"); - }, - writable: true, - configurable: true - }, - createEditNodeButton: { - value: function createEditNodeButton(locale) { - var button = this.createButton("editNode", "network-manipulationUI edit", locale.editNode); - this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, "_editNode"); - }, - writable: true, - configurable: true - }, - createEditEdgeButton: { - value: function createEditEdgeButton(locale) { - var button = this.createButton("editEdge", "network-manipulationUI edit", locale.editEdge); - this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, "editEdgeMode"); - }, - writable: true, - configurable: true - }, - createDeleteButton: { - value: function createDeleteButton(locale) { - var button = this.createButton("delete", "network-manipulationUI delete", locale.del); - this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, "deleteSelected"); - }, - writable: true, - configurable: true - }, - createBackButton: { - value: function createBackButton(locale) { - var button = this.createButton("back", "network-manipulationUI back", locale.back); - this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, "createManipulatorBar"); - }, - writable: true, - configurable: true - }, - createDescription: { - value: function createDescription(label) { - this.manipulationDiv.appendChild(this.createButton("description", "network-manipulationUI none", label)); - }, - writable: true, - configurable: true - }, - createButton: { - value: function createButton(id, className, label) { - var labelClassName = arguments[3] === undefined ? "network-manipulationLabel" : arguments[3]; - this.manipulationDOM[id + "Div"] = document.createElement("div"); - this.manipulationDOM[id + "Div"].className = className; - this.manipulationDOM[id + "Label"] = document.createElement("div"); - this.manipulationDOM[id + "Label"].className = labelClassName; - this.manipulationDOM[id + "Label"].innerHTML = label; - this.manipulationDOM[id + "Div"].appendChild(this.manipulationDOM[id + "Label"]); - return this.manipulationDOM[id + "Div"]; + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + /** + * Created by Alex on 3/19/2015. + */ + var NodeBase = _interopRequire(__webpack_require__(100)); + + var ShapeBase = (function (NodeBase) { + function ShapeBase(options, body, labelModule) { + _classCallCheck(this, ShapeBase); + + _get(Object.getPrototypeOf(ShapeBase.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(ShapeBase, NodeBase); + + _prototypeProperties(ShapeBase, null, { + _resizeShape: { + value: function _resizeShape() { + if (this.width === undefined) { + var size = 2 * this.options.size; + this.width = size; + this.height = size; + } }, writable: true, configurable: true }, - temporaryBind: { - value: function temporaryBind(fn, event) { - this.boundFunction = { fn: fn.bind(this), event: event }; - this.body.emitter.on(event, this.boundFunction.fn); + _drawShape: { + value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { + this._resizeShape(); + + this.left = x - this.width / 2; + this.top = y - this.height / 2; + + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx[shape](x, y, this.options.size); + ctx.fill(); + ctx.stroke(); + + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + + if (this.options.label !== undefined) { + var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. + this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + } }, writable: true, configurable: true - }, - createManipulatorBar: { + } + }); - /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private - */ - value: function createManipulatorBar() { - this._clean(); + return ShapeBase; + })(NodeBase); - // resume calculation - this.body.emitter.emit("restorePhysics"); + module.exports = ShapeBase; - // reset global letiables - this.manipulationDOM = {}; +/***/ }, +/* 103 */ +/***/ function(module, exports, __webpack_require__) { - var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); - var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); - var selectedTotalCount = selectedNodeCount + selectedEdgeCount; - var locale = this.options.locales[this.options.locale]; - var needSeperator = false; + "use strict"; - if (this.options.functionality.addNode === true) { - this.createAddNodeButton(locale); - needSeperator = true; - } - if (this.options.functionality.addEdge === true) { - if (needSeperator === true) { - this.createSeperator(1); - } else { - needSeperator = true; - } - this.createAddEdgeButton(locale); - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - if (selectedNodeCount === 1 && typeof this.options.handlerFunctions.editNode === "function" && this.options.functionality.editNode === true) { - if (needSeperator === true) { - this.createSeperator(2); - } else { - needSeperator = true; - } - this.createEditNodeButton(locale); - } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.functionality.editEdge === true) { - if (needSeperator === true) { - this.createSeperator(3); - } else { - needSeperator = true; - } - this.createEditEdgeButton(locale); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // remove buttons - if (selectedTotalCount !== 0) { - if (selectedNodeCount === 1 && this.options.functionality.deleteNode === true) { - if (needSeperator === true) { - this.createSeperator(4); - } - this.createDeleteButton(locale); - } else if (selectedNodeCount === 0 && this.options.functionality.deleteEdge === true) { - if (needSeperator === true) { - this.createSeperator(4); - } - this.createDeleteButton(locale); - } - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - // bind the close button - this.bindHammerToDiv(this.closeDiv, "toggleEditMode"); + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - // refresh this bar based on what has been selected - this.temporaryBind(this.createManipulatorBar, "select"); - }, - writable: true, - configurable: true - }, - bindHammerToDiv: { + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - /** - * Bind an hammer instance to a DOM element. TODO: remove the double check. - * @param domElement - * @param funct - */ - value: function bindHammerToDiv(domElement, funct) { - var hammer = new Hammer(domElement, {}); - hammerUtil.onTouch(hammer, this[funct].bind(this)); - this.manipulationHammers.push(hammer); - }, - writable: true, - configurable: true - }, - addNodeMode: { + /** + * Created by Alex on 3/20/2015. + */ + var EdgeBase = _interopRequire(__webpack_require__(104)); + + var BezierEdgeBase = (function (EdgeBase) { + function BezierEdgeBase(options, body, labelModule) { + _classCallCheck(this, BezierEdgeBase); + + _get(Object.getPrototypeOf(BezierEdgeBase.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(BezierEdgeBase, EdgeBase); + + _prototypeProperties(BezierEdgeBase, null, { + _findBorderPositionBezier: { /** - * Create the toolbar for adding Nodes + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. * - * @private + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode */ - value: function addNodeMode() { - // clear the toolbar - this._clean(); + value: function _findBorderPositionBezier(nearNode, ctx) { + var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; + var maxIterations = 10; + var iteration = 0; + var low = 0; + var high = 1; + var pos, angle, distanceToBorder, distanceToPoint, difference; + var threshold = 0.2; + var node = this.to; + var from = false; + if (nearNode.id === this.from.id) { + node = this.from; + from = true; + } - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this.createBackButton(locale); - this.createSeperator(); - this.createDescription(locale.addDescription); + while (low <= high && iteration < maxIterations) { + var middle = (low + high) * 0.5; - // bind the close button - this.bindHammerToDiv(this.closeDiv, "toggleEditMode"); + pos = this.getPoint(middle, viaNode); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + if (Math.abs(difference) < threshold) { + break; // found + } else if (difference < 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (from == false) { + low = middle; + } else { + high = middle; + } + } else { + if (from == false) { + high = middle; + } else { + low = middle; + } + } + + iteration++; } + pos.t = middle; - this.temporaryBind(this._addNode, "click"); + return pos; }, writable: true, configurable: true }, - addEdgeMode: { + _getDistanceToBezierEdge: { + /** - * create the toolbar to connect nodes - * + * 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 */ - value: function addEdgeMode() { - // _clean the system - this._clean(); - - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this.createBackButton(locale); - this.createSeperator(); - this.createDescription(locale.edgeDescription); - - // bind the close button - this.bindHammerToDiv(this.closeDiv, "toggleEditMode"); + value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var xVia = undefined, + yVia = undefined; + xVia = via.x; + yVia = via.y; + var minDistance = 1000000000; + var distance = undefined; + var i = undefined, + t = undefined, + x = undefined, + y = undefined; + var lastX = x1; + var lastY = y1; + for (i = 1; 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; } - // temporarily overload functions - this.cachedFunctions.onTouch = this.body.eventListeners.onTouch; - this.cachedFunctions.onDragEnd = this.body.eventListeners.onDragEnd; - this.cachedFunctions.onHold = this.body.eventListeners.onHold; - - this.body.eventListeners.onTouch = this._handleConnect.bind(this); - this.body.eventListeners.onDragEnd = this._finishConnect.bind(this); - this.body.eventListeners.onHold = function () {}; + return minDistance; }, writable: true, configurable: true - }, - editEdgeMode: { + } + }); - /** - * create the toolbar to edit edges - * - * @private - */ - value: function editEdgeMode() { - // clear the system - this._clean(); + return BezierEdgeBase; + })(EdgeBase); - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this.createBackButton(locale); - this.createSeperator(); - this.createDescription(locale.editEdgeDescription); + module.exports = BezierEdgeBase; - // bind the close button - this.bindHammerToDiv(this.closeDiv, "toggleEditMode"); - } +/***/ }, +/* 104 */ +/***/ function(module, exports, __webpack_require__) { - this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; - var edge = this.body.edges[this.edgeBeingEditedId]; + "use strict"; - // create control nodes - var controlNodeFrom = this.body.functions.createNode(this.getTargetNodeProperties(edge.from.x, edge.from.y)); - var controlNodeTo = this.body.functions.createNode(this.getTargetNodeProperties(edge.to.x, edge.to.y)); + var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; - this.temporaryIds.nodes.push(controlNodeFrom.id); - this.temporaryIds.nodes.push(controlNodeTo.id); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - this.body.nodes[controlNodeFrom.id] = controlNodeFrom; - this.body.nodeIndices.push(controlNodeFrom.id); - this.body.nodes[controlNodeTo.id] = controlNodeTo; - this.body.nodeIndices.push(controlNodeTo.id); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // temporarily overload functions - this.cachedFunctions.onTouch = this.body.eventListeners.onTouch; - this.cachedFunctions.onTap = this.body.eventListeners.onTap; - this.cachedFunctions.onHold = this.body.eventListeners.onHold; - this.cachedFunctions.onDragStart = this.body.eventListeners.onDragStart; - this.cachedFunctions.onDrag = this.body.eventListeners.onDrag; - this.cachedFunctions.onDragEnd = this.body.eventListeners.onDragEnd; - this.cachedFunctions.onMouseOver = this.body.eventListeners.onMouseOver; - - this.body.eventListeners.onTouch = this._controlNodeTouch.bind(this); - this.body.eventListeners.onTap = function () {}; - this.body.eventListeners.onHold = function () {}; - this.body.eventListeners.onDragStart = this._controlNodeDragStart.bind(this); - this.body.eventListeners.onDrag = this._controlNodeDrag.bind(this); - this.body.eventListeners.onDragEnd = this._controlNodeDragEnd.bind(this); - this.body.eventListeners.onMouseOver = function () {}; + /** + * Created by Alex on 3/20/2015. + */ + var util = __webpack_require__(1); - // create function to position control nodes correctly on movement - var positionControlNodes = function (ctx) { - var positions = edge.edgeType.findBorderPositions(ctx); - if (controlNodeFrom.selected === false) { - controlNodeFrom.x = positions.from.x; - controlNodeFrom.y = positions.from.y; - } - if (controlNodeTo.selected === false) { - controlNodeTo.x = positions.to.x; - controlNodeTo.y = positions.to.y; - } - }; - this.temporaryBind(positionControlNodes, "beforeDrawing"); + var EdgeBase = (function () { + function EdgeBase(options, body, labelModule) { + _classCallCheck(this, EdgeBase); - this.body.emitter.emit("_redraw"); + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.colorDirty = true; + } + + _prototypeProperties(EdgeBase, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; }, writable: true, configurable: true }, - _controlNodeTouch: { - value: function _controlNodeTouch(event) { - this.lastTouch = this.body.functions.getPointer(event.center); - this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object + drawLine: { + + /** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function drawLine(ctx, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.lineWidth = this.getLineWidth(selected, hover); + var via = undefined; + if (this.from != this.to) { + // draw line + if (this.options.dashes.enabled == true) { + via = this._drawDashedLine(ctx); + } else { + via = this._line(ctx); + } + } else { + var _getCircleData = this._getCircleData(); + + var _getCircleData2 = _slicedToArray(_getCircleData, 3); + + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; + this._circle(ctx, x, y, radius); + } + + return via; }, writable: true, configurable: true }, - _controlNodeDragStart: { - value: function _controlNodeDragStart(event) { - var pointer = this.lastTouch; - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - var to = this.body.nodes[this.temporaryIds.nodes[1]]; - var edge = this.body.edges[this.edgeBeingEditedId]; - this.selectedControlNode = undefined; + _drawDashedLine: { + value: function _drawDashedLine(ctx) { + var via = undefined; + // only firefox and chrome support this method, else we use the legacy one. + if (ctx.setLineDash !== undefined) { + ctx.save(); + // configure the dash pattern + var pattern = [0]; + if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) { + pattern = [this.options.dashes.length, this.options.dashes.gap]; + } else { + pattern = [5, 5]; + } - var fromSelect = from.isOverlappingWith(pointerObj); - var toSelect = to.isOverlappingWith(pointerObj); + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; - if (fromSelect === true) { - this.selectedControlNode = from; - edge.edgeType.from = from; - } else if (toSelect === true) { - this.selectedControlNode = to; - edge.edgeType.to = to; - } + // draw the line + via = this._line(ctx); - this.body.emitter.emit("_redraw"); + // restore the dash settings. + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + ctx.restore(); + } else { + // unsupporting smooth lines + // draw dashes line + ctx.beginPath(); + ctx.lineCap = "round"; + if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]); + } else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap]); + } else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + } + return via; }, writable: true, configurable: true }, - _controlNodeDrag: { - value: function _controlNodeDrag(event) { - this.body.emitter.emit("disablePhysics"); - var pointer = this.body.functions.getPointer(event.center); - var pos = this.canvas.DOMtoCanvas(pointer); - - if (this.selectedControlNode !== undefined) { - this.selectedControlNode.x = pos.x; - this.selectedControlNode.y = pos.y; + findBorderPosition: { + value: function findBorderPosition(nearNode, ctx, options) { + if (this.from != this.to) { + return this._findBorderPosition(nearNode, ctx, options); } else { - // if the drag was not started properly because the click started outside the network div, start it now. - var diffX = pointer.x - this.lastTouch.x; - var diffY = pointer.y - this.lastTouch.y; - this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + return this._findBorderPositionCircle(nearNode, ctx, options); } - this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - _controlNodeDragEnd: { - value: function _controlNodeDragEnd(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var edge = this.body.edges[this.edgeBeingEditedId]; + findBorderPositions: { + value: function findBorderPositions(ctx) { + var from = {}; + var to = {}; + if (this.from != this.to) { + from = this._findBorderPosition(this.from, ctx); + to = this._findBorderPosition(this.to, ctx); + } else { + var _getCircleData = this._getCircleData(); - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - if (overlappingNodeIds[i] !== this.selectedControlNode.id) { - node = this.body.nodes[overlappingNodeIds[i]]; - break; - } + var _getCircleData2 = _slicedToArray(_getCircleData, 3); + + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; + + + from = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); + to = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.6, high: 0.8, direction: 1 }); } + return { from: from, to: to }; + }, + writable: true, + configurable: true + }, + _getCircleData: { + value: function _getCircleData() { + var x = undefined, + y = undefined; + var node = this.from; + var radius = this.options.selfReferenceSize; - // perform the connection - if (node !== undefined && this.selectedControlNode !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale].createEdgeError); - } else { - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - if (this.selectedControlNode.id == from.id) { - this._editEdge(node.id, edge.to.id); - } else { - this._editEdge(edge.from.id, node.id); - } - } + // get circle coordinates + if (node.shape.width > node.shape.height) { + x = node.x + node.shape.width * 0.5; + y = node.y - radius; } else { - edge.updateEdgeType(); - this.body.emitter.emit("restorePhysics"); + x = node.x + radius; + y = node.y - node.shape.height * 0.5; } - this.body.emitter.emit("_redraw"); + return [x, y, radius]; }, writable: true, configurable: true }, - _selectControlNode: { + _pointOnCircle: { + /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point * @private */ - value: function _selectControlNode(event) {}, + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; + }, writable: true, configurable: true }, - _releaseControlNode: { - + _findBorderPositionCircle: { /** - * - * @param pointer + * This function uses binary search to look for the point where the circle crosses the border of the node. + * @param node + * @param ctx + * @param options + * @returns {*} * @private */ - value: function _releaseControlNode(pointer) { - if (new Date().valueOf() - this.touchTime > 100) { - console.log("release"); - // perform the connection - var node = this.selectionHandler.getNodeAt(pointer); - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale].createEdgeError); - } else { - var edge = this.body.edges[this.edgeBeingEditedId]; + value: function _findBorderPositionCircle(node, ctx, options) { + var x = options.x; + var y = options.y; + var low = options.low; + var high = options.high; + var direction = options.direction; - var targetNodeId = undefined; - if (edge.to.selected === true) { - targetNodeId = edge.toId; - } else if (edge.from.selected === true) { - targetNodeId = edge.fromId; - } + var maxIterations = 10; + var iteration = 0; + var radius = this.options.selfReferenceSize; + var pos = undefined, + angle = undefined, + distanceToBorder = undefined, + distanceToPoint = undefined, + difference = undefined; + var threshold = 0.05; + var middle = (low + high) * 0.5; + + while (low <= high && iteration < maxIterations) { + middle = (low + high) * 0.5; - //this.body.eventListeners.onDrag = this.cachedFunctions["onDrag"]; - //this.body.eventListeners.onRelease = this.cachedFunctions["onRelease"]; - //delete this.cachedFunctions["onRelease"]; - //delete this.cachedFunctions["onDrag"]; - //// - // - // - // - // - // - // - //if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { - // this._createEdge(connectFromId, node.id); - //} + pos = this._pointOnCircle(x, y, radius, middle); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + if (Math.abs(difference) < threshold) { + break; // found + } else if (difference > 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (direction > 0) { + low = middle; + } else { + high = middle; + } + } else { + if (direction > 0) { + high = middle; + } else { + low = middle; } } - this.body.emitter.emit("_redraw"); - //this.body.emitter.emit("_redraw"); - //let newNode = this.getNodeAt(pointer); - //if (newNode !== undefined) { - // if (this.edgeBeingEditedId.controlNodes.from.selected == true) { - // this.edgeBeingEditedId._restoreControlNodes(); - // this._editEdge(newNode.id, this.edgeBeingEditedId.to.id); - // this.edgeBeingEditedId.controlNodes.from.unselect(); - // } - // if (this.edgeBeingEditedId.controlNodes.to.selected == true) { - // this.edgeBeingEditedId._restoreControlNodes(); - // this._editEdge(this.edgeBeingEditedId.from.id, newNode.id); - // this.edgeBeingEditedId.controlNodes.to.unselect(); - // } - //} - //else { - // this.edgeBeingEditedId._restoreControlNodes(); - //} - this.touchTime = new Date().valueOf(); + iteration++; } + pos.t = middle; + + return pos; }, writable: true, configurable: true }, - _handleConnect: { + getLineWidth: { /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width * @private */ - value: function _handleConnect(event) { - var _this = this; - // check to avoid double fireing of this function. - if (new Date().valueOf() - this.touchTime > 100) { - var pointer = this.body.functions.getPointer(event.center); - var node = this.selectionHandler.getNodeAt(pointer); - - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale].createEdgeError); - } else { - (function () { - // create a node the temporary line can look at - var targetNode = _this.body.functions.createNode(_this.getTargetNodeProperties(node.x, node.y)); - var targetNodeId = targetNode.id; - _this.body.nodes[targetNode.id] = targetNode; - _this.body.nodeIndices.push(targetNode.id); - - // create a temporary edge - var connectionEdge = _this.body.functions.createEdge({ - id: "connectionEdge" + util.randomUUID(), - from: node.id, - to: targetNode.id, - physics: false, - smooth: { - enabled: true, - dynamic: false, - type: "continuous", - roundness: 0.5 - } - }); - _this.body.edges[connectionEdge.id] = connectionEdge; - _this.body.edgeIndices.push(connectionEdge.id); - - _this.temporaryIds.nodes.push(targetNode.id); - _this.temporaryIds.edges.push(connectionEdge.id); - - _this.cachedFunctions.onDrag = _this.body.eventListeners.onDrag; - _this.body.eventListeners.onDrag = function (event) { - var pointer = _this.body.functions.getPointer(event.center); - var targetNode = _this.body.nodes[targetNodeId]; - targetNode.x = _this.canvas._XconvertDOMtoCanvas(pointer.x); - targetNode.y = _this.canvas._YconvertDOMtoCanvas(pointer.y); - _this.body.emitter.emit("_redraw"); - }; - })(); - } + value: function getLineWidth(selected, hover) { + if (selected == true) { + return Math.max(Math.min(this.options.widthSelectionMultiplier * this.options.width, this.options.scaling.max), 0.3 / this.body.view.scale); + } else { + if (hover == true) { + return Math.max(Math.min(this.options.hoverWidth, this.options.scaling.max), 0.3 / this.body.view.scale); + } else { + return Math.max(this.options.width, 0.3 / this.body.view.scale); } - this.touchTime = new Date().valueOf(); - - // do the original touch events - this.cachedFunctions.onTouch(event); } }, writable: true, configurable: true }, - _finishConnect: { - value: function _finishConnect(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + getColor: { + value: function getColor(ctx) { + var colorObj = this.options.color; - // remember the edge id - var connectFromId = undefined; - if (this.temporaryIds.edges[0] !== undefined) { - connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; - } + if (colorObj.inherit.enabled === true) { + if (colorObj.inherit.useGradients == true) { + var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); + var fromColor, toColor; + fromColor = this.from.options.color.highlight.border; + toColor = this.to.options.color.highlight.border; - //restore the drag function - if (this.cachedFunctions.onDrag !== undefined) { - this.body.eventListeners.onDrag = this.cachedFunctions.onDrag; - delete this.cachedFunctions.onDrag; - } + if (this.from.selected == false && this.to.selected == false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); + } else if (this.from.selected == true && this.to.selected == false) { + toColor = this.to.options.color.border; + } else if (this.from.selected == false && this.to.selected == true) { + fromColor = this.from.options.color.border; + } + grd.addColorStop(0, fromColor); + grd.addColorStop(1, toColor); - // get the overlapping node but NOT the temporary node; - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) !== -1) { - node = this.body.nodes[overlappingNodeIds[i]]; - break; + // -------------------- this returns -------------------- // + return grd; } - } - - // clean temporary nodes and edges. - this._cleanupTemporaryNodesAndEdges(); - // perform the connection - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale].createEdgeError); - } else { - if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { - this._createEdge(connectFromId, node.id); + if (this.colorDirty === true) { + if (colorObj.inherit.source == "to") { + colorObj.highlight = this.to.options.color.highlight.border; + colorObj.hover = this.to.options.color.hover.border; + colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); + } else { + // (this.options.color.inherit.source == "from") { + colorObj.highlight = this.from.options.color.highlight.border; + colorObj.hover = this.from.options.color.hover.border; + colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); } } } - this.body.emitter.emit("_redraw"); - }, - writable: true, - configurable: true - }, - _cleanupTemporaryNodesAndEdges: { - value: function _cleanupTemporaryNodesAndEdges() { - // _clean temporary edges - for (var i = 0; i < this.temporaryIds.edges.length; i++) { - this.body.edges[this.temporaryIds.edges[i]].disconnect(); - delete this.body.edges[this.temporaryIds.edges[i]]; - var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); - if (indexTempEdge !== -1) { - this.body.edgeIndices.splice(indexTempEdge, 1); - } - } - // _clean temporary nodes - for (var i = 0; i < this.temporaryIds.nodes.length; i++) { - delete this.body.nodes[this.temporaryIds.nodes[i]]; - var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); - if (indexTempNode !== -1) { - this.body.nodeIndices.splice(indexTempNode, 1); - } - } + // if color inherit is on and gradients are used, the function has already returned by now. + this.colorDirty = false; - this.temporaryIds = { nodes: [], edges: [] }; + if (this.selected == true) { + return colorObj.highlight; + } else if (this.hover == true) { + return colorObj.hover; + } else { + return colorObj.color; + } }, writable: true, configurable: true }, - _addNode: { + _circle: { /** - * Adds a node on the specified location + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private */ - value: function _addNode(clickData) { - var _this = this; - var defaultData = { - id: util.randomUUID(), - x: clickData.pointer.canvas.x, - y: clickData.pointer.canvas.y, - label: "new" - }; - - if (typeof this.options.handlerFunctions.addNode === "function") { - if (this.options.handlerFunctions.addNode.length == 2) { - this.options.handlerFunctions.addNode(defaultData, function (finalizedData) { - _this.body.data.nodes.add(finalizedData); - _this.createManipulatorBar(); - }); - } else { - throw new Error("The function for add does not support two arguments (data,callback)"); - this.createManipulatorBar(); - } - } else { - this.body.data.nodes.add(defaultData); - this.createManipulatorBar(); - } + value: function _circle(ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); }, writable: true, configurable: true }, - _createEdge: { + getDistanceToEdge: { /** - * connect two nodes with a new edge. - * + * 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 */ - value: function _createEdge(sourceNodeId, targetNodeId) { - var _this = this; - var defaultData = { from: sourceNodeId, to: targetNodeId }; - if (this.options.handlerFunctions.addEdge) { - if (this.options.handlerFunctions.addEdge.length == 2) { - this.options.handlerFunctions.addEdge(defaultData, function (finalizedData) { - _this.body.data.edges.add(finalizedData); - _this.selectionHandler.unselectAll(); - _this.createManipulatorBar(); - }); - } else { - throw new Error("The function for connect does not support two arguments (data,callback)"); - } + value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var returnValue = 0; + if (this.from != this.to) { + returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); } else { - this.body.data.edges.add(defaultData); - this.selectionHandler.unselectAll(); - this.createManipulatorBar(); + var _getCircleData = this._getCircleData(); + + var _getCircleData2 = _slicedToArray(_getCircleData, 3); + + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; + var dx = x - x3; + var dy = y - y3; + returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); + } + + if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { + return 0; + } else { + return returnValue; } }, writable: true, configurable: true }, - _editEdge: { + _getDistanceToLine: { + value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { + var px = x2 - x1; + var py = y2 - y1; + var something = px * px + py * py; + var u = ((x3 - x1) * px + (y3 - y1) * py) / something; - /** - * connect two nodes with a new edge. - * - * @private - */ - value: function _editEdge(sourceNodeId, targetNodeId) { - var _this = this; - var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; - console.log(defaultData); - if (this.options.handlerFunctions.editEdge) { - if (this.options.handlerFunctions.editEdge.length == 2) { - this.options.handlerFunctions.editEdge(defaultData, function (finalizedData) { - _this.body.data.edges.update(finalizedData); - _this.selectionHandler.unselectAll(); - _this.createManipulatorBar(); - }); - } else { - throw new Error("The function for edit does not support two arguments (data, callback)"); - } - } else { - this.body.data.edges.update(defaultData); - this.selectionHandler.unselectAll(); - this.createManipulatorBar(); + if (u > 1) { + u = 1; + } else if (u < 0) { + u = 0; } + + var x = x1 + u * px; + var y = y1 + u * py; + var dx = x - x3; + var dy = y - y3; + + //# Note: If the actual distance does not matter, + //# if you only want to compare what this function + //# returns to other results of this function, you + //# can just return the squared distance instead + //# (i.e. remove the sqrt) to gain a little performance + + return Math.sqrt(dx * dx + dy * dy); }, writable: true, configurable: true }, - _editNode: { + drawArrowHead: { /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. * - * @private + * @param ctx + * @param position + * @param viaNode */ - value: function _editNode() { - var _this = this; - if (this.options.handlerFunctions.edit && this.editMode == true) { - var node = this._getSelectedNode(); - var data = { - id: node.id, - label: node.label, - group: node.options.group, - shape: node.options.shape, - color: { - background: node.options.color.background, - border: node.options.color.border, - highlight: { - background: node.options.color.highlight.background, - border: node.options.color.highlight.border - } + value: function drawArrowHead(ctx, position, viaNode, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this.getLineWidth(selected, hover); + + // set lets + var angle = undefined; + var length = undefined; + var arrowPos = undefined; + var node1 = undefined; + var node2 = undefined; + var guideOffset = undefined; + var scaleFactor = undefined; + + if (position == "from") { + node1 = this.from; + node2 = this.to; + guideOffset = 0.1; + scaleFactor = this.options.arrows.from.scaleFactor; + } else if (position == "to") { + node1 = this.to; + node2 = this.from; + guideOffset = -0.1; + scaleFactor = this.options.arrows.to.scaleFactor; + } else { + node1 = this.to; + node2 = this.from; + scaleFactor = this.options.arrows.middle.scaleFactor; + } + + // if not connected to itself + if (node1 != node2) { + if (position !== "middle") { + // draw arrow head + if (this.options.smooth.enabled == true) { + arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); + var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); + angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + } else { + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.findBorderPosition(node1, ctx); } - }; - if (this.options.handlerFunctions.edit.length == 2) { - (function () { - var me = _this; - _this.options.handlerFunctions.edit(data, function (finalizedData) { - me.body.data.nodes.update(finalizedData); - me.createManipulatorBar(); - me.moving = true; - me.start(); - }); - })(); } else { - throw new Error("The function for edit does not support two arguments (data, callback)"); + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. } + // draw arrow at the end of the line + length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(arrowPos.x, arrowPos.y, angle, length); + ctx.fill(); + ctx.stroke(); } else { - throw new Error("No edit function has been bound to this button"); - } - }, - writable: true, - configurable: true - }, - deleteSelected: { + // draw circle + var _angle = undefined, + point = undefined; + var _getCircleData = this._getCircleData(); + var _getCircleData2 = _slicedToArray(_getCircleData, 3); - /** - * delete everything in the selection - * - * @private - */ - value: function deleteSelected() { - var _this = this; - var selectedNodes = this.selectionHandler.getSelectedNodes(); - var selectedEdges = this.selectionHandler.getSelectedEdges(); - var deleteFunction = undefined; - if (selectedNodes.length > 0) { - for (var i = 0; i < selectedNodes.length; i++) { - if (this.body.nodes[selectedNodes[i]].isCluster === true) { - alert("You cannot delete a cluster."); - return; - } - } + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; - if (typeof this.options.handlerFunctions.deleteNode === "function") { - deleteFunction = this.options.handlerFunctions.deleteNode; - } - } else if (selectedEdges.length > 0) { - if (typeof this.options.handlerFunctions.deleteEdge === "function") { - deleteFunction = this.options.handlerFunctions.deleteEdge; - } - } - if (typeof deleteFunction === "function") { - var data = { nodes: selectedNodes, edges: selectedEdges }; - if (deleteFunction.length == 2) { - deleteFunction(data, function (finalizedData) { - _this.body.data.edges.remove(finalizedData.edges); - _this.body.data.nodes.remove(finalizedData.nodes); - _this.body.emitter.emit("startSimulation"); - }); + if (position == "from") { + point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; + } else if (position == "to") { + point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.6, high: 1, direction: 1 }); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; } else { - throw new Error("The function for delete does not support two arguments (data, callback)"); + point = this._pointOnCircle(x, y, radius, 0.175); + _angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; } - } else { - this.body.data.edges.remove(selectedEdges); - this.body.data.nodes.remove(selectedNodes); - this.body.emitter.emit("startSimulation"); + + // draw the arrowhead + var _length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(point.x, point.y, _angle, _length); + ctx.fill(); + ctx.stroke(); } }, writable: true, configurable: true - }, - getTargetNodeProperties: { - value: function getTargetNodeProperties(x, y) { - return { - id: "targetNode" + util.randomUUID(), - hidden: false, - physics: false, - shape: "dot", - size: 6, - x: x, - y: y, - color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } }, - borderWidth: 2, - borderWidthSelected: 2 - }; - }, - writable: true, - configurable: true } }); - return ManipulationSystem; + return EdgeBase; })(); - module.exports = ManipulationSystem; - -/***/ }, -/* 104 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - // English - exports.en = { - edit: "Edit", - del: "Delete selected", - back: "Back", - addNode: "Add Node", - addEdge: "Add Edge", - editNode: "Edit Node", - editEdge: "Edit Edge", - addDescription: "Click in an empty space to place a new node.", - edgeDescription: "Click on a node and drag the edge to another node to connect them.", - editEdgeDescription: "Click on the control points and drag them to a node to connect to it.", - createEdgeError: "Cannot link edges to a cluster.", - deleteClusterError: "Clusters cannot be deleted." - }; - exports.en_EN = exports.en; - exports.en_US = exports.en; - - // Dutch - exports.nl = { - edit: "Wijzigen", - del: "Selectie verwijderen", - back: "Terug", - addNode: "Node toevoegen", - addEdge: "Link toevoegen", - editNode: "Node wijzigen", - editEdge: "Link wijzigen", - addDescription: "Klik op een leeg gebied om een nieuwe node te maken.", - edgeDescription: "Klik op een node en sleep de link naar een andere node om ze te verbinden.", - editEdgeDescription: "Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.", - createEdgeError: "Kan geen link maken naar een cluster.", - deleteClusterError: "Clusters kunnen niet worden verwijderd." - }; - exports.nl_NL = exports.nl; - exports.nl_BE = exports.nl; + module.exports = EdgeBase; /***/ } /******/ ]) diff --git a/examples/network/01_basic_usage.html b/examples/network/01_basic_usage.html index b6f33261..665ed785 100644 --- a/examples/network/01_basic_usage.html +++ b/examples/network/01_basic_usage.html @@ -44,7 +44,17 @@ nodes: nodes, edges: edges }; - var options = {edges:{arrows:'to'},manipulation:{initiallyVisible: true}}//{physics:{stabilization:false}}; + var options = {edges:{arrows:'to'}, + manipulation:{ + initiallyVisible: true, + handlerFunctions: { + editNode: function(data, callback) { + console.log(data) + data.label = data.label + "i" + callback(data) + } + } + }}//{physics:{stabilization:false}}; var network = new vis.Network(container, data, options); diff --git a/lib/network/locales.js b/lib/network/locales.js index 2e3303b7..82bf0c55 100644 --- a/lib/network/locales.js +++ b/lib/network/locales.js @@ -11,7 +11,8 @@ exports['en'] = { edgeDescription: 'Click on a node and drag the edge to another node to connect them.', editEdgeDescription: 'Click on the control points and drag them to a node to connect to it.', createEdgeError: 'Cannot link edges to a cluster.', - deleteClusterError: 'Clusters cannot be deleted.' + deleteClusterError: 'Clusters cannot be deleted.', + editClusterError: 'Clusters cannot be edited.' }; exports['en_EN'] = exports['en']; exports['en_US'] = exports['en']; @@ -29,7 +30,8 @@ exports['nl'] = { edgeDescription: 'Klik op een node en sleep de link naar een andere node om ze te verbinden.', editEdgeDescription: 'Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.', createEdgeError: 'Kan geen link maken naar een cluster.', - deleteClusterError: 'Clusters kunnen niet worden verwijderd.' + deleteClusterError: 'Clusters kunnen niet worden verwijderd.', + editClusterError: 'Clusters kunnen niet worden aangepast.' }; exports['nl_NL'] = exports['nl']; exports['nl_BE'] = exports['nl']; diff --git a/lib/network/modules/ManipulationSystem.js b/lib/network/modules/ManipulationSystem.js index eb9668e8..d68f1e3a 100644 --- a/lib/network/modules/ManipulationSystem.js +++ b/lib/network/modules/ManipulationSystem.js @@ -19,9 +19,11 @@ class ManipulationSystem { this.manipulationDiv = undefined; this.editModeDiv = undefined; this.closeDiv = undefined; - this.boundFunction = undefined; + this.manipulationHammers = []; - this.cachedFunctions = {}; + this.temporaryUIFunctions = {}; + this.temporaryEventFunctions = []; + this.touchTime = 0; this.temporaryIds = {nodes: [], edges:[]}; this.guiEnabled = false; @@ -48,11 +50,23 @@ class ManipulationSystem { editEdge: undefined, deleteNode: undefined, deleteEdge: undefined + }, + controlNodeStyle:{ + shape:'dot', + size:6, + color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968'}}, + borderWidth: 2, + borderWidthSelected: 2 } } util.extend(this.options, this.defaultOptions); } + + /** + * Set the Options + * @param options + */ setOptions(options) { if (options !== undefined) { if (typeof options == 'boolean') { @@ -69,11 +83,298 @@ class ManipulationSystem { if (this.options.initiallyVisible === true) { this.editMode = true; } - this.init(); + this._setup(); + } + } + + + /** + * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. + * + * @private + */ + toggleEditMode() { + this.editMode = !this.editMode; + if (this.guiEnabled === true) { + let toolbar = this.manipulationDiv; + let closeDiv = this.closeDiv; + let editModeDiv = this.editModeDiv; + if (this.editMode === true) { + toolbar.style.display = "block"; + closeDiv.style.display = "block"; + editModeDiv.style.display = "none"; + this._bindHammerToDiv(closeDiv, this.toggleEditMode.bind(this)); + this.showManipulatorToolbar(); + } + else { + toolbar.style.display = "none"; + closeDiv.style.display = "none"; + editModeDiv.style.display = "block"; + this._createEditButton(); + } + } + } + + + /** + * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * + * @private + */ + showManipulatorToolbar() { + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); + + // reset global letiables + this.manipulationDOM = {}; + + let selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); + let selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); + let selectedTotalCount = selectedNodeCount + selectedEdgeCount; + let locale = this.options.locales[this.options.locale]; + let needSeperator = false; + + if (this.options.functionality.addNode === true) { + this._createAddNodeButton(locale); + needSeperator = true; + } + if (this.options.functionality.addEdge === true) { + if (needSeperator === true) {this._createSeperator(1);} else {needSeperator = true;} + this._createAddEdgeButton(locale); + } + + if (selectedNodeCount === 1 && typeof this.options.handlerFunctions.editNode === 'function' && this.options.functionality.editNode === true) { + if (needSeperator === true) {this._createSeperator(2);} else {needSeperator = true;} + this._createEditNodeButton(locale); + } + else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.functionality.editEdge === true) { + if (needSeperator === true) {this._createSeperator(3);} else {needSeperator = true;} + this._createEditEdgeButton(locale); + } + + // remove buttons + if (selectedTotalCount !== 0) { + if (selectedNodeCount === 1 && this.options.functionality.deleteNode === true) { + if (needSeperator === true) {this._createSeperator(4);} + this._createDeleteButton(locale); + } + else if (selectedNodeCount === 0 && this.options.functionality.deleteEdge === true) { + if (needSeperator === true) {this._createSeperator(4);} + this._createDeleteButton(locale); + } + } + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + + // refresh this bar based on what has been selected + this._temporaryBindEvent('select', this.showManipulatorToolbar.bind(this)); + + // redraw to show any possible changes + this.body.emitter.emit('_redraw'); + } + + + /** + * Create the toolbar for adding Nodes + * + * @private + */ + addNodeMode() { + // clear the toolbar + this._clean(); + + if (this.guiEnabled === true) { + let locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['addDescription']) + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } + + this._temporaryBindEvent('click', this._performAddNode.bind(this)); + } + + /** + * call the bound function to handle the editing of the node. The node has to be selected. + * + * @private + */ + editNode() { + if (typeof this.options.handlerFunctions.editNode === 'function') { + let node = this.selectionHandler._getSelectedNode(); + if (node.isCluster !== true) { + let data = util.deepExtend({}, node.options, true); + data.x = node.x; + data.y = node.y; + + if (this.options.handlerFunctions.editNode.length == 2) { + this.options.handlerFunctions.editNode(data, (finalizedData) => { + this.body.data.nodes.update(finalizedData); + this.showManipulatorToolbar(); + }); + } + else { + throw new Error('The function for edit does not support two arguments (data, callback)'); + } + } + else { + alert(this.options.locales[this.options.locale]["editClusterError"]); + } + } + else { + throw new Error('No function has been configured to handle the editing of nodes.'); } } - init() { + + /** + * create the toolbar to connect nodes + * + * @private + */ + addEdgeMode() { + // _clean the system + this._clean(); + + if (this.guiEnabled === true) { + let locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['edgeDescription']); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } + + // temporarily overload functions + this._temporaryBindUI('onTouch', this._handleConnect.bind(this)); + this._temporaryBindUI('onDragEnd', this._finishConnect.bind(this)); + this._temporaryBindUI('onHold', () => {}); + } + + /** + * create the toolbar to edit edges + * + * @private + */ + editEdgeMode() { + // clear the system + this._clean(); + + if (this.guiEnabled === true) { + let locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale['editEdgeDescription']); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } + + this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; + let edge = this.body.edges[this.edgeBeingEditedId]; + + // create control nodes + let controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); + let controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); + + this.temporaryIds.nodes.push(controlNodeFrom.id); + this.temporaryIds.nodes.push(controlNodeTo.id); + + this.body.nodes[controlNodeFrom.id] = controlNodeFrom; + this.body.nodeIndices.push(controlNodeFrom.id); + this.body.nodes[controlNodeTo.id] = controlNodeTo; + this.body.nodeIndices.push(controlNodeTo.id); + + // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI + this._temporaryBindUI('onTouch', this._controlNodeTouch.bind(this)); // used to get the position + this._temporaryBindUI('onTap', () => {}); // disabled + this._temporaryBindUI('onHold', () => {}); // disabled + this._temporaryBindUI('onDragStart', this._controlNodeDragStart.bind(this));// used to select control node + this._temporaryBindUI('onDrag', this._controlNodeDrag.bind(this)); // used to drag control node + this._temporaryBindUI('onDragEnd', this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes + this._temporaryBindUI('onMouseMove', () => {}); // disabled + + // create function to position control nodes correctly on movement + // automatically cleaned up because we use the temporary bind + this._temporaryBindEvent('beforeDrawing', (ctx) => { + let positions = edge.edgeType.findBorderPositions(ctx); + if (controlNodeFrom.selected === false) { + controlNodeFrom.x = positions.from.x; + controlNodeFrom.y = positions.from.y; + } + if (controlNodeTo.selected === false) { + controlNodeTo.x = positions.to.x; + controlNodeTo.y = positions.to.y; + } + }); + + this.body.emitter.emit('_redraw'); + } + + /** + * delete everything in the selection + * + * @private + */ + deleteSelected() { + let selectedNodes = this.selectionHandler.getSelectedNodes(); + let selectedEdges = this.selectionHandler.getSelectedEdges(); + let deleteFunction = undefined; + if (selectedNodes.length > 0) { + for (let i = 0; i < selectedNodes.length; i++) { + if (this.body.nodes[selectedNodes[i]].isCluster === true) { + alert(this.options.locales[this.options.locale]["deleteClusterError"]); + return; + } + } + + if (typeof this.options.handlerFunctions.deleteNode === 'function') { + deleteFunction = this.options.handlerFunctions.deleteNode; + } + } + else if (selectedEdges.length > 0) { + if (typeof this.options.handlerFunctions.deleteEdge === 'function') { + deleteFunction = this.options.handlerFunctions.deleteEdge; + } + } + + if (typeof deleteFunction === 'function') { + let data = {nodes: selectedNodes, edges: selectedEdges}; + if (deleteFunction.length == 2) { + deleteFunction(data, (finalizedData) => { + this.body.data.edges.remove(finalizedData.edges); + this.body.data.nodes.remove(finalizedData.nodes); + this.body.emitter.emit("startSimulation"); + }); + } + else { + throw new Error('The function for delete does not support two arguments (data, callback)') + } + } + else { + this.body.data.edges.remove(selectedEdges); + this.body.data.nodes.remove(selectedNodes); + this.body.emitter.emit("startSimulation"); + } + } + + + + + //********************************************** PRIVATE ***************************************// + + /** + * draw or remove the DOM + * @private + */ + _setup() { if (this.options.enabled === true) { // Enable the GUI this.guiEnabled = true; @@ -81,23 +382,28 @@ class ManipulationSystem { // remove override this.selectionHandler.forceSelectEdges = true; - this.createWrappers(); + this._createWrappers(); if (this.editMode === false) { - this.createEditButton(); + this._createEditButton(); } else { - this.createManipulatorBar(); + this.showManipulatorToolbar(); } } else { - this.removeManipulationDOM(); + this._removeManipulationDOM(); // disable the gui this.guiEnabled = false; } } - createWrappers() { + + /** + * create the div overlays that contain the DOM + * @private + */ + _createWrappers() { // load the manipulator HTML elements. All styling done in css. if (this.manipulationDiv === undefined) { this.manipulationDiv = document.createElement('div'); @@ -111,6 +417,7 @@ class ManipulationSystem { this.canvas.frame.appendChild(this.manipulationDiv); } + // container for the edit button. if (this.editModeDiv === undefined) { this.editModeDiv = document.createElement('div'); this.editModeDiv.className = 'network-manipulation-editMode'; @@ -123,6 +430,8 @@ class ManipulationSystem { this.canvas.frame.appendChild(this.editModeDiv); } + + // container for the close div button if (this.closeDiv === undefined) { this.closeDiv = document.createElement('div'); this.closeDiv.className = 'network-manipulation-closeDiv'; @@ -132,10 +441,30 @@ class ManipulationSystem { } + /** + * generate a new target node. Used for creating new edges and editing edges + * @param x + * @param y + * @returns {*} + * @private + */ + _getNewTargetNode(x,y) { + let controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); + + controlNodeStyle.id = 'targetNode' + util.randomUUID(); + controlNodeStyle.hidden = false; + controlNodeStyle.physics = false; + controlNodeStyle.x = x; + controlNodeStyle.y = y; + + return this.body.functions.createNode(controlNodeStyle); + } + + /** * Create the edit button */ - createEditButton() { + _createEditButton() { // restore everything to it's original state (if applicable) this._clean(); @@ -147,45 +476,46 @@ class ManipulationSystem { // create the contents for the editMode button let locale = this.options.locales[this.options.locale]; - let button = this.createButton('editMode', 'network-manipulationUI edit editmode', locale['edit']); + let button = this._createButton('editMode', 'network-manipulationUI edit editmode', locale['edit']); this.editModeDiv.appendChild(button); // bind a hammer listener to the button, calling the function toggleEditMode. - this.bindHammerToDiv(button, 'toggleEditMode'); + this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); } - removeManipulationDOM() { - // removes all the bindings and overloads - this._clean(); + /** + * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. + * @private + */ + _clean() { + // _clean the divs + if (this.guiEnabled === true) { + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.manipulationDiv); - // empty the manipulation divs - util.recursiveDOMDelete(this.manipulationDiv); - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.closeDiv); + // removes all the bindings and overloads + this._cleanManipulatorHammers(); + } - // remove the manipulation divs - this.canvas.frame.removeChild(this.manipulationDiv); - this.canvas.frame.removeChild(this.editModeDiv); - this.canvas.frame.removeChild(this.closeDiv); + // remove temporary nodes and edges + this._cleanupTemporaryNodesAndEdges(); - // set the references to undefined - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + // restore overloaded UI functions + this._unbindTemporaryUIs(); - // remove override - this.selectionHandler.forceSelectEdges = false; - } + // remove the temporaryEventFunctions + this._unbindTemporaryEvents(); - //clearManipulatorBar() { - // util._recursiveDOMDelete(this.manipulationDiv); - // this.manipulationDOM = {}; - // this._cleanManipulatorHammers(); - // this._manipulationReleaseOverload(); - //} + // restore the physics if required + this.body.emitter.emit("restorePhysics"); + } + /** + * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. + * @private + */ _cleanManipulatorHammers() { // _clean hammer bindings if (this.manipulationHammers.length != 0) { @@ -196,121 +526,85 @@ class ManipulationSystem { } } - /** - * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore - * these functions to their original functionality, we saved them in this.cachedFunctions. - * This function restores these functions to their original function. - * - * @private - */ - _restoreOverloadedFunctions() { - for (let functionName in this.cachedFunctions) { - if (this.cachedFunctions.hasOwnProperty(functionName)) { - this.body.eventListeners[functionName] = this.cachedFunctions[functionName]; - delete this.cachedFunctions[functionName]; - } - } - this.cachedFunctions = {}; - } /** - * Enable or disable edit-mode. - * + * Remove all DOM elements created by this module. * @private */ - toggleEditMode() { - this.editMode = !this.editMode; - let toolbar = this.manipulationDiv; - let closeDiv = this.closeDiv; - let editModeDiv = this.editModeDiv; - if (this.editMode === true) { - toolbar.style.display = "block"; - closeDiv.style.display = "block"; - editModeDiv.style.display = "none"; - this.bindHammerToDiv(closeDiv, 'toggleEditMode'); - this.createManipulatorBar(); - } - else { - toolbar.style.display = "none"; - closeDiv.style.display = "none"; - editModeDiv.style.display = "block"; - this.createEditButton(); - } - - } - - _clean() { - // _clean the divs - if (this.guiEnabled === true) { - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.manipulationDiv); + _removeManipulationDOM() { + // removes all the bindings and overloads + this._clean(); - // removes all the bindings and overloads - this._cleanManipulatorHammers(); - } + // empty the manipulation divs + util.recursiveDOMDelete(this.manipulationDiv); + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.closeDiv); - // remove temporary nodes and edges - this._cleanupTemporaryNodesAndEdges(); + // remove the manipulation divs + this.canvas.frame.removeChild(this.manipulationDiv); + this.canvas.frame.removeChild(this.editModeDiv); + this.canvas.frame.removeChild(this.closeDiv); - // restore overloaded UI functions - this._restoreOverloadedFunctions(); + // set the references to undefined + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; - // remove the boundFunction - if (this.boundFunction !== undefined) { - this.body.emitter.off(this.boundFunction.event, this.boundFunction.fn); - } - this.boundFunction = undefined; + // remove override + this.selectionHandler.forceSelectEdges = false; } - createSeperator(index = 1) { + + /** + * create a seperator line. the index is to differentiate in the manipulation dom + * @param index + * @private + */ + _createSeperator(index = 1) { this.manipulationDOM['seperatorLineDiv' + index] = document.createElement('div'); this.manipulationDOM['seperatorLineDiv' + index].className = 'network-seperatorLine'; this.manipulationDiv.appendChild(this.manipulationDOM['seperatorLineDiv' + index]); } - createAddNodeButton(locale) { - let button = this.createButton('addNode', 'network-manipulationUI add', locale['addNode']); - this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, 'addNodeMode'); - } - - createAddEdgeButton(locale) { - let button = this.createButton('addEdge', 'network-manipulationUI connect', locale['addEdge']); + // ---------------------- DOM functions for buttons --------------------------// + + _createAddNodeButton(locale) { + let button = this._createButton('addNode', 'network-manipulationUI add', locale['addNode']); this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, 'addEdgeMode'); + this._bindHammerToDiv(button, this.addNodeMode.bind(this)); } - createEditNodeButton(locale) { - let button = this.createButton('editNode', 'network-manipulationUI edit', locale['editNode']); + _createAddEdgeButton(locale) { + let button = this._createButton('addEdge', 'network-manipulationUI connect', locale['addEdge']); this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, '_editNode'); + this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); } - createEditEdgeButton(locale) { - let button = this.createButton('editEdge', 'network-manipulationUI edit', locale['editEdge']); + _createEditNodeButton(locale) { + let button = this._createButton('editNode', 'network-manipulationUI edit', locale['editNode']); this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, 'editEdgeMode'); + this._bindHammerToDiv(button, this.editNode.bind(this)); } - createDeleteButton(locale) { - let button = this.createButton('delete', 'network-manipulationUI delete', locale['del']); + _createEditEdgeButton(locale) { + let button = this._createButton('editEdge', 'network-manipulationUI edit', locale['editEdge']); this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, 'deleteSelected'); + this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); } - createBackButton(locale) { - let button = this.createButton('back', 'network-manipulationUI back', locale['back']); + _createDeleteButton(locale) { + let button = this._createButton('delete', 'network-manipulationUI delete', locale['del']); this.manipulationDiv.appendChild(button); - this.bindHammerToDiv(button, 'createManipulatorBar'); + this._bindHammerToDiv(button, this.deleteSelected.bind(this)); } - createDescription(label) { - this.manipulationDiv.appendChild( - this.createButton('description', 'network-manipulationUI none', label) - ); + _createBackButton(locale) { + let button = this._createButton('back', 'network-manipulationUI back', locale['back']); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); } - createButton(id, className, label, labelClassName = 'network-manipulationLabel') { + _createButton(id, className, label, labelClassName = 'network-manipulationLabel') { this.manipulationDOM[id+"Div"] = document.createElement('div'); this.manipulationDOM[id+"Div"].className = className; this.manipulationDOM[id+"Label"] = document.createElement('div'); @@ -320,209 +614,122 @@ class ManipulationSystem { return this.manipulationDOM[id+"Div"]; } - temporaryBind(fn, event) { - this.boundFunction = {fn:fn.bind(this), event}; - this.body.emitter.on(event, this.boundFunction.fn); + _createDescription(label) { + this.manipulationDiv.appendChild( + this._createButton('description', 'network-manipulationUI none', label) + ); } + // -------------------------- End of DOM functions for buttons ------------------------------// + /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * + * this binds an event until cleanup by the clean functions. + * @param event + * @param newFunction * @private */ - createManipulatorBar() { - this._clean(); - - // resume calculation - this.body.emitter.emit("restorePhysics"); - - // reset global letiables - this.manipulationDOM = {}; - - let selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); - let selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); - let selectedTotalCount = selectedNodeCount + selectedEdgeCount; - let locale = this.options.locales[this.options.locale]; - let needSeperator = false; - - if (this.options.functionality.addNode === true) { - this.createAddNodeButton(locale); - needSeperator = true; - } - if (this.options.functionality.addEdge === true) { - if (needSeperator === true) {this.createSeperator(1);} else {needSeperator = true;} - this.createAddEdgeButton(locale); - } + _temporaryBindEvent(event, newFunction) { + this.temporaryEventFunctions.push({event:event, boundFunction:newFunction}); + this.body.emitter.on(event, newFunction); + } - if (selectedNodeCount === 1 && typeof this.options.handlerFunctions.editNode === 'function' && this.options.functionality.editNode === true) { - if (needSeperator === true) {this.createSeperator(2);} else {needSeperator = true;} - this.createEditNodeButton(locale); + /** + * this overrides an UI function until cleanup by the clean function + * @param UIfunctionName + * @param newFunction + * @private + */ + _temporaryBindUI(UIfunctionName, newFunction) { + if (this.body.eventListeners[UIfunctionName] !== undefined) { + this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; + this.body.eventListeners[UIfunctionName] = newFunction; } - else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.functionality.editEdge === true) { - if (needSeperator === true) {this.createSeperator(3);} else {needSeperator = true;} - this.createEditEdgeButton(locale); + else { + throw new Error('This UI function does not exist. Typo? You tried: "' + UIfunctionName + '" possible are: ' + JSON.stringify(Object.keys(this.body.eventListeners))); } + } - // remove buttons - if (selectedTotalCount !== 0) { - if (selectedNodeCount === 1 && this.options.functionality.deleteNode === true) { - if (needSeperator === true) {this.createSeperator(4);} - this.createDeleteButton(locale); - } - else if (selectedNodeCount === 0 && this.options.functionality.deleteEdge === true) { - if (needSeperator === true) {this.createSeperator(4);} - this.createDeleteButton(locale); + /** + * Restore the overridden UI functions to their original state. + * + * @private + */ + _unbindTemporaryUIs() { + for (let functionName in this.temporaryUIFunctions) { + if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { + this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; + delete this.temporaryUIFunctions[functionName]; } } + this.temporaryUIFunctions = {}; + } - // bind the close button - this.bindHammerToDiv(this.closeDiv, 'toggleEditMode'); - - // refresh this bar based on what has been selected - this.temporaryBind(this.createManipulatorBar,'select'); + /** + * Unbind the events created by _temporaryBindEvent + * @private + */ + _unbindTemporaryEvents() { + for (let i = 0; i < this.temporaryEventFunctions.length; i++) { + let eventName = this.temporaryEventFunctions[i].event; + let boundFunction = this.temporaryEventFunctions[i].boundFunction; + this.body.emitter.off(eventName, boundFunction); + } + this.temporaryEventFunctions = []; } /** - * Bind an hammer instance to a DOM element. TODO: remove the double check. + * Bind an hammer instance to a DOM element. * @param domElement * @param funct */ - bindHammerToDiv(domElement, funct) { + _bindHammerToDiv(domElement, boundFunction) { let hammer = new Hammer(domElement, {}); - hammerUtil.onTouch(hammer, this[funct].bind(this)); + hammerUtil.onTouch(hammer, boundFunction); this.manipulationHammers.push(hammer); } /** - * Create the toolbar for adding Nodes - * + * Neatly clean up temporary edges and nodes * @private */ - addNodeMode() { - // clear the toolbar - this._clean(); - - if (this.guiEnabled === true) { - let locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this.createBackButton(locale); - this.createSeperator(); - this.createDescription(locale['addDescription']) - - // bind the close button - this.bindHammerToDiv(this.closeDiv, 'toggleEditMode'); + _cleanupTemporaryNodesAndEdges() { + // _clean temporary edges + for (let i = 0; i < this.temporaryIds.edges.length; i++) { + this.body.edges[this.temporaryIds.edges[i]].disconnect(); + delete this.body.edges[this.temporaryIds.edges[i]]; + let indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); + if (indexTempEdge !== -1) {this.body.edgeIndices.splice(indexTempEdge,1);} } - this.temporaryBind(this._addNode,'click'); - } - - - /** - * create the toolbar to connect nodes - * - * @private - */ - addEdgeMode() { - // _clean the system - this._clean(); - - if (this.guiEnabled === true) { - let locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this.createBackButton(locale); - this.createSeperator(); - this.createDescription(locale['edgeDescription']); - - // bind the close button - this.bindHammerToDiv(this.closeDiv, 'toggleEditMode'); + // _clean temporary nodes + for (let i = 0; i < this.temporaryIds.nodes.length; i++) { + delete this.body.nodes[this.temporaryIds.nodes[i]]; + let indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); + if (indexTempNode !== -1) {this.body.nodeIndices.splice(indexTempNode,1);} } - // temporarily overload functions - this.cachedFunctions["onTouch"] = this.body.eventListeners.onTouch; - this.cachedFunctions["onDragEnd"] = this.body.eventListeners.onDragEnd; - this.cachedFunctions["onHold"] = this.body.eventListeners.onHold; - - this.body.eventListeners.onTouch = this._handleConnect.bind(this); - this.body.eventListeners.onDragEnd = this._finishConnect.bind(this); - this.body.eventListeners.onHold = function () {}; + this.temporaryIds = {nodes: [], edges: []}; } + // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// + /** - * create the toolbar to edit edges - * + * the touch is used to get the position of the initial click + * @param event * @private */ - editEdgeMode() { - // clear the system - this._clean(); - - if (this.guiEnabled === true) { - let locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this.createBackButton(locale); - this.createSeperator(); - this.createDescription(locale['editEdgeDescription']); - - // bind the close button - this.bindHammerToDiv(this.closeDiv, 'toggleEditMode'); - } - - this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; - let edge = this.body.edges[this.edgeBeingEditedId]; - - // create control nodes - let controlNodeFrom = this.body.functions.createNode(this.getTargetNodeProperties(edge.from.x, edge.from.y)); - let controlNodeTo = this.body.functions.createNode(this.getTargetNodeProperties(edge.to.x, edge.to.y)); - - this.temporaryIds.nodes.push(controlNodeFrom.id); - this.temporaryIds.nodes.push(controlNodeTo.id); - - this.body.nodes[controlNodeFrom.id] = controlNodeFrom; - this.body.nodeIndices.push(controlNodeFrom.id); - this.body.nodes[controlNodeTo.id] = controlNodeTo; - this.body.nodeIndices.push(controlNodeTo.id); - - // temporarily overload functions - this.cachedFunctions['onTouch'] = this.body.eventListeners.onTouch; - this.cachedFunctions['onTap'] = this.body.eventListeners.onTap; - this.cachedFunctions['onHold'] = this.body.eventListeners.onHold; - this.cachedFunctions['onDragStart'] = this.body.eventListeners.onDragStart; - this.cachedFunctions['onDrag'] = this.body.eventListeners.onDrag; - this.cachedFunctions['onDragEnd'] = this.body.eventListeners.onDragEnd; - this.cachedFunctions['onMouseOver'] = this.body.eventListeners.onMouseOver; - - this.body.eventListeners.onTouch = this._controlNodeTouch.bind(this); - this.body.eventListeners.onTap = function() {}; - this.body.eventListeners.onHold = function() {}; - this.body.eventListeners.onDragStart= this._controlNodeDragStart.bind(this); - this.body.eventListeners.onDrag = this._controlNodeDrag.bind(this); - this.body.eventListeners.onDragEnd = this._controlNodeDragEnd.bind(this); - this.body.eventListeners.onMouseOver= function() {} - - // create function to position control nodes correctly on movement - let positionControlNodes = (ctx) => { - let positions = edge.edgeType.findBorderPositions(ctx); - if (controlNodeFrom.selected === false) { - controlNodeFrom.x = positions.from.x; - controlNodeFrom.y = positions.from.y; - } - if (controlNodeTo.selected === false) { - controlNodeTo.x = positions.to.x; - controlNodeTo.y = positions.to.y; - } - } - this.temporaryBind(positionControlNodes, "beforeDrawing"); - - this.body.emitter.emit("_redraw"); - } - _controlNodeTouch(event) { this.lastTouch = this.body.functions.getPointer(event.center); this.lastTouch.translation = util.extend({},this.body.view.translation); // copy the object } + /** + * the drag start is used to mark one of the control nodes as selected. + * @param event + * @private + */ _controlNodeDragStart(event) { let pointer = this.lastTouch; let pointerObj = this.selectionHandler._pointerToPositionObject(pointer); @@ -546,6 +753,11 @@ class ManipulationSystem { this.body.emitter.emit("_redraw"); } + /** + * dragging the control nodes or the canvas + * @param event + * @private + */ _controlNodeDrag(event) { this.body.emitter.emit("disablePhysics"); let pointer = this.body.functions.getPointer(event.center); @@ -564,6 +776,12 @@ class ManipulationSystem { this.body.emitter.emit("_redraw"); } + + /** + * connecting or restoring the control nodes. + * @param event + * @private + */ _controlNodeDragEnd(event) { let pointer = this.body.functions.getPointer(event.center); let pointerObj = this.selectionHandler._pointerToPositionObject(pointer); @@ -586,10 +804,10 @@ class ManipulationSystem { else { let from = this.body.nodes[this.temporaryIds.nodes[0]]; if (this.selectedControlNode.id == from.id) { - this._editEdge(node.id, edge.to.id); + this._performEditEdge(node.id, edge.to.id); } else { - this._editEdge(edge.from.id, node.id); + this._performEditEdge(edge.from.id, node.id); } } } @@ -599,80 +817,12 @@ class ManipulationSystem { } this.body.emitter.emit("_redraw"); } - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - _selectControlNode(event) { - - } + // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// - /** - * - * @param pointer - * @private - */ - _releaseControlNode(pointer) { - if (new Date().valueOf() - this.touchTime > 100) { - console.log("release") - // perform the connection - let node = this.selectionHandler.getNodeAt(pointer); - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]["createEdgeError"]) - } - else { - let edge = this.body.edges[this.edgeBeingEditedId]; - - let targetNodeId = undefined; - if (edge.to.selected === true) { - targetNodeId = edge.toId; - } - else if (edge.from.selected === true) { - targetNodeId = edge.fromId; - } - //this.body.eventListeners.onDrag = this.cachedFunctions["onDrag"]; - //this.body.eventListeners.onRelease = this.cachedFunctions["onRelease"]; - //delete this.cachedFunctions["onRelease"]; - //delete this.cachedFunctions["onDrag"]; - //// - // - // - // - // - // - // - //if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { - // this._createEdge(connectFromId, node.id); - //} - } - } - this.body.emitter.emit("_redraw"); - //this.body.emitter.emit("_redraw"); - //let newNode = this.getNodeAt(pointer); - //if (newNode !== undefined) { - // if (this.edgeBeingEditedId.controlNodes.from.selected == true) { - // this.edgeBeingEditedId._restoreControlNodes(); - // this._editEdge(newNode.id, this.edgeBeingEditedId.to.id); - // this.edgeBeingEditedId.controlNodes.from.unselect(); - // } - // if (this.edgeBeingEditedId.controlNodes.to.selected == true) { - // this.edgeBeingEditedId._restoreControlNodes(); - // this._editEdge(this.edgeBeingEditedId.from.id, newNode.id); - // this.edgeBeingEditedId.controlNodes.to.unselect(); - // } - //} - //else { - // this.edgeBeingEditedId._restoreControlNodes(); - //} - this.touchTime = new Date().valueOf(); - } - } + // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// /** * the function bound to the selection event. It checks if you want to connect a cluster and changes the description * to walk the user through the process. @@ -691,7 +841,7 @@ class ManipulationSystem { } else { // create a node the temporary line can look at - let targetNode = this.body.functions.createNode(this.getTargetNodeProperties(node.x,node.y)); + let targetNode = this._getNewTargetNode(node.x,node.y); let targetNodeId = targetNode.id; this.body.nodes[targetNode.id] = targetNode; this.body.nodeIndices.push(targetNode.id); @@ -715,7 +865,7 @@ class ManipulationSystem { this.temporaryIds.nodes.push(targetNode.id); this.temporaryIds.edges.push(connectionEdge.id); - this.cachedFunctions["onDrag"] = this.body.eventListeners.onDrag; + this.temporaryUIFunctions["onDrag"] = this.body.eventListeners.onDrag; this.body.eventListeners.onDrag = (event) => { let pointer = this.body.functions.getPointer(event.center); let targetNode = this.body.nodes[targetNodeId]; @@ -728,10 +878,15 @@ class ManipulationSystem { this.touchTime = new Date().valueOf(); // do the original touch events - this.cachedFunctions["onTouch"](event); + this.temporaryUIFunctions["onTouch"](event); } } + /** + * Connect the new edge to the target if one exists, otherwise remove temp line + * @param event + * @private + */ _finishConnect(event) { let pointer = this.body.functions.getPointer(event.center); let pointerObj = this.selectionHandler._pointerToPositionObject(pointer); @@ -743,9 +898,9 @@ class ManipulationSystem { } //restore the drag function - if (this.cachedFunctions["onDrag"] !== undefined) { - this.body.eventListeners.onDrag = this.cachedFunctions["onDrag"]; - delete this.cachedFunctions["onDrag"]; + if (this.temporaryUIFunctions["onDrag"] !== undefined) { + this.body.eventListeners.onDrag = this.temporaryUIFunctions["onDrag"]; + delete this.temporaryUIFunctions["onDrag"]; } // get the overlapping node but NOT the temporary node; @@ -764,40 +919,26 @@ class ManipulationSystem { // perform the connection if (node !== undefined) { if (node.isCluster === true) { - alert(this.options.locales[this.options.locale]["createEdgeError"]) + alert(this.options.locales[this.options.locale]["createEdgeError"]); } else { if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { - this._createEdge(connectFromId, node.id); + this._performCreateEdge(connectFromId, node.id); } } } this.body.emitter.emit("_redraw"); } - _cleanupTemporaryNodesAndEdges() { - // _clean temporary edges - for (let i = 0; i < this.temporaryIds.edges.length; i++) { - this.body.edges[this.temporaryIds.edges[i]].disconnect(); - delete this.body.edges[this.temporaryIds.edges[i]]; - let indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); - if (indexTempEdge !== -1) {this.body.edgeIndices.splice(indexTempEdge,1);} - } + // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// - // _clean temporary nodes - for (let i = 0; i < this.temporaryIds.nodes.length; i++) { - delete this.body.nodes[this.temporaryIds.nodes[i]]; - let indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); - if (indexTempNode !== -1) {this.body.nodeIndices.splice(indexTempNode,1);} - } - this.temporaryIds = {nodes: [], edges: []}; - } + // ------------------------------ Performing all the actual data manipulation ------------------------// /** * Adds a node on the specified location */ - _addNode(clickData) { + _performAddNode(clickData) { let defaultData = { id: util.randomUUID(), x: clickData.pointer.canvas.x, @@ -809,17 +950,17 @@ class ManipulationSystem { if (this.options.handlerFunctions.addNode.length == 2) { this.options.handlerFunctions.addNode(defaultData, (finalizedData) => { this.body.data.nodes.add(finalizedData); - this.createManipulatorBar(); + this.showManipulatorToolbar(); }); } else { throw new Error('The function for add does not support two arguments (data,callback)'); - this.createManipulatorBar(); + this.showManipulatorToolbar(); } } else { this.body.data.nodes.add(defaultData); - this.createManipulatorBar(); + this.showManipulatorToolbar(); } } @@ -829,14 +970,14 @@ class ManipulationSystem { * * @private */ - _createEdge(sourceNodeId, targetNodeId) { + _performCreateEdge(sourceNodeId, targetNodeId) { let defaultData = {from: sourceNodeId, to: targetNodeId}; if (this.options.handlerFunctions.addEdge) { if (this.options.handlerFunctions.addEdge.length == 2) { this.options.handlerFunctions.addEdge(defaultData, (finalizedData) => { this.body.data.edges.add(finalizedData); this.selectionHandler.unselectAll(); - this.createManipulatorBar(); + this.showManipulatorToolbar(); }); } else { @@ -846,7 +987,7 @@ class ManipulationSystem { else { this.body.data.edges.add(defaultData); this.selectionHandler.unselectAll(); - this.createManipulatorBar(); + this.showManipulatorToolbar(); } } @@ -855,15 +996,14 @@ class ManipulationSystem { * * @private */ - _editEdge(sourceNodeId, targetNodeId) { + _performEditEdge(sourceNodeId, targetNodeId) { let defaultData = {id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId}; - console.log(defaultData) if (this.options.handlerFunctions.editEdge) { if (this.options.handlerFunctions.editEdge.length == 2) { this.options.handlerFunctions.editEdge(defaultData, (finalizedData) => { this.body.data.edges.update(finalizedData); this.selectionHandler.unselectAll(); - this.createManipulatorBar(); + this.showManipulatorToolbar(); }); } else { @@ -873,112 +1013,11 @@ class ManipulationSystem { else { this.body.data.edges.update(defaultData); this.selectionHandler.unselectAll(); - this.createManipulatorBar(); + this.showManipulatorToolbar(); } } - /** - * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. - * - * @private - */ - _editNode() { - if (this.options.handlerFunctions.edit && this.editMode == true) { - let node = this._getSelectedNode(); - let data = { - id: node.id, - label: node.label, - group: node.options.group, - shape: node.options.shape, - color: { - background: node.options.color.background, - border: node.options.color.border, - highlight: { - background: node.options.color.highlight.background, - border: node.options.color.highlight.border - } - } - }; - if (this.options.handlerFunctions.edit.length == 2) { - let me = this; - this.options.handlerFunctions.edit(data, function (finalizedData) { - me.body.data.nodes.update(finalizedData); - me.createManipulatorBar(); - me.moving = true; - me.start(); - }); - } - else { - throw new Error('The function for edit does not support two arguments (data, callback)'); - } - } - else { - throw new Error('No edit function has been bound to this button'); - } - } - - - /** - * delete everything in the selection - * - * @private - */ - deleteSelected() { - let selectedNodes = this.selectionHandler.getSelectedNodes(); - let selectedEdges = this.selectionHandler.getSelectedEdges(); - let deleteFunction = undefined; - if (selectedNodes.length > 0) { - for (let i = 0; i < selectedNodes.length; i++) { - if (this.body.nodes[selectedNodes[i]].isCluster === true) { - alert("You cannot delete a cluster."); - return; - } - } - if (typeof this.options.handlerFunctions.deleteNode === 'function') { - deleteFunction = this.options.handlerFunctions.deleteNode; - } - } - else if (selectedEdges.length > 0) { - if (typeof this.options.handlerFunctions.deleteEdge === 'function') { - deleteFunction = this.options.handlerFunctions.deleteEdge; - } - } - - if (typeof deleteFunction === 'function') { - let data = {nodes: selectedNodes, edges: selectedEdges}; - if (deleteFunction.length == 2) { - deleteFunction(data, (finalizedData) => { - this.body.data.edges.remove(finalizedData.edges); - this.body.data.nodes.remove(finalizedData.nodes); - this.body.emitter.emit("startSimulation"); - }); - } - else { - throw new Error('The function for delete does not support two arguments (data, callback)') - } - } - else { - this.body.data.edges.remove(selectedEdges); - this.body.data.nodes.remove(selectedNodes); - this.body.emitter.emit("startSimulation"); - } - } - - getTargetNodeProperties(x,y) { - return { - id: 'targetNode' + util.randomUUID(), - hidden: false, - physics: false, - shape:'dot', - size:6, - x:x, - y:y, - color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968'}}, - borderWidth: 2, - borderWidthSelected: 2 - } - } } export default ManipulationSystem;