From a294f98686959152fd535dd0df1cdf3280c720a9 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Mon, 2 Mar 2015 18:45:23 +0100 Subject: [PATCH] working events like zoom and drag, testing on mobile still to be done. --- dist/vis.js | 54318 ++++++++-------- lib/network/Network.js | 91 +- lib/network/Node.js | 9 +- lib/network/mixins/SelectionMixin.js | 4 +- lib/network/modules/Canvas.js | 65 +- lib/network/modules/CanvasRenderer.js | 64 +- lib/network/modules/InteractionHandler.js | 462 + lib/network/modules/PhysicsEngine.js | 27 +- .../{components => }/SelectionHandler.js | 179 +- lib/network/modules/TouchEventHandler.js | 460 - lib/network/modules/View.js | 42 +- lib/util.js | 1 - 12 files changed, 27861 insertions(+), 27861 deletions(-) create mode 100644 lib/network/modules/InteractionHandler.js rename lib/network/modules/{components => }/SelectionHandler.js (82%) delete mode 100644 lib/network/modules/TouchEventHandler.js diff --git a/dist/vis.js b/dist/vis.js index 95a6a664..9766b6c4 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -85,67 +85,67 @@ return /******/ (function(modules) { // webpackBootstrap // utils exports.util = __webpack_require__(1); - exports.DOMutil = __webpack_require__(2); + exports.DOMutil = __webpack_require__(6); // data - exports.DataSet = __webpack_require__(3); - exports.DataView = __webpack_require__(4); - exports.Queue = __webpack_require__(5); + exports.DataSet = __webpack_require__(7); + exports.DataView = __webpack_require__(9); + exports.Queue = __webpack_require__(8); // Graph3d - exports.Graph3d = __webpack_require__(6); + exports.Graph3d = __webpack_require__(10); exports.graph3d = { - Camera: __webpack_require__(7), - Filter: __webpack_require__(8), - Point2d: __webpack_require__(9), - Point3d: __webpack_require__(10), - Slider: __webpack_require__(11), - StepNumber: __webpack_require__(12) + Camera: __webpack_require__(14), + Filter: __webpack_require__(15), + Point2d: __webpack_require__(13), + Point3d: __webpack_require__(12), + Slider: __webpack_require__(16), + StepNumber: __webpack_require__(17) }; // Timeline - exports.Timeline = __webpack_require__(13); - exports.Graph2d = __webpack_require__(14); + exports.Timeline = __webpack_require__(18); + exports.Graph2d = __webpack_require__(44); exports.timeline = { - DateUtil: __webpack_require__(15), - DataStep: __webpack_require__(16), - Range: __webpack_require__(17), - stack: __webpack_require__(18), - TimeStep: __webpack_require__(19), + DateUtil: __webpack_require__(26), + DataStep: __webpack_require__(47), + Range: __webpack_require__(23), + stack: __webpack_require__(31), + TimeStep: __webpack_require__(29), components: { items: { - Item: __webpack_require__(20), - BackgroundItem: __webpack_require__(21), - BoxItem: __webpack_require__(22), - PointItem: __webpack_require__(23), - RangeItem: __webpack_require__(24) + Item: __webpack_require__(33), + BackgroundItem: __webpack_require__(37), + BoxItem: __webpack_require__(35), + PointItem: __webpack_require__(36), + RangeItem: __webpack_require__(32) }, Component: __webpack_require__(25), - CurrentTime: __webpack_require__(26), - CustomTime: __webpack_require__(27), - DataAxis: __webpack_require__(28), - GraphGroup: __webpack_require__(29), + CurrentTime: __webpack_require__(43), + CustomTime: __webpack_require__(40), + DataAxis: __webpack_require__(46), + GraphGroup: __webpack_require__(48), Group: __webpack_require__(30), - BackgroundGroup: __webpack_require__(31), - ItemSet: __webpack_require__(32), - Legend: __webpack_require__(33), - LineGraph: __webpack_require__(34), - TimeAxis: __webpack_require__(35) + BackgroundGroup: __webpack_require__(34), + ItemSet: __webpack_require__(28), + Legend: __webpack_require__(52), + LineGraph: __webpack_require__(45), + TimeAxis: __webpack_require__(42) } }; // Network - exports.Network = __webpack_require__(36); + exports.Network = __webpack_require__(53); exports.network = { - Edge: __webpack_require__(37), - Groups: __webpack_require__(38), - Images: __webpack_require__(39), - Node: __webpack_require__(40), - Popup: __webpack_require__(41), - dotparser: __webpack_require__(42), - gephiParser: __webpack_require__(43) + Edge: __webpack_require__(54), + Groups: __webpack_require__(56), + Images: __webpack_require__(57), + Node: __webpack_require__(55), + Popup: __webpack_require__(58), + dotparser: __webpack_require__(59), + gephiParser: __webpack_require__(60) }; // Deprecated since v3.0.0 @@ -154,9 +154,9 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(44); - exports.hammer = __webpack_require__(45); // TODO: deprecate exports.hammer some day - exports.Hammer = __webpack_require__(45); + exports.moment = __webpack_require__(2); + exports.hammer = __webpack_require__(19); // TODO: deprecate exports.hammer some day + exports.Hammer = __webpack_require__(19); /***/ }, /* 1 */ @@ -168,7 +168,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__(44); + var moment = __webpack_require__(2); /** * Test whether given object is a number @@ -267,7 +267,6 @@ return /******/ (function(modules) { // webpackBootstrap } } } - return a; }; @@ -1391,30108 +1390,29872 @@ return /******/ (function(modules) { // webpackBootstrap "use strict"; - // 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); - } - }; + // 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); /***/ }, /* 3 */ /***/ function(module, exports, __webpack_require__) { - "use strict"; + 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 - var util = __webpack_require__(1); - var Queue = __webpack_require__(5); + (function (undefined) { + /************************************ + Constants + ************************************/ - /** - * 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, - var subscribers = []; - if (event in this._subscribers) { - subscribers = subscribers.concat(this._subscribers[event]); - } - if ("*" in this._subscribers) { - subscribers = subscribers.concat(this._subscribers["*"]); - } + // 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 + }, - for (var i = 0; i < subscribers.length; i++) { - var subscriber = subscribers[i]; - if (subscriber.callback) { - subscriber.callback(event, params, senderId || null); - } - } - }; + 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' + }, - /** - * 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; + camelFunctions = { + dayofyear : 'dayOfYear', + isoweekday : 'isoWeekday', + isoweek : 'isoWeek', + weekyear : 'weekYear', + isoweekyear : 'isoWeekYear' + }, - 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); - } + // format function strings + formatFunctions = {}, - 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"); - } + // 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 + }, - if (addedIds.length) { - this._trigger("add", { items: addedIds }, senderId); - } + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w W M D d'.split(' '), + paddedTokens = 'M D H h m s w W'.split(' '), - return addedIds; - }; + 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(); + } + }, - /** - * 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; + deprecations = {}, - 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); - } - }; + lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'], - 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); - } + updateInProgress = false; - addOrUpdate(item); + // 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'); + } } - } 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); - } + function hasOwnProp(a, b) { + return hasOwnProperty.call(a, b); + } - return addedIds.concat(updatedIds); - }; + 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 + }; + } - /** - * 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; + function printMsg(msg) { + if (moment.suppressDeprecationWarnings === false && + typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); + } + } - // 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]; - } + function deprecate(msg, fn) { + var firstTime = true; + return extend(function () { + if (firstTime) { + printMsg(msg); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } - // determine the return type - var returnType; - if (options && options.returnType) { - var allowedValues = ["DataTable", "Array", "Object"]; - returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + printMsg(msg); + deprecations[name] = true; + } + } - 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 + ")"); + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; } - if (returnType == "DataTable" && !util.isDataTable(data)) { - throw new Error("Parameter \"data\" must be a DataTable " + "when options.type is \"DataTable\""); + function ordinalizeToken(func, period) { + return function (a) { + return this.localeData().ordinal(func.call(this, a), period); + }; } - } else if (data) { - returnType = util.getType(data) == "DataTable" ? "DataTable" : "Array"; - } else { - returnType = "Array"; - } - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], - item, - itemId, - i, - len; + 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; - // 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); + 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); } - } - } - } - - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } - // filter fields of the items - if (options && options.fields) { - var fields = options.fields; - if (id != undefined) { - item = this._filterFields(item, fields); - } else { - for (i = 0, len = items.length; i < len; i++) { - items[i] = this._filterFields(items[i], fields); - } + return -(wholeMonthDiff + adjust); } - } - // 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]; + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], 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; - } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); } - } - }; - - /** - * 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 = []; + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); - if (filter) { - // get filtered items - if (order) { - // create ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (filter(item)) { - items.push(item); - } - } - } - this._sort(items, order); + function meridiemFixWrap(locale, hour, meridiem) { + var isPm; - 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]); - } + if (meridiem == null) { + // nothing to do + return hour; } - } - } - } else { - // get all items - if (order) { - // create an ordered list - items = []; - for (id in data) { - if (data.hasOwnProperty(id)) { - items.push(data[id]); + 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._sort(items, order); + /************************************ + Constructors + ************************************/ - 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]); + function Locale() { + } + + // 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 ids; - }; + // 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; - /** - * 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; - }; + // 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; - /** - * 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; + this._data = {}; - if (options && options.order) { - // execute forEach on ordered list - var items = this.get(options); + this._locale = moment.localeData(); - 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); - } - } + this._bubble(); } - } - }; - - /** - * Map every item in the dataset. - * @param {function} callback - * @param {Object} [options] Available options: - * {Object.} [type] - * {String[]} [fields] filter fields - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * @return {Object[]} mappedItems - */ - DataSet.prototype.map = function (callback, options) { - var filter = options && options.filter, - type = options && options.type || this._options.type, - mappedItems = [], - data = this._data, - item; - // convert and filter items - for (var id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - mappedItems.push(callback(item, id)); - } - } - } + /************************************ + Helpers + ************************************/ - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } - return mappedItems; - }; + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } + } - /** - * Filter the fields of an item - * @param {Object | null} item - * @param {String[]} fields Field names - * @return {Object | null} filteredItem or null if no item is provided - * @private - */ - DataSet.prototype._filterFields = function (item, fields) { - if (!item) { - // item is null - return item; - } + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } - var filteredItem = {}; + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } - for (var field in item) { - if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { - filteredItem[field] = item[field]; + return a; } - } - return filteredItem; - }; + function copyConfig(to, from) { + var i, prop, val; - /** - * 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"); - } - }; + 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; + } - /** - * 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 (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } - 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); + return to; } - } - if (removedIds.length) { - this._trigger("remove", { items: removedIds }, senderId); - } + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } - return removedIds; - }; + // 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; - /** - * Remove an item by its id - * @param {Number | String | Object} id id or item - * @returns {Number | String | null} id - * @private - */ - DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this._data[id]) { - delete this._data[id]; - this.length--; - return id; - } - } else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - this.length--; - return itemId; + while (output.length < targetLength) { + output = '0' + output; + } + return (sign ? (forceSign ? '+' : '') : '-') + output; } - } - return null; - }; - /** - * Clear the data - * @param {String} [senderId] Optional sender id - * @return {Array} removedIds The ids of all removed items - */ - DataSet.prototype.clear = function (senderId) { - var ids = Object.keys(this._data); + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - this._data = {}; - this.length = 0; + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } - this._trigger("remove", { items: ids }, senderId); + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - return ids; - }; + return res; + } - /** - * 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; + 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; + } - 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 res; } - } - return max; - }; + // 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; + } - /** - * 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; + val = typeof val === 'string' ? +val : val; + dur = moment.duration(val, period); + addOrSubtractDurationFromMoment(this, dur, direction); + return this; + }; + } - 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 addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; + + 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); + } } - } - return min; - }; + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } - /** - * 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 isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } - 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; + // 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++; + } } - } - if (!exists && value !== undefined) { - values[count] = value; - count++; - } + return diffs + lengthDiff; } - } - if (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); + function normalizeUnits(units) { + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; + } + return units; } - } - return values; - }; + 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; + } + + function makeList(field) { + var count, setter; + + if (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; + } + else if (field.indexOf('month') === 0) { + count = 12; + setter = 'month'; + } + else { + return; + } + + moment[field] = function (format, index) { + var i, getter, + method = moment._locale[field], + results = []; - /** - * Add a single item. Will fail when an item with the same id already exists. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._addItem = function (item) { - var id = item[this._fieldId]; + if (typeof format === 'number') { + index = format; + format = 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"); - } - } else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; - } + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment._locale, m, format || ''); + }; - 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); + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; } - } - this._data[id] = d; - this.length++; - return id; - }; + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - /** - * 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; + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; - } + return value; + } - // 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]); - } + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); } - } else { - // no field types specified, no converting needed - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - converted[field] = value; - } + + function weeksInYear(year, dow, doy) { + return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; } - } - return converted; - }; - /** - * Update a single item: merge with existing item. - * Will fail when the item has no id, or when there does not exist an item - * with the same id. - * @param {Object} item - * @return {String} id - * @private - */ - DataSet.prototype._updateItem = function (item) { - var id = item[this._fieldId]; - if (id == undefined) { - throw new Error("Cannot update item: item has no id (item: " + JSON.stringify(item) + ")"); - } - var d = this._data[id]; - if (!d) { - // item doesn't exist - throw new Error("Cannot update item: no item with id " + id + " found"); - } + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } - // 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); + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } - } - return id; - }; + 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; - /** - * 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; - }; + if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } - /** - * 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(); + m._pf.overflow = overflow; + } + } - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - dataTable.setValue(row, col, item[field]); - } - }; + 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; - module.exports = DataSet; + if (m._strict) { + m._isValid = m._isValid && + m._pf.charsLeftOver === 0 && + m._pf.unusedTokens.length === 0 && + m._pf.bigHour === undefined; + } + } + return m._isValid; + } -/***/ }, -/* 4 */ -/***/ function(module, exports, __webpack_require__) { + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; + } - "use strict"; + // pick the locale from the array + // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each + // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root + function chooseLocale(names) { + var i = 0, j, next, locale, split; - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); + 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; + } - /** - * 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 + 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]; + } - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + // 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(); + } + } - this.setData(data); - } + /************************************ + Locale + ************************************/ - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly - /** - * Set a data source for the view - * @param {DataSet | DataView} data - */ - DataView.prototype.setData = function (data) { - var ids, i, len; + extend(Locale.prototype, { - if (this._data) { - // unsubscribe from current dataset - if (this._data.unsubscribe) { - this._data.unsubscribe("*", this.listener); - } + 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); + }, - // 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 }); - } + _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + months : function (m) { + return this._months[m.month()]; + }, - this._data = data; + _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, - if (this._data) { - // update fieldId - this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || "id"; + monthsParse : function (monthName, format, strict) { + var i, mom, regex; - // 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 }); + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } - // subscribe to new dataset - if (this._data.on) { - this._data.on("*", this.listener); - } - } - }; + 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; + } + } + }, - /** - * 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 = []; + _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, - // 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++; - } - } + _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, - // 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--; - } - } - } + _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, - // trigger events - if (added.length) { - this._trigger("add", { items: added }); - } - if (removed.length) { - this._trigger("remove", { items: removed }); - } - }; + weekdaysParse : function (weekdayName) { + var i, mom, regex; - /** - * 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; + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } - // 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]; - } + 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; + } + } + }, + + _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; + }, - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); + 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'); + }, - // 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); - }; - } + _meridiemParse : /[ap]\.?m?\.?/i, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); - return this._data && this._data.get.apply(this._data, getArguments); - }; + _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; + }, - /** - * 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; + _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' + }, - if (this._data) { - var defaultFilter = this._options.filter; - var filter; + 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); + }, - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); - }; - } else { - filter = options.filter; - } - } else { - filter = defaultFilter; - } + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); - } else { - ids = []; - } + ordinal : function (number) { + return this._ordinal.replace('%d', number); + }, + _ordinal : '%d', + _ordinalParse : /\d{1,2}/, - return ids; - }; + preparse : function (string) { + return string; + }, - /** - * 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; - }; + postformat : function (string) { + return string; + }, - /** - * 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 = []; + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + }, - 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); - } - } + _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. + }, - break; + firstDayOfWeek : function () { + return this._week.dow; + }, - 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); + firstDayOfYear : function () { + return this._week.doy; + }, - 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 {} - } + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; } + }); - break; + /************************************ + Formatting + ************************************/ - 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; + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); } - this.length += added.length - removed.length; + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; - 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); + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + 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; + }; } - } - }; - // copy subscription functionality from DataSet - DataView.prototype.on = DataSet.prototype.on; - DataView.prototype.off = DataSet.prototype.off; - DataView.prototype._trigger = DataSet.prototype._trigger; + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } - // 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; + format = expandFormat(format, m.localeData()); - module.exports = DataView; - // nothing interesting for me :-( + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } -/***/ }, -/* 5 */ -/***/ function(module, exports, __webpack_require__) { + return formatFunctions[format](m); + } - "use strict"; + function expandFormat(format, locale) { + var i = 5; - /** - * 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; + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } - // properties - this._queue = []; - this._timeout = null; - this._extended = null; + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } - this.setOptions(options); - } + return format; + } - /** - * 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(); - }; + /************************************ + Parsing + ************************************/ - /** - * 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(); - }; + // 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 methods = [{ - name: "flush", - original: undefined - }]; + 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]); - if (options && options.replace) { - for (var i = 0; i < options.replace.length; i++) { - var name = options.replace[i]; - methods.push({ - name: name, - original: object[name] - }); - queue.replace(object, name); + return parts[0] === '+' ? minutes : -minutes; } - } - - queue._extended = { - object: object, - methods: methods - }; - return queue; - }; + // function to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, datePartArray = config._a; - /** - * 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(); + 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); + } - 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]; - } + 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); + } } - this._extended = null; - } - }; - /** - * Replace a method on an object with a queued version - * @param {Object} object Object having the method - * @param {string} method The method name - */ - Queue.prototype.replace = function (object, method) { - var me = this; - var original = object[method]; - if (!original) { - throw new Error("Method " + method + " undefined"); - } + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; - object[method] = function () { - // create an Array with the arguments - var args = []; - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; - } + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - // add this call to the queue - me.queue({ - args: args, - fn: original, - context: this - }); - }; - }; + // 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; - /** - * 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); - } + weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); + week = dfl(w.w, 1); - this._flushIfNeeded(); - }; + 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); - /** - * 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(); - } + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } - // 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); - } - }; + // 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; - /** - * 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 || []); - } - }; + if (config._d) { + return; + } - module.exports = Queue; + currentDate = currentDateArray(config); -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } - "use strict"; + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); - 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); + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } - /** - * @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"); - } + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } - // 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%"; + // 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]; + } - this.xLabel = "x"; - this.yLabel = "y"; - this.zLabel = "z"; + // 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]; + } - var passValueFn = function (v) { - return v; - }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; + // 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.filterLabel = "time"; - this.legendLabel = "value"; + 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); + } - 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._nextDay) { + config._a[HOUR] = 24; + } + } - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + function dateFromObject(config) { + var normalizedInput; - this.camera = new Camera(); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + if (config._d) { + return; + } - this.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day || normalizedInput.date, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; + dateFromConfig(config); + } - this.xMin = 0; - this.xStep = undefined; // auto by default - this.xMax = 1; - this.yMin = 0; - this.yStep = undefined; // auto by default - this.yMax = 1; - this.zMin = 0; - this.zStep = undefined; // auto by default - this.zMax = 1; - this.valueMin = 0; - this.valueMax = 1; - this.xBarWidth = 1; - this.yBarWidth = 1; - // TODO: customize axis range + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [ + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + ]; + } else { + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } + } - // constants - this.colorAxis = "#4D4D4D"; - this.colorGrid = "#D3D3D3"; - this.colorDot = "#7DC1FF"; - this.colorDotBorder = "#3267D2"; + // date from string and format string + function makeDateFromStringAndFormat(config) { + if (config._f === moment.ISO_8601) { + parseISO(config); + return; + } - // create a frame and canvas - this.create(); + config._a = []; + config._pf.empty = true; - // apply options (also when undefined) - this.setOptions(options); + // 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; - // apply data - if (data) { - this.setData(data); - } - } + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); + 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); + } + } - /** - * 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)); + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); + } - // 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; + // 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); } - } - - // scale the vertical axis - this.scale.z *= this.verticalRatio; - // TODO: can this be automated? verticalRatio? - - // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); - - // position the camera arm - var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; - var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; - var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; - this.camera.setArmLocation(xCenter, yCenter, zCenter); - }; + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } - /** - * 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); - }; + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } - /** - * 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, + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + bestMoment, + scoreToBeat, + i, + currentScore; - // 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), + 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); - // calculate translation - dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), - dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), - dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); + if (!isValid(tempConfig)) { + continue; + } - return new Point3d(dx, dy, dz); - }; + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; - /** - * 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; + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; - // 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()); - } + tempConfig._pf.score = currentScore; - // 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); - }; + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } - /** - * 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; + extend(config, bestMoment || tempConfig); + } - 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"; - } + // date from iso format + function parseISO(config) { + var i, l, + string = config._i, + match = isoRegex.exec(string); - this.frame.style.backgroundColor = fill; - this.frame.style.borderColor = stroke; - this.frame.style.borderWidth = strokeWidth + "px"; - this.frame.style.borderStyle = "solid"; - }; + 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; + } + } + // date from iso format or fallback + function makeDateFromString(config) { + parseISO(config); + if (config._isValid === false) { + delete config._isValid; + moment.createFromInputFallback(config); + } + } - /// 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 - }; + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); + } + return res; + } - /** - * 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; - } + 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); + } + } - return -1; - }; + 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); - /** - * 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; + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } - if (data.getNumberOfColumns() > 3) { - this.colFilter = 3; + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); + } + return date; } - } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { - // 4 columns expected, and optionally a 5th with filter values - this.colX = 0; - this.colY = 1; - this.colZ = 2; - this.colValue = 3; - if (data.getNumberOfColumns() > 4) { - this.colFilter = 4; + 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; } - } else { - throw "Unknown style \"" + this.style + "\""; - } - }; - Graph3d.prototype.getNumberOfRows = function (data) { - return data.length; - }; + /************************************ + Relative Time + ************************************/ - Graph3d.prototype.getNumberOfColumns = function (data) { - var counter = 0; - for (var column in data[0]) { - if (data[0].hasOwnProperty(column)) { - counter++; + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); } - } - return 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]; - 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]); + args[2] = withoutSuffix; + args[3] = +posNegDuration > 0; + args[4] = locale; + return substituteTimeAgo.apply({}, args); } - } - return distinctValues; - }; - Graph3d.prototype.getColumnRange = function (data, column) { - var minMax = { min: data[0][column], max: data[0][column] }; - for (var i = 0; i < data.length; i++) { - if (minMax.min > data[i][column]) { - minMax.min = data[i][column]; - } - if (minMax.max < data[i][column]) { - minMax.max = data[i][column]; - } - } - return minMax; - }; + /************************************ + Week of Year + ************************************/ - /** - * Initialize the data from the data table. Calculate minimum and maximum values - * and column index values - * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph. - * @param {Number} style Style Number - */ - Graph3d.prototype._dataInitialize = function (rawData, style) { - var me = this; - // unsubscribe from the dataTable - if (this.dataSet) { - this.dataSet.off("*", this._onChange); - } + // 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; - if (rawData === undefined) return; - if (Array.isArray(rawData)) { - rawData = new DataSet(rawData); - } + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } - var data; - if (rawData instanceof DataSet || rawData instanceof DataView) { - data = rawData.get(); - } else { - throw new Error("Array, DataSet, or DataView expected"); - } + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } - if (data.length == 0) return; + adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; + } - this.dataSet = rawData; - this.dataTable = data; + //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; - // subscribe to changes in the dataset - this._onChange = function () { - me.setData(me.dataSet); - }; - this.dataSet.on("*", this._onChange); + 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; - // _determineColumnIndexes - // getNumberOfRows (points) - // getNumberOfColumns (x,y,z,v,t,t1,t2...) - // getDistinctValues (unique values?) - // getColumnRange + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } - // 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"; + /************************************ + Top Level Functions + ************************************/ + function makeMoment(config) { + var input = config._i, + format = config._f, + res; + config._locale = config._locale || moment.localeData(config._l); - // 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 (input === null || (format === undefined && input === '')) { + return moment.invalid({nullInput: true}); + } + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } - var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; + if (moment.isMoment(input)) { + return new Moment(input, true); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } + } else { + makeDateFromInput(config); + } - // 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; + res = new Moment(config); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } + + 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; + + 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(); + + return makeMoment(c); + }; + + moment.suppressDeprecationWarnings = false; + + moment.createFromInputFallback = deprecate( + 'moment construction falls back to js Date. This is ' + + 'discouraged and will be removed in upcoming major ' + + 'release. Please refer to ' + + 'https://github.com/moment/moment/issues/1407 for more info.', + function (config) { + config._d = new Date(config._i + (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; } - } - // 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; + moment.min = function () { + var args = [].slice.call(arguments, 0); + + return pickBy('isBefore', args); + }; - 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; + moment.max = function () { + var args = [].slice.call(arguments, 0); - 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; + return pickBy('isAfter', args); + }; - 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; - } + // creating with utc + moment.utc = function (input, format, locale, strict) { + var c; - // set the scale dependent on the ranges. - this._setScale(); - }; + 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 makeMoment(c).utc(); + }; + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; - /** - * 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; + // duration + moment.duration = function (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso, + diffRes; - var dataPoints = []; + 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)); - 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 + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } - // 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; + ret = new Duration(duration); - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); - } - } + if (moment.isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } - var sortNumber = function (a, b) { - return a - b; + return ret; }; - 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; + // version number + moment.version = VERSION; - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); + // default format + moment.defaultFormat = isoFormat; - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } + // constant that refers to the ISO standard + moment.ISO_8601 = function () {}; - var point3d = new Point3d(); - point3d.x = x; - point3d.y = y; - point3d.z = z; + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + moment.momentProperties = momentProperties; - obj = {}; - obj.point = point3d; - obj.trans = undefined; - obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + // 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 () {}; - dataMatrix[xIndex][yIndex] = 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; + }; - dataPoints.push(obj); - } + moment.lang = deprecate( + 'moment.lang is deprecated. Use moment.locale instead.', + function (key, value) { + return moment.locale(key, value); + } + ); - // 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; + // 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); + } + + if (data) { + moment.duration._locale = moment._locale = data; + } } - } - } - } 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; - } + return moment._locale._abbr; + }; - obj = {}; - obj.point = point; - obj.bottom = new Point3d(point.x, point.y, this.zMin); - obj.trans = undefined; - obj.screen = undefined; + moment.defineLocale = function (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); + } + locales[name].set(values); - dataPoints.push(obj); - } - } + // backwards compat for now: also set the locale + moment.locale(name); - return dataPoints; - }; + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } + }; - /** - * 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); - } + moment.langData = deprecate( + 'moment.langData is deprecated. Use moment.localeData instead.', + function (key) { + return moment.localeData(key); + } + ); - this.frame = document.createElement("div"); - this.frame.style.position = "relative"; - this.frame.style.overflow = "hidden"; + // returns locale data + moment.localeData = function (key) { + var locale; - // 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); - } + if (key && key._locale && key._locale._abbr) { + key = key._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); + if (!key) { + return moment._locale; + } - // 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' + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } - 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 chooseLocale(key); + }; - // add the new graph to the container element - this.containerElement.appendChild(this.frame); - }; + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment || + (obj != null && hasOwnProp(obj, '_isAMomentObject')); + }; + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; - /** - * 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; + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); + } - this._resizeCanvas(); - }; + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; - /** - * 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%"; + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; + return m; + }; - // adjust with for margin - this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + "px"; - }; + moment.parseZone = function () { + return moment.apply(null, arguments).parseZone(); + }; - /** - * Start animation - */ - Graph3d.prototype.animationStart = function () { - if (!this.frame.filter || !this.frame.filter.slider) throw "No animation available"; + moment.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; - this.frame.filter.slider.play(); - }; + moment.isDate = isDate; + /************************************ + Moment Prototype + ************************************/ - /** - * Stop animation - */ - Graph3d.prototype.animationStop = function () { - if (!this.frame.filter || !this.frame.filter.slider) return; - this.frame.filter.slider.stop(); - }; + extend(moment.fn = Moment.prototype, { + clone : function () { + return moment(this); + }, - /** - * 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 - } + valueOf : function () { + return +this._d - ((this._offset || 0) * 60000); + }, - // 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 - } - }; + unix : function () { + return Math.floor(+this / 1000); + }, - /** - * 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; - } + toString : function () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + }, - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, + + 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]'); + } + }, + + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } + isValid : function () { + return isValid(this); + }, - this.redraw(); - }; + isDSTShifted : function () { + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } + return false; + }, - /** - * 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; - }; + parsingFlags : function () { + return extend({}, this._pf); + }, - /** - * Load data into the 3D Graph - */ - Graph3d.prototype._readData = function (data) { - // read the data - this._dataInitialize(data, this.style); + invalidAt: function () { + return this._pf.overflow; + }, + utc : function (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + }, - if (this.dataFilter) { - // apply filtering - this.dataPoints = this.dataFilter._getDataPoints(); - } else { - // no filtering. load all data - this.dataPoints = this._getDataPoints(this.dataTable); - } + local : function (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; - // draw the filter - this._redrawFilter(); - }; + if (keepLocalTime) { + this.subtract(this._dateUtcOffset(), 'm'); + } + } + return this; + }, - /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data - */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.localeData().postformat(output); + }, - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + add : createAdder(1, 'add'), - /** - * Update the options. Options will be merged with current options - * @param {Object} options - */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; + subtract : createAdder(-1, 'subtract'), - this.animationStop(); + diff : function (input, units, asFloat) { + var that = makeAs(input, this), + zoneDiff = (that.utcOffset() - this.utcOffset()) * 6e4, + anchor, diff, output, daysAdjust; - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + units = normalizeUnits(units); - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + 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); + }, - 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; + from : function (time, withoutSuffix) { + return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + }, - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, - 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; + 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))); + }, - if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; + isLeapYear : function () { + return isLeapYear(this.year()); + }, - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; + isDST : function () { + return (this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset()); + }, - 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; + 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 (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + month : makeAccessor('Month', true), - 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); - } - } + 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 */ + } - this._setBackgroundColor(options && options.backgroundColor); + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } - this.setSize(this.width, this.height); + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); - } + return this; + }, - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + 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'); + }, - /** - * Redraw the Graph. - */ - Graph3d.prototype.redraw = function () { - if (this.dataPoints === undefined) { - throw "Error: graph data not initialized"; - } + 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); + } + }, - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); + 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; + } + }, - 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(); - } + isBetween: function (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + }, - this._redrawInfo(); - this._redrawLegend(); - }; + 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)); + } + }, - /** - * Clear the canvas before redrawing - */ - Graph3d.prototype._redrawClear = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); + 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; + } + ), - ctx.clearRect(0, 0, canvas.width, canvas.height); - }; + 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; + } + ), + 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; + } - /** - * Redraw the legend showing the colors - */ - Graph3d.prototype._redrawLegend = function () { - var y; + this.utcOffset(input, keepLocalTime); - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { - var dotSize = this.frame.clientWidth * 0.02; + return this; + } else { + return -this.utcOffset(); + } + } + ), - 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 - } + // 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; + } + } - var height = Math.max(this.frame.clientHeight * 0.25, 100); - var top = this.margin; - var right = this.frame.clientWidth - this.margin; - var left = right - widthMax; - var bottom = top + height; - } + return this; + } else { + return this._isUTC ? offset : this._dateUtcOffset(); + } + }, - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); - ctx.lineWidth = 1; - ctx.font = "14px arial"; // TODO: put in options + isLocal : function () { + return !this._isUTC; + }, - 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); + isUtcOffset : function () { + return this._isUTC; + }, - //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); + isUtc : function () { + return this._isUTC && this._offset === 0; + }, - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); - } + zoneAbbr : function () { + return this._isUTC ? 'UTC' : ''; + }, - ctx.strokeStyle = this.colorAxis; - ctx.strokeRect(left, top, widthMax, height); - } + zoneName : function () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + }, - 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(); - } + parseZone : function () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(utcOffsetFromString(this._i)); + } + return this; + }, - 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; + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).utcOffset(); + } - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + return (this.utcOffset() - input) % 60 === 0; + }, - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.colorAxis; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + daysInMonth : function () { + return daysInMonth(this.year(), this.month()); + }, - step.next(); - } + 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'); + }, - ctx.textAlign = "right"; - ctx.textBaseline = "top"; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); - } - }; + quarter : function (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + }, - /** - * Redraw the filter - */ - Graph3d.prototype._redrawFilter = function () { - this.frame.filter.innerHTML = ""; + 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'); + }, - if (this.dataFilter) { - var options = { - visible: this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add((input - year), 'y'); + }, - // TODO: css here is not nice here... - this.frame.filter.style.padding = "10px"; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; + week : function (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + }, - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + }, - // create an event handler - var me = this; - var onchange = function () { - var index = slider.getIndex(); + weekday : function (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + }, - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + 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.redraw(); - }; - slider.setOnChangeCallback(onchange); - } else { - this.frame.filter.slider = undefined; - } - }; + isoWeeksInYear : function () { + return weeksInYear(this.year(), 1, 4); + }, - /** - * Redraw the slider - */ - Graph3d.prototype._redrawSlider = function () { - if (this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); - } - }; + weeksInYear : function () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + }, + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, - /** - * Redraw common information - */ - Graph3d.prototype._redrawInfo = function () { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); + 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; + }, - ctx.font = "14px arial"; // TODO: put in options - ctx.lineStyle = "gray"; - ctx.fillStyle = "gray"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; + // 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 x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); - } - }; + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = moment.localeData(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + }, + 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); + } + } + ), - /** - * 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; + localeData : function () { + return this._locale; + }, - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + "px arial"; + _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; + } - // calculate the length for the short grid lines - var gridLenX = 0.025 / this.scale.x; - var gridLenY = 0.025 / this.scale.y; - var textMargin = 5 / this.camera.getArmLength(); // px - var armAngle = this.camera.getArmRotation().horizontal; + }); - // draw x-grid lines - ctx.lineWidth = 1; - prettyStep = this.defaultXStep === undefined; - step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); - step.start(); - if (step.getCurrent() < this.xMin) { - step.next(); - } - while (!step.end()) { - var x = step.getCurrent(); + function rawMonthSetter(mom, value) { + var dayOfMonth; - 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(); + // 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; + } + } - 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(); + dayOfMonth = Math.min(mom.date(), + daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; } - 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"; + function rawGetter(mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); } - ctx.fillStyle = this.colorAxis; - ctx.fillText(" " + this.xValueLabel(step.getCurrent()) + " ", text.x, text.y); - - step.next(); - } - - // draw y-grid lines - ctx.lineWidth = 1; - prettyStep = this.defaultYStep === undefined; - step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep); - step.start(); - if (step.getCurrent() < this.yMin) { - step.next(); - } - while (!step.end()) { - if (this.showGrid) { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorGrid; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - } else { - from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + function rawSetter(mom, unit, value) { + if (unit === 'Month') { + return rawMonthSetter(mom, value); + } else { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } } - xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax; - text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin)); - if (Math.cos(armAngle * 2) < 0) { - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - text.y += textMargin; - } else if (Math.sin(armAngle * 2) > 0) { - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - } else { - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; + function makeAccessor(unit, keepTime) { + return function (value) { + if (value != null) { + rawSetter(this, unit, value); + moment.updateOffset(this, keepTime); + return this; + } else { + return rawGetter(this, unit); + } + }; } - ctx.fillStyle = this.colorAxis; - ctx.fillText(" " + this.yValueLabel(step.getCurrent()) + " ", text.x, text.y); - step.next(); - } + 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)); - // 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 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; - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.colorAxis; - ctx.fillText(this.zValueLabel(step.getCurrent()) + " ", from.x - 5, from.y); + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; - 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(); + // alias isUtc for dev-friendliness + moment.fn.isUTC = moment.fn.isUtc; - // 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(); + /************************************ + Duration Prototype + ************************************/ - // draw y-axis - ctx.lineWidth = 1; - // line at xMin - from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // line at xMax - from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // draw x-label - var xLabel = this.xLabel; - if (xLabel.length > 0) { - yOffset = 0.1 / this.scale.y; - xText = (this.xMin + this.xMax) / 2; - yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset; - text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); - if (Math.cos(armAngle * 2) > 0) { - ctx.textAlign = "center"; - ctx.textBaseline = "top"; - } else if (Math.sin(armAngle * 2) < 0) { - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - } else { - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; } - 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"; - } else { - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; + function yearsToDays (years) { + // years * 365 + absRound(years / 4) - + // absRound(years / 100) + absRound(years / 400); + return years * 146097 / 400; } - ctx.fillStyle = this.colorAxis; - ctx.fillText(yLabel, text.x, text.y); - } - // draw z-label - var zLabel = this.zLabel; - if (zLabel.length > 0) { - offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? - xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; - yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; - zText = (this.zMin + this.zMax) / 2; - text = this._convert3Dto2D(new Point3d(xText, yText, zText)); - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.colorAxis; - ctx.fillText(zLabel, text.x - offset, text.y); - } - }; + extend(moment.duration.fn = Duration.prototype, { - /** - * 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; + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years = 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)); + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - 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; + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; + + hours = absRound(minutes / 60); + data.hours = hours % 24; + + 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; - default: - R = 0;G = 0;B = 0;break; - } + // 12 months -> 1 year + years += absRound(months / 12); + months %= 12; - return "RGB(" + parseInt(R * 255) + "," + parseInt(G * 255) + "," + parseInt(B * 255) + ")"; - }; + 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); - /** - * Draw all datapoints as a grid - * This function can be used when the style is 'grid' - */ - Graph3d.prototype._redrawDataGrid = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext("2d"), - point, - right, - top, - cross, - i, - topSideVisible, - fillStyle, - strokeStyle, - lineWidth, - h, - s, - v, - zAvg; + this._data.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); + return this; + }, - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + weeks : function () { + return absRound(this.days() / 7); + }, - // 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); + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; + }, - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + humanize : function (withSuffix) { + var output = relativeTime(this, !withSuffix, this.localeData()); - // calculate the translation of the point at the bottom (needed for sorting) - var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); - this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; - } + if (withSuffix) { + output = this.localeData().pastFuture(+this, 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); + return this.localeData().postformat(output); + }, - 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; + add : function (input, val) { + // supports only 2.0-style add(1, 's') or add(moment) + var dur = moment.duration(input, val); - 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._milliseconds += dur._milliseconds; + this._days += dur._days; + this._months += dur._months; - topSideVisible = crossproduct.z > 0; - } else { - topSideVisible = true; - } + this._bubble(); - 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 + return this; + }, - 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; + subtract : function (input, val) { + var dur = moment.duration(input, val); - 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._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; - if (point !== undefined) { - if (this.showPerspective) { - lineWidth = 2 / -point.trans.z; - } else { - lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); - } - } + this._bubble(); - 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; + return this; + }, - 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(); - } + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); + }, - 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; + as : function (units) { + var days, months; + units = normalizeUnits(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(); - } - } - } - }; + 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); + } + } + }, + lang : moment.fn.lang, + locale : moment.fn.locale, - /** - * 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 : deprecate( + 'toIsoString() is deprecated. Please use toISOString() instead ' + + '(notice the capitals)', + function () { + return this.toISOString(); + } + ), - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + 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); - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } - // 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; - } + 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' : ''); + }, - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + localeData : function () { + return this._locale; + }, - // 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]; + toJSON : function () { + return this.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(); - } + moment.duration.fn.toString = moment.duration.fn.toISOString; - // 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; + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; } - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } else { - radius = size * -(this.eye.z / this.camera.getArmLength()); - } - if (radius < 0) { - radius = 0; + for (i in unitMillisecondFactors) { + if (hasOwnProp(unitMillisecondFactors, i)) { + makeDurationGetter(i.toLowerCase()); + } } - 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); - } + 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'); + }; - // 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(); - } - }; + /************************************ + Default Locale + ************************************/ - /** - * Draw all datapoints as bars. - * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' - */ - Graph3d.prototype._redrawDataBar = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); - var i, j, surface, corners; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + // 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; + } + }); - // 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; + /* EMBED_LOCALES */ - // 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; - } + /************************************ + Exposing Moment + ************************************/ - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + 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; + } + } - // 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]; + // 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; + } - // 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; + return moment; + }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + makeGlobal(true); } 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); + makeGlobal(); } + }).call(this); + + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(5)(module))) - // 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); - } +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { - // 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) }]; + function webpackContext(req) { + throw new Error("Cannot find module '" + req + "'."); + } + webpackContext.keys = function() { return []; }; + webpackContext.resolve = webpackContext; + module.exports = webpackContext; + webpackContext.id = 4; - // 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); - }); - // 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; +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { - // 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}) - } + module.exports = function(module) { + if(!module.webpackPolyfill) { + module.deprecate = function() {}; + module.paths = []; + // module.parent = undefined by default + module.children = []; + module.webpackPolyfill = 1; + } + return module; + } - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; - // both are equal - return 0; - }); + // DOM utility methods - // 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 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 a line through all datapoints. - * This function can be used when the style is 'line' + * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from + * which to remove the redundant elements. + * + * @param JSONcontainer + * @private */ - Graph3d.prototype._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; + 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 = []; + } + } } + }; - // 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); + /** + * 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; + }; - // 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 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; }; + + + /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) + * 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 {*} */ - 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); + 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); } - // 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); + 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; + } - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; + } - this.frame.style.cursor = "move"; + if (labelObj.className) { + label.setAttributeNS(null, "class", labelObj.className + " label"); + } - // 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); + } + label.setAttributeNS(null, "x", x); + label.setAttributeNS(null, "y", y); + return point; }; - /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event + * draw a bar SVG element centered on the X coordinate + * + * @param x + * @param y + * @param className */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.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); + } + }; - // calculate change in mouse position - var diffX = parseFloat(getMouseX(event)) - this.startMouseX; - var diffY = parseFloat(getMouseY(event)) - this.startMouseY; +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { - var horizontalNew = this.startArmRotation.horizontal + diffX / 200; - var verticalNew = this.startArmRotation.vertical + diffY / 200; + "use strict"; - var snapAngle = 4; // degrees - var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); + var util = __webpack_require__(1); + var Queue = __webpack_require__(8); - // 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; + /** + * 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.} [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 */ - Graph3d.prototype._insideTriangle = function (point, triangle) { - var a = triangle[0], - b = triangle[1], - c = triangle[2]; + DataSet.prototype.get = function (args) { + var me = this; - function sign(x) { - return x > 0 ? 1 : x < 0 ? -1 : 0; + // 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]; } - 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)); + // determine the return type + var returnType; + if (options && options.returnType) { + var allowedValues = ["DataTable", "Array", "Object"]; + returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; - // each of the three signs must be either equal to each other or zero - return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs); - }; + if (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"; + } - /** - * Find a data point close to given screen position (x, y) - * @param {Number} x - * @param {Number} y - * @return {Object | null} The closest data point or null if not close to any data point - * @private - */ - Graph3d.prototype._dataPointFromXY = function (x, y) { - var i, - distMax = 100, - // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], + item, + itemId, + i, + len; - 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; - } - } + // 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 { - // 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 all items + for (itemId in this._data) { + if (this._data.hasOwnProperty(itemId)) { + item = me._getItem(itemId, type); + if (!filter || filter(item)) { + items.push(item); } } } } + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } + + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; + if (id != undefined) { + item = this._filterFields(item, fields); + } else { + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } + } + } - return closestDataPoint; + // 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; + } + } + } }; /** - * Display a tooltip for given data point - * @param {Object} dataPoint - * @private + * 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 */ - Graph3d.prototype._showTooltip = function (dataPoint) { - var content, line, dot; - - if (!this.tooltip) { - content = document.createElement("div"); - content.style.position = "absolute"; - content.style.padding = "10px"; - content.style.border = "1px solid #4d4d4d"; - content.style.color = "#1a1a1a"; - content.style.background = "rgba(255,255,255,0.7)"; - content.style.borderRadius = "2px"; - content.style.boxShadow = "5px 5px 10px rgba(128,128,128,0.5)"; + 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 = []; - line = document.createElement("div"); - line.style.position = "absolute"; - line.style.height = "40px"; - line.style.width = "0"; - line.style.borderLeft = "1px solid #4d4d4d"; + 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); + } + } + } - dot = document.createElement("div"); - dot.style.position = "absolute"; - dot.style.height = "0"; - dot.style.width = "0"; - dot.style.border = "5px solid #4d4d4d"; - dot.style.borderRadius = "5px"; + this._sort(items, order); - this.tooltip = { - dataPoint: null, - dom: { - content: content, - line: line, - dot: dot + 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 { - content = this.tooltip.dom.content; - line = this.tooltip.dom.line; - dot = this.tooltip.dom.dot; - } + // get all items + if (order) { + // create an ordered list + items = []; + for (id in data) { + if (data.hasOwnProperty(id)) { + items.push(data[id]); + } + } - this._hideTooltip(); + this._sort(items, order); - 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 + "
"; + 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]); + } + } + } } - content.style.left = "0"; - content.style.top = "0"; - this.frame.appendChild(content); - this.frame.appendChild(line); - this.frame.appendChild(dot); - - // calculate sizes - var contentWidth = content.offsetWidth; - var contentHeight = content.offsetHeight; - var lineHeight = line.offsetHeight; - var dotWidth = dot.offsetWidth; - var dotHeight = dot.offsetHeight; - - var left = dataPoint.screen.x - contentWidth / 2; - left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + return ids; + }; - 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"; + /** + * 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; }; /** - * Hide the tooltip when displayed - * @private + * 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. */ - Graph3d.prototype._hideTooltip = function () { - if (this.tooltip) { - this.tooltip.dataPoint = null; + DataSet.prototype.forEach = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + data = this._data, + item, + id; - 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); + if (options && options.order) { + // execute forEach on ordered list + var items = this.get(options); + + for (var i = 0, len = items.length; i < len; i++) { + item = items[i]; + id = item[this._fieldId]; + callback(item, id); + } + } else { + // unordered + for (id in data) { + if (data.hasOwnProperty(id)) { + item = this._getItem(id, type); + if (!filter || filter(item)) { + callback(item, id); } } } } }; - /**--------------------------------------------------------------------------**/ - - - /** - * Get the horizontal mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse x - */ - function getMouseX(event) { - if ("clientX" in event) { - return event.clientX; - }return event.targetTouches[0] && event.targetTouches[0].clientX || 0; - } - /** - * Get the vertical mouse position from a mouse event - * @param {Event} event - * @return {Number} mouse y + * Map every item in the dataset. + * @param {function} callback + * @param {Object} [options] Available options: + * {Object.} [type] + * {String[]} [fields] filter fields + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Object[]} mappedItems */ - function getMouseY(event) { - if ("clientY" in event) { - return event.clientY; - }return event.targetTouches[0] && event.targetTouches[0].clientY || 0; - } - - module.exports = Graph3d; - // use use defaults + DataSet.prototype.map = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + mappedItems = [], + data = this._data, + item; -/***/ }, -/* 7 */ -/***/ function(module, exports, __webpack_require__) { + // 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)); + } + } + } - "use strict"; + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } - var Point3d = __webpack_require__(10); + return 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 + * 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 */ - function Camera() { - this.armLocation = new Point3d(); - this.armRotation = {}; - this.armRotation.horizontal = 0; - this.armRotation.vertical = 0; - this.armLength = 1.7; + DataSet.prototype._filterFields = function (item, fields) { + if (!item) { + // item is null + return item; + } - this.cameraLocation = new Point3d(); - this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); + var filteredItem = {}; - this.calculateCameraOrientation(); - } + for (var field in item) { + if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { + filteredItem[field] = item[field]; + } + } + + return filteredItem; + }; /** - * 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 + * Sort the provided array with items + * @param {Object[]} items + * @param {String | function} order A field name or custom sort function. + * @private */ - Camera.prototype.setArmLocation = function (x, y, z) { - this.armLocation.x = x; - this.armLocation.y = y; - this.armLocation.z = z; - - this.calculateCameraOrientation(); + 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"); + } }; /** - * 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. + * 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 */ - Camera.prototype.setArmRotation = function (horizontal, vertical) { - if (horizontal !== undefined) { - this.armRotation.horizontal = horizontal; - } + DataSet.prototype.remove = function (id, senderId) { + var removedIds = [], + i, + len, + removedId; - 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; + if (Array.isArray(id)) { + for (i = 0, len = id.length; i < len; i++) { + removedId = this._remove(id[i]); + if (removedId != null) { + removedIds.push(removedId); + } + } + } else { + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); + } } - if (horizontal !== undefined || vertical !== undefined) { - this.calculateCameraOrientation(); + if (removedIds.length) { + this._trigger("remove", { items: removedIds }, senderId); } + + return removedIds; }; /** - * Retrieve the current arm rotation - * @return {object} An object with parameters horizontal and vertical + * Remove an item by its id + * @param {Number | String | Object} id id or item + * @returns {Number | String | null} id + * @private */ - Camera.prototype.getArmRotation = function () { - var rot = {}; - rot.horizontal = this.armRotation.horizontal; - rot.vertical = this.armRotation.vertical; - - return rot; + 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; }; /** - * Set the (normalized) length of the camera arm. - * @param {Number} length A length between 0.71 and 5.0 + * Clear the data + * @param {String} [senderId] Optional sender id + * @return {Array} removedIds The ids of all removed items */ - Camera.prototype.setArmLength = function (length) { - if (length === undefined) return; + DataSet.prototype.clear = function (senderId) { + var ids = Object.keys(this._data); - this.armLength = length; + this._data = {}; + this.length = 0; - // 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._trigger("remove", { items: ids }, senderId); - this.calculateCameraOrientation(); + return ids; }; /** - * Retrieve the arm length - * @return {Number} length + * Find the item with maximum value of a specified field + * @param {String} field + * @return {Object | null} item Item containing max value, or null if no items */ - Camera.prototype.getArmLength = function () { - return this.armLength; - }; + DataSet.prototype.max = function (field) { + var data = this._data, + max = null, + maxField = null; - /** - * Retrieve the camera location - * @return {Point3d} cameraLocation - */ - Camera.prototype.getCameraLocation = function () { - return this.cameraLocation; - }; + 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; + } + } + } - /** - * Retrieve the camera rotation - * @return {Point3d} cameraRotation - */ - Camera.prototype.getCameraRotation = function () { - return this.cameraRotation; + return max; }; /** - * Calculate the location and rotation of the camera based on the - * position and orientation of the camera arm + * 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 */ - 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; - }; - - module.exports = Camera; - -/***/ }, -/* 8 */ -/***/ function(module, exports, __webpack_require__) { + DataSet.prototype.min = function (field) { + var data = this._data, + min = null, + minField = null; - "use strict"; + 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; + } + } + } - var DataView = __webpack_require__(4); + return min; + }; /** - * @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 + * 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. */ - 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; - }); + DataSet.prototype.distinct = function (field) { + var data = this._data; + var values = []; + var fieldType = this._options.type && this._options.type[field] || null; + var count = 0; + var i; - if (this.values.length > 0) { - this.selectValue(0); + 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++; + } + } } - // create an array with the filtered datapoints. this will be loaded afterwards - this.dataPoints = []; - - this.loaded = false; - this.onLoadCallback = undefined; - - if (graph.animationPreload) { - this.loaded = false; - this.loadInBackground(); - } else { - this.loaded = true; + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); + } } - }; - - /** - * Return the label - * @return {string} label - */ - Filter.prototype.isLoaded = function () { - return this.loaded; + return values; }; - /** - * Return the loaded progress - * @return {Number} percentage between 0 and 100 + * Add a single item. Will fail when an item with the same id already exists. + * @param {Object} item + * @return {String} id + * @private */ - Filter.prototype.getLoadedProgress = function () { - var len = this.values.length; + DataSet.prototype._addItem = function (item) { + var id = item[this._fieldId]; - var i = 0; - while (this.dataPoints[i]) { - i++; + 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; } - return Math.round(i / len * 100); - }; + 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 + * 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 */ - Filter.prototype.getLabel = function () { - return this.graph.filterLabel; - }; + DataSet.prototype._getItem = function (id, types) { + var field, value; + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; + } - /** - * Return the columnIndex of the filter - * @return {Number} columnIndex - */ - Filter.prototype.getColumn = function () { - return this.column; + // 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 the currently selected value. Returns undefined if there is no selection - * @return {*} value + * 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 */ - Filter.prototype.getSelectedValue = function () { - if (this.index === undefined) return undefined; + 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"); + } - return this.values[this.index]; - }; + // 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); + } + } - /** - * Retrieve all values of the filter - * @return {Array} values - */ - Filter.prototype.getValues = function () { - return this.values; + return id; }; /** - * Retrieve one value of the filter - * @param {Number} index - * @return {*} value + * Get an array with the column names of a Google DataTable + * @param {DataTable} dataTable + * @return {String[]} columnNames + * @private */ - Filter.prototype.getValue = function (index) { - if (index >= this.values.length) throw "Error: index out of range"; - - return this.values[index]; + 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; }; - /** - * Retrieve the (filtered) dataPoints for the currently selected filter index - * @param {Number} [index] (optional) - * @return {Array} dataPoints + * Append an item as a row to the dataTable + * @param dataTable + * @param columns + * @param item + * @private */ - 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); + DataSet.prototype._appendRow = function (dataTable, columns, item) { + var row = dataTable.addRow(); - this.dataPoints[index] = dataPoints; + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + dataTable.setValue(row, col, item[field]); } - - return dataPoints; }; + module.exports = DataSet; + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; /** - * Set a callback function when the filter is fully loaded. + * 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 */ - Filter.prototype.setOnLoadCallback = function (callback) { - this.onLoadCallback = callback; - }; + function Queue(options) { + // options + this.delay = null; + this.max = Infinity; + + // properties + this._queue = []; + this._timeout = null; + this._extended = null; + this.setOptions(options); + } /** - * Add a value to the list with available values for this filter - * No double entries will be created. - * @param {Number} 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 */ - Filter.prototype.selectValue = function (index) { - if (index >= this.values.length) throw "Error: index out of range"; + 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.index = index; - this.value = this.values[index]; + this._flushIfNeeded(); }; /** - * Load all filtered rows in the background one by one - * Start this method without providing an index! + * Extend an object with queuing functionality. + * The object will be extended with a function flush, and the methods provided + * in options.replace will be replaced with queued ones. + * @param {Object} object + * @param {Object} options + * Available options: + * - replace: Array. + * A list with method names of the methods + * on the object to be replaced with queued ones. + * - delay: number When provided, the queue will be flushed + * automatically after an inactivity of this delay + * in milliseconds. + * Default value is null. + * - max: number When the queue exceeds the given maximum number + * of entries, the queue is flushed automatically. + * Default value of max is Infinity. + * @return {Queue} Returns the created queue */ - Filter.prototype.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 + Queue.extend = function (object, options) { + var queue = new Queue(options); - // 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"; + if (object.flush !== undefined) { + throw new Error("Target object already has a property flush"); + } + object.flush = function () { + queue.flush(); + }; - var me = this; - setTimeout(function () { - me.loadInBackground(index + 1); - }, 10); - this.loaded = false; - } else { - this.loaded = true; + var methods = [{ + name: "flush", + original: undefined + }]; - // remove the progress box - if (frame.progress !== undefined) { - frame.removeChild(frame.progress); - frame.progress = 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); } - - if (this.onLoadCallback) this.onLoadCallback(); } - }; - - module.exports = Filter; -/***/ }, -/* 9 */ -/***/ function(module, exports, __webpack_require__) { + queue._extended = { + object: object, + methods: methods + }; - "use strict"; + return queue; + }; /** - * @prototype Point2d - * @param {Number} [x] - * @param {Number} [y] + * Destroy the queue. The queue will first flush all queued actions, and in + * case it has extended an object, will restore the original object. */ - function Point2d(x, y) { - this.x = x !== undefined ? x : 0; - this.y = y !== undefined ? y : 0; - } - - module.exports = Point2d; - -/***/ }, -/* 10 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; + Queue.prototype.destroy = function () { + this.flush(); - /** - * @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; + 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; + } }; /** - * Subtract the two provided points, returns a-b - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} a-b + * Replace a method on an object with a queued version + * @param {Object} object Object having the method + * @param {string} method The method name */ - 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; - }; + Queue.prototype.replace = function (object, method) { + var me = this; + var original = object[method]; + if (!original) { + throw new Error("Method " + method + " undefined"); + } - /** - * 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; + object[method] = function () { + // create an Array with the arguments + var args = []; + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i]; + } + + // add this call to the queue + me.queue({ + args: args, + fn: original, + context: this + }); + }; }; /** - * Calculate the average of two 3d points - * @param {Point3d} a - * @param {Point3d} b - * @return {Point3d} The average, (a+b)/2 + * Queue a call + * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry */ - Point3d.avg = function (a, b) { - return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2); + Queue.prototype.queue = function (entry) { + if (typeof entry === "function") { + this._queue.push({ fn: entry }); + } else { + this._queue.push(entry); + } + + this._flushIfNeeded(); }; /** - * 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 + * Check whether the queue needs to be flushed + * @private */ - 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; + Queue.prototype._flushIfNeeded = function () { + // flush when the maximum is exceeded. + if (this._queue.length > this.max) { + this.flush(); + } - return crossproduct; + // 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); + } }; - /** - * Rtrieve the length of the vector (or the distance from this point to the origin - * @return {Number} length + * Flush all queued calls */ - Point3d.prototype.length = function () { - return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + Queue.prototype.flush = function () { + while (this._queue.length > 0) { + var entry = this._queue.shift(); + entry.fn.apply(entry.context || entry.fn, entry.args || []); + } }; - module.exports = Point3d; + module.exports = Queue; /***/ }, -/* 11 */ +/* 9 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); /** - * @constructor Slider + * DataView * - * An html slider control with start/stop/prev/next buttons - * @param {Element} container The element where the slider will be created - * @param {Object} options Available options: - * {boolean} visible If true (default) the - * slider is visible. + * a dataview offers a filtered view on a dataset or an other dataview. + * + * @param {DataSet | DataView} data + * @param {Object} [options] Available options: see method get + * + * @constructor DataView */ - function Slider(container, options) { - if (container === undefined) { - throw "Error: No container element defined"; - } - this.container = container; - this.visible = options && options.visible != undefined ? options.visible : true; - - if (this.visible) { - this.frame = document.createElement("DIV"); - //this.frame.style.backgroundColor = '#E5E5E5'; - this.frame.style.width = "100%"; - this.frame.style.position = "relative"; - this.container.appendChild(this.frame); - - 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); - }; - } - - this.onChangeCallback = undefined; + function DataView(data, options) { + this._data = null; + this._ids = {}; // ids of the items currently in memory (just contains a boolean true) + this.length = 0; // number of items in the DataView + this._options = options || {}; + this._fieldId = "id"; // name of the field containing id + this._subscribers = {}; // event subscribers - this.values = []; - this.index = undefined; + var me = this; + this.listener = function () { + me._onEvent.apply(me, arguments); + }; - this.playTimeout = undefined; - this.playInterval = 1000; // milliseconds - this.playLoop = true; + this.setData(data); } - /** - * Select the previous index - */ - Slider.prototype.prev = function () { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); - } - }; - - /** - * Select the next index - */ - Slider.prototype.next = function () { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); - } - }; + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly /** - * Select the next index + * Set a data source for the view + * @param {DataSet | DataView} data */ - 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); - } - - 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 + DataView.prototype.setData = function (data) { + var ids, i, len; - var me = this; - this.playTimeout = setTimeout(function () { - me.playNext(); - }, interval); - }; + if (this._data) { + // unsubscribe from current dataset + if (this._data.unsubscribe) { + this._data.unsubscribe("*", this.listener); + } - /** - * Toggle start or stop playing - */ - Slider.prototype.togglePlay = function () { - if (this.playTimeout === undefined) { - this.play(); - } else { - this.stop(); + // 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 }); } - }; - /** - * Start playing - */ - Slider.prototype.play = function () { - // Test whether already playing - if (this.playTimeout) return; + this._data = data; - this.playNext(); + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || "id"; - if (this.frame) { - this.frame.play.value = "Stop"; + // trigger an add of all added items + ids = this._data.getIds({ filter: this._options && this._options.filter }); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this._ids[id] = true; + } + this.length = ids.length; + this._trigger("add", { items: ids }); + + // subscribe to new dataset + if (this._data.on) { + this._data.on("*", this.listener); + } } }; /** - * Stop playing + * Refresh the DataView. Useful when the DataView has a filter function + * containing a variable parameter. */ - Slider.prototype.stop = function () { - clearInterval(this.playTimeout); - this.playTimeout = undefined; + DataView.prototype.refresh = function () { + var id; + var ids = this._data.getIds({ filter: this._options && this._options.filter }); + var newIds = {}; + var added = []; + var removed = []; - if (this.frame) { - this.frame.play.value = "Play"; + // 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++; + } } - }; - /** - * 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; - }; + // 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--; + } + } + } - /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds - */ - Slider.prototype.setPlayInterval = function (interval) { - this.playInterval = interval; + // trigger events + if (added.length) { + this._trigger("add", { items: added }); + } + if (removed.length) { + this._trigger("remove", { items: removed }); + } }; /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds + * Get data from the data view + * + * Usage: + * + * get() + * get(options: Object) + * get(options: Object, data: Array | DataTable) + * + * get(id: Number) + * get(id: Number, options: Object) + * get(id: Number, options: Object, data: Array | DataTable) + * + * get(ids: Number[]) + * get(ids: Number[], options: Object) + * get(ids: Number[], options: Object, data: Array | DataTable) + * + * Where: + * + * {Number | String} id The id of an item + * {Number[] | String{}} ids An array with ids of items + * {Object} options An Object with options. Available options: + * {String} [type] Type of data to be returned. Can + * be 'DataTable' or 'Array' (default) + * {Object.} [convert] + * {String[]} [fields] field names to be returned + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * {Array | DataTable} [data] If provided, items will be appended to this + * array or table. Required in case of Google + * DataTable. + * @param args */ - Slider.prototype.getPlayInterval = function (interval) { - return this.playInterval; - }; + DataView.prototype.get = function (args) { + var me = this; - /** - * 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; - }; + // parse the arguments + var ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == "String" || firstType == "Number" || firstType == "Array") { + // get(id(s) [, options] [, data]) + ids = arguments[0]; // can be a single id or an array with ids + options = arguments[1]; + data = arguments[2]; + } else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); - /** - * Execute the onchange callback function - */ - Slider.prototype.onChange = function () { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); + // 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); + }; } - }; - - /** - * 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"; + // build up the call to the linked data set + var getArguments = []; + if (ids != undefined) { + getArguments.push(ids); } - }; + getArguments.push(viewOptions); + getArguments.push(data); + return this._data && this._data.get.apply(this._data, getArguments); + }; /** - * Set the list with values for the slider - * @param {Array} values A javascript array with values (any type) + * Get ids of all items or from a filtered set of items. + * @param {Object} [options] An Object with options. Available options: + * {function} [filter] filter items + * {String | function} [order] Order the items by + * a field name or custom sort function. + * @return {Array} ids */ - Slider.prototype.setValues = function (values) { - this.values = values; + DataView.prototype.getIds = function (options) { + var ids; - if (this.values.length > 0) this.setIndex(0);else this.index = undefined; - }; + if (this._data) { + var defaultFilter = this._options.filter; + var filter; - /** - * Select a value by its index - * @param {Number} index - */ - Slider.prototype.setIndex = function (index) { - if (index < this.values.length) { - this.index = index; + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + }; + } else { + filter = options.filter; + } + } else { + filter = defaultFilter; + } - this.redraw(); - this.onChange(); + ids = this._data.getIds({ + filter: filter, + order: options && options.order + }); } else { - throw "Error: index out of range"; + ids = []; } + + return ids; }; /** - * retrieve the index of the currently selected vaue - * @return {Number} index + * Get the DataSet to which this DataView is connected. In case there is a chain + * of multiple DataViews, the root DataSet of this chain is returned. + * @return {DataSet} dataSet */ - Slider.prototype.getIndex = function () { - return this.index; + DataView.prototype.getDataSet = function () { + var dataSet = this; + while (dataSet instanceof DataView) { + dataSet = dataSet._data; + } + return dataSet || null; }; - /** - * retrieve the currently selected value - * @return {*} value + * Event listener. Will propagate all events from the connected data set to + * the subscribers of the DataView, but will filter the items and only trigger + * when there are changes in the filtered data set. + * @param {String} event + * @param {Object | null} params + * @param {String} senderId + * @private */ - Slider.prototype.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; - }; + DataView.prototype._onEvent = function (event, params, senderId) { + var i, + len, + id, + item, + ids = params && params.items, + data = this._data, + added = [], + updated = [], + removed = []; - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + 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); + } + } - var x = index / (this.values.length - 1) * width; - var left = x + 3; + break; - return left; - }; + case "update": + // determine the event from the views viewpoint: an updated + // item can be added, updated, or removed from this view. + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + if (item) { + if (this._ids[id]) { + updated.push(id); + } else { + this._ids[id] = true; + added.push(id); + } + } else { + if (this._ids[id]) { + delete this._ids[id]; + removed.push(id); + } else {} + } + } + break; - Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; + 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); + } + } - var index = this.leftToIndex(x); + break; + } - this.setIndex(index); + this.length += added.length - removed.length; - util.preventDefault(); + if (added.length) { + this._trigger("add", { items: added }, senderId); + } + if (updated.length) { + this._trigger("update", { items: updated }, senderId); + } + if (removed.length) { + this._trigger("remove", { items: removed }, senderId); + } + } }; + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; - 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(); - }; + // 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 = Slider; + module.exports = DataView; + // nothing interesting for me :-( /***/ }, -/* 12 */ +/* 10 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + 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); + /** - * @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(); - * } + * @constructor Graph3d + * Graph3d displays data in 3d. * - * Version: 1.0 + * Graph3d is developed in javascript as a Google Visualization Chart. * - * @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, ...) + * @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 StepNumber(start, end, step, prettyStep) { - // set default values - this._start = 0; - this._end = 0; - this._step = 1; - this.prettyStep = true; - this.precision = 5; + function Graph3d(container, data, options) { + if (!(this instanceof Graph3d)) { + throw new SyntaxError("Constructor must be called with the new operator"); + } - this._current = 0; - this.setRange(start, end, step, prettyStep); - }; + // 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%"; - /** - * 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.xLabel = "x"; + this.yLabel = "y"; + this.zLabel = "z"; - this.setStep(step, prettyStep); - }; + var passValueFn = function (v) { + return v; + }; + this.xValueLabel = passValueFn; + this.yValueLabel = passValueFn; + this.zValueLabel = passValueFn; - /** - * 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; + this.filterLabel = "time"; + this.legendLabel = "value"; - if (prettyStep !== undefined) this.prettyStep = prettyStep; + 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 (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; - }; + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - /** - * Calculate a nice step size, closest to the desired step size. - * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an - * integer Number. For example 1, 2, 5, 10, 20, 50, etc... - * @param {Number} step Desired step size - * @return {Number} Nice step size - */ - StepNumber.calculatePrettyStep = function (step) { - var log10 = function (x) { - return Math.log(x) / Math.LN10; - }; + this.camera = new Camera(); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + + this.dataTable = null; // The original data table + this.dataPoints = null; // The table with point objects + + // the column indexes + this.colX = undefined; + this.colY = undefined; + this.colZ = undefined; + this.colValue = undefined; + this.colFilter = undefined; + + this.xMin = 0; + this.xStep = undefined; // auto by default + this.xMax = 1; + this.yMin = 0; + this.yStep = undefined; // auto by default + this.yMax = 1; + this.zMin = 0; + this.zStep = undefined; // auto by default + this.zMax = 1; + this.valueMin = 0; + this.valueMax = 1; + this.xBarWidth = 1; + this.yBarWidth = 1; + // TODO: customize axis range - // 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))); + // constants + this.colorAxis = "#4D4D4D"; + this.colorGrid = "#D3D3D3"; + this.colorDot = "#7DC1FF"; + this.colorDotBorder = "#3267D2"; - // 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; + // create a frame and canvas + this.create(); - // for safety - if (prettyStep <= 0) { - prettyStep = 1; + // apply options (also when undefined) + this.setOptions(options); + + // apply data + if (data) { + this.setData(data); } + } - return prettyStep; - }; + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); /** - * returns the current value of the step - * @return {Number} current value + * Calculate the scaling values, dependent on the range in x, y, and z direction */ - StepNumber.prototype.getCurrent = function () { - return parseFloat(this._current.toPrecision(this.precision)); - }; + Graph3d.prototype._setScale = function () { + this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin)); - /** - * returns the current step size - * @return {Number} current step size - */ - StepNumber.prototype.getStep = function () { - return this._step; - }; + // 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; + } + } - /** - * 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; + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? + + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); + + // position the camera arm + var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x; + var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y; + var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z; + this.camera.setArmLocation(xCenter, yCenter, zCenter); }; + /** - * Do a step, add the step size to the current value + * Convert a 3D location to a 2D location on screen + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point2d} point2d A 2D point with parameters x, y */ - StepNumber.prototype.next = function () { - this._current += this._step; + Graph3d.prototype._convert3Dto2D = function (point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); }; /** - * Returns true whether the end is reached - * @return {boolean} True if the current value has passed the end value. + * Convert a 3D location its translation seen from the camera + * http://en.wikipedia.org/wiki/3D_projection + * @param {Point3d} point3d A 3D point with parameters x, y, z + * @return {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera */ - StepNumber.prototype.end = function () { - return this._current > this._end; - }; + 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, - module.exports = StepNumber; -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { + // 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), - "use strict"; - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(45); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(17); - var Core = __webpack_require__(46); - var TimeAxis = __webpack_require__(35); - var CurrentTime = __webpack_require__(26); - var CustomTime = __webpack_require__(27); - var ItemSet = __webpack_require__(32); + // calculate translation + dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz), + dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)), + dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx)); + + return new Point3d(dx, dy, dz); + }; /** - * 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 + * 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 */ - function Timeline(container, items, groups, options) { - if (!(this instanceof Timeline)) { - throw new SyntaxError("Constructor must be called with the new operator"); - } + 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 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; + // 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()); } - 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); + // 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.itemsData = null; // DataSet - this.groupsData = null; // DataSet + /** + * 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; - // apply options - if (options) { - this.setOptions(options); + 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"; } - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + "px"; + this.frame.style.borderStyle = "solid"; + }; - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); - } - } - // Extend the functionality from Core - Timeline.prototype = new Core(); + /// 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 + }; /** - * 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. + * 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 */ - Timeline.prototype.redraw = function () { - this.itemSet && this.itemSet.markDirty({ refreshItems: true }); - this._redraw(); + 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; }; /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ - Timeline.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; + 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; - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; + } + } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + // 4 columns expected, and optionally a 5th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = 3; + + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; + } } else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: "Date", - end: "Date" - } - }); + throw "Unknown style \"" + this.style + "\""; } + }; - // 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(); - } + Graph3d.prototype.getNumberOfRows = function (data) { + return data.length; + }; - 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 }); + Graph3d.prototype.getNumberOfColumns = function (data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; } } + return counter; }; - /** - * 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); + 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; }; - /** - * 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); + 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; }; /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * 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 */ - Timeline.prototype.getSelection = function () { - return this.itemSet && this.itemSet.getSelection() || []; - }; + Graph3d.prototype._dataInitialize = function (rawData, style) { + var me = this; - /** - * 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; + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off("*", this._onChange); + } - var ids = Array.isArray(id) ? id : [id]; + if (rawData === undefined) return; - // get the specified item(s) - var itemsData = this.itemsData.getDataSet().get(ids, { - type: { - start: "Date", - end: "Date" - } - }); + if (Array.isArray(rawData)) { + rawData = new DataSet(rawData); + } - // 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(); + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } else { + throw new Error("Array, DataSet, or DataView expected"); + } - if (start === null || s < start) { - start = s; - } + if (data.length == 0) return; + + this.dataSet = rawData; + this.dataTable = data; + + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on("*", this._onChange); + + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange + + // determine the location of x,y,z,value,filter columns + this.colX = "x"; + this.colY = "y"; + this.colZ = "z"; + this.colValue = "style"; + this.colFilter = "filter"; - 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); + // 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(); + }); + } } - }; - /** - * 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 + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; - // calculate maximum value of fields 'start' and 'end' - var maxStartItem = dataset.max("start"); - if (maxStartItem) { - max = util.convert(maxStartItem.start, "Date").valueOf(); + // 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; } - 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()); - } + + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; + } else { + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 1; } } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; - }; + // calculate minimums and maximums + var xRange = this.getColumnRange(data, this.colX); + if (withBars) { + xRange.min -= this.xBarWidth / 2; + xRange.max += this.xBarWidth / 2; + } + this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min; + this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max; + if (this.xMax <= this.xMin) this.xMax = this.xMin + 1; + this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5; + var yRange = this.getColumnRange(data, this.colY); + if (withBars) { + yRange.min -= this.yBarWidth / 2; + yRange.max += this.yBarWidth / 2; + } + this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min; + this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max; + if (this.yMax <= this.yMin) this.yMax = this.yMin + 1; + this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5; - module.exports = Timeline; + 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; -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { + if (this.colValue !== undefined) { + var valueRange = this.getColumnRange(data, this.colValue); + this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; + this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; + if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; + } + + // set the scale dependent on the ranges. + this._setScale(); + }; - "use strict"; - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(45); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(17); - var Core = __webpack_require__(46); - var TimeAxis = __webpack_require__(35); - var CurrentTime = __webpack_require__(26); - var CustomTime = __webpack_require__(27); - var LineGraph = __webpack_require__(34); /** - * 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 + * Filter the data based on the current filter + * @param {Array} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen */ - 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, + 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; - orientation: "bottom", - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); + var dataPoints = []; - // Create the DOM, props, and emitter - this._create(container); + 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 - // all components listed here will be repainted automatically - this.components = []; + // 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; - 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 (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } } - }; - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; + var sortNumber = function (a, b) { + return a - b; + }; + dataX.sort(sortNumber); + dataY.sort(sortNumber); - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); - //this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); + // 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; - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - // 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 (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - // item set - this.linegraph = new LineGraph(this.body); - this.components.push(this.linegraph); + var point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); - // apply options - if (options) { - this.setOptions(options); - } + dataMatrix[xIndex][yIndex] = obj; - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + dataPoints.push(obj); + } - // create itemset - if (items) { - this.setItems(items); + // 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 { - this._redraw(); + // 'dot', 'dot-line', etc. + // copy all values from the google data table to a list with Point3d objects + for (i = 0; i < data.length; i++) { + point = new Point3d(); + point.x = data[i][this.colX] || 0; + point.y = data[i][this.colY] || 0; + point.z = data[i][this.colZ] || 0; + + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; + } + + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; + + dataPoints.push(obj); + } } - } - // Extend the functionality from Core - Graph2d.prototype = new Core(); + return dataPoints; + }; /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + * 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. */ - Graph2d.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; + Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } - // 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" - } - }); + 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); } - // set items - this.itemsData = newDataSet; - this.linegraph && this.linegraph.setItems(newDataSet); + 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); - 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; + // 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' - this.setWindow(start, end, { animate: false }); - } else { - this.fit({ animate: false }); - } - } + util.addEventListener(this.frame.canvas, "keydown", onkeydown); + util.addEventListener(this.frame.canvas, "mousedown", onmousedown); + util.addEventListener(this.frame.canvas, "touchstart", ontouchstart); + util.addEventListener(this.frame.canvas, "mousewheel", onmousewheel); + util.addEventListener(this.frame.canvas, "mousemove", ontooltip); + + // add the new graph to the container element + this.containerElement.appendChild(this.frame); }; + /** - * Set groups - * @param {vis.DataSet | Array | google.visualization.DataTable} groups + * 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%') */ - 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); - } + Graph3d.prototype.setSize = function (width, height) { + this.frame.style.width = width; + this.frame.style.height = height; - this.groupsData = newDataSet; - this.linegraph.setGroups(newDataSet); + this._resizeCanvas(); }; /** - * 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 + * Resize the canvas to the current size of the frame */ - 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; - } + 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"; }; /** - * This checks if the visible option of the supplied group (by ID) is true or false. - * @param groupId - * @returns {*} + * Start animation */ - 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; - } + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw "No animation available"; + + this.frame.filter.slider.play(); }; /** - * 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 + * Stop animation */ - Graph2d.prototype.getItemRange = function () { - var min = null; - var max = null; + Graph3d.prototype.animationStop = function () { + if (!this.frame.filter || !this.frame.filter.slider) return; - // calculate min from start filed - for (var groupId in this.linegraph.groups) { - if (this.linegraph.groups.hasOwnProperty(groupId)) { - if (this.linegraph.groups[groupId].visible == true) { - for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) { - var item = this.linegraph.groups[groupId].itemsData[i]; - var value = util.convert(item.x, "Date").valueOf(); - min = min == null ? value : min > value ? value : min; - max = max == null ? value : max < value ? value : max; - } - } - } + this.frame.filter.slider.stop(); + }; + + + /** + * Resize the center position based on the current values in this.defaultXCenter + * and this.defaultYCenter (which are strings with a percentage or a value + * in pixels). The center positions are the variables this.xCenter + * and this.yCenter + */ + Graph3d.prototype._resizeCenter = function () { + // calculate the horizontal center position + if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === "%") { + this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; + } else { + this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; + // calculate the vertical center position + if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === "%") { + this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight); + } else { + this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px + } }; + /** + * Set the rotation and distance of the camera + * @param {Object} pos An object with the camera position. The object + * contains three parameters: + * - horizontal {Number} + * The horizontal rotation, between 0 and 2*PI. + * Optional, can be left undefined. + * - vertical {Number} + * The vertical rotation, between 0 and 0.5*PI + * if vertical=0.5*PI, the graph is shown from the + * top. Optional, can be left undefined. + * - distance {Number} + * The (normalized) distance of the camera to the + * center of the graph, a value between 0.71 and 5.0. + * Optional, can be left undefined. + */ + Graph3d.prototype.setCameraPosition = function (pos) { + if (pos === undefined) { + return; + } + + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); + } + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); + } - module.exports = Graph2d; + this.redraw(); + }; -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + /** + * 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; + }; /** - * Created by Alex on 10/3/2014. + * Load data into the 3D Graph */ - var moment = __webpack_require__(44); + Graph3d.prototype._readData = function (data) { + // read the data + this._dataInitialize(data, this.style); + + + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); + } + // draw the filter + this._redrawFilter(); + }; /** - * used in Core to convert the options into a volatile variable - * - * @param Core + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data */ - 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 - } + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); + + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); } }; - /** - * create new entrees for the repeating hidden dates - * @param body - * @param hiddenDates + * Update the options. Options will be merged with current options + * @param {Object} options */ - exports.updateHiddenDates = function (body, hiddenDates) { - if (hiddenDates && body.domProps.centerContainer.width !== undefined) { - exports.convertHiddenOptions(body, hiddenDates); + Graph3d.prototype.setOptions = function (options) { + var cameraPosition = undefined; - var start = moment(body.range.start); - var end = moment(body.range.end); + this.animationStop(); - var totalRange = body.range.end - body.range.start; - var pixelTime = totalRange / body.domProps.centerContainer.width; + if (options !== undefined) { + // retrieve parameter values + if (options.width !== undefined) this.width = options.width; + if (options.height !== undefined) this.height = options.height; - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat !== undefined) { - var startDate = moment(hiddenDates[i].start); - var endDate = moment(hiddenDates[i].end); + if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; + if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; - 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); - } + 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; - 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"); + if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; + if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; + if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; - endDate.dayOfYear(start.dayOfYear()); - endDate.year(start.year()); - endDate.subtract(7 - offset, "days"); + 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; - runUntil.add(1, "weeks"); - break; - case "weekly": - var dayOffset = endDate.diff(startDate, "days"); - var day = startDate.day(); + if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; + if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; + if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; - // set the start date to the range.start - startDate.date(start.date()); - startDate.month(start.month()); - startDate.year(start.year()); - endDate = startDate.clone(); + if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; + if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; - // force - startDate.day(day); - endDate.day(day); - endDate.add(dayOffset, "days"); + 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; - startDate.subtract(1, "weeks"); - endDate.subtract(1, "weeks"); + if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; - runUntil.add(1, "weeks"); - break; - case "monthly": - if (startDate.month() != endDate.month()) { - offset = 1; - } - startDate.month(start.month()); - startDate.year(start.year()); - startDate.subtract(1, "months"); + if (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); + } + } - endDate.month(start.month()); - endDate.year(start.year()); - endDate.subtract(1, "months"); - endDate.add(offset, "months"); + this._setBackgroundColor(options && options.backgroundColor); - 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"); + this.setSize(this.width, this.height); - 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); - } + // re-load the data + if (this.dataTable) { + this.setData(this.dataTable); } - }; + // start animation when option is true + if (this.animationAutoStart && this.dataFilter) { + this.animationStart(); + } + }; /** - * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. - * Scales with N^2 - * @param body + * Redraw the Graph. */ - exports.removeDuplicates = function (body) { - var hiddenDates = body.hiddenDates; - var safeDates = []; - for (var i = 0; i < hiddenDates.length; i++) { - for (var j = 0; j < hiddenDates.length; j++) { - if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { - // j inside i - if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[j].remove = true; - } - // j start inside i - else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { - hiddenDates[i].end = hiddenDates[j].end; - hiddenDates[j].remove = true; - } - // j end inside i - else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { - hiddenDates[i].start = hiddenDates[j].start; - hiddenDates[j].remove = true; - } - } - } + Graph3d.prototype.redraw = function () { + if (this.dataPoints === undefined) { + throw "Error: graph data not initialized"; } - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].remove !== true) { - safeDates.push(hiddenDates[i]); - } + this._resizeCanvas(); + this._resizeCenter(); + this._redrawSlider(); + this._redrawClear(); + this._redrawAxis(); + + if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) { + this._redrawDataGrid(); + } else if (this.style === Graph3d.STYLE.LINE) { + this._redrawDataLine(); + } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + this._redrawDataBar(); + } else { + // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE + this._redrawDataDot(); } - body.hiddenDates = safeDates; - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time + this._redrawInfo(); + this._redrawLegend(); }; - 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); - } + /** + * Clear the canvas before redrawing + */ + Graph3d.prototype._redrawClear = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + + ctx.clearRect(0, 0, canvas.width, canvas.height); }; + /** - * Used in TimeStep to avoid the hidden times. - * @param timeStep - * @param previousTime + * Redraw the legend showing the colors */ - 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; + Graph3d.prototype._redrawLegend = function () { + var y; + + if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { + var dotSize = this.frame.clientWidth * 0.02; + + var widthMin, widthMax; + if (this.style === Graph3d.STYLE.DOTSIZE) { + widthMin = dotSize / 2; // px + widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function + } else { + widthMin = 20; // px + widthMax = 20; // px } + + var height = Math.max(this.frame.clientHeight * 0.25, 100); + var top = this.margin; + var right = this.frame.clientWidth - this.margin; + var left = right - widthMax; + var bottom = top + height; } - 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 canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + ctx.lineWidth = 1; + ctx.font = "14px arial"; // TODO: put in options + + if (this.style === Graph3d.STYLE.DOTCOLOR) { + // draw the color bar + var ymin = 0; + var ymax = height; // Todo: make height customizable + for (y = ymin; y < ymax; y++) { + var f = (y - ymin) / (ymax - ymin); + + //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function + var hue = f * 240; + var color = this._hsv2rgb(hue, 1, 1); + + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(left, top + y); + ctx.lineTo(right, top + y); + ctx.stroke(); } - timeStep.current = newValue.toDate(); + ctx.strokeStyle = this.colorAxis; + ctx.strokeRect(left, top, widthMax, height); } - }; - - ///** - // * 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(); - // } - //}; + 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(); + } - /** - * replaces the Core toScreen methods - * @param Core - * @param time - * @param width - * @returns {number} - */ - exports.toScreen = function (Core, time, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return (time.valueOf() - conversion.offset) * conversion.scale; - } else { - var hidden = exports.isHidden(time, Core.body.hiddenDates); - if (hidden.hidden == true) { - time = hidden.startDate; + if (this.style === Graph3d.STYLE.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; - var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); - var conversion = Core.range.conversion(width, duration); - return (time.valueOf() - conversion.offset) * conversion.scale; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + + step.next(); + } + + ctx.textAlign = "right"; + ctx.textBaseline = "top"; + var label = this.legendLabel; + ctx.fillText(label, right, bottom + this.margin); } }; - /** - * Replaces the core toTime methods - * @param body - * @param range - * @param x - * @param width - * @returns {Date} + * Redraw the filter */ - exports.toTime = function (Core, x, width) { - if (Core.body.hiddenDates.length == 0) { - var conversion = Core.range.conversion(width); - return new Date(x / conversion.scale + conversion.offset); - } else { - var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - var totalDuration = Core.range.end - Core.range.start - hiddenDuration; - var partialDuration = totalDuration * x / width; - var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); + Graph3d.prototype._redrawFilter = function () { + this.frame.filter.innerHTML = ""; - var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); - return newTime; + if (this.dataFilter) { + var options = { + visible: this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; + + // TODO: css here is not nice here... + this.frame.filter.style.padding = "10px"; + //this.frame.filter.style.backgroundColor = '#EFEFEF'; + + slider.setValues(this.dataFilter.values); + slider.setPlayInterval(this.animationInterval); + + // create an event handler + var me = this; + var onchange = function () { + var index = slider.getIndex(); + + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); + + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } else { + this.frame.filter.slider = undefined; } }; - - /** - * Support function - * - * @param hiddenDates - * @param range - * @returns {number} + /** + * Redraw the slider */ - 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; - } + Graph3d.prototype._redrawSlider = function () { + if (this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); } - return duration; }; /** - * Support function - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} + * Redraw common information */ - exports.correctTimeForHidden = function (hiddenDates, range, time) { - time = moment(time).toDate().valueOf(); - time -= exports.getHiddenDurationBefore(hiddenDates, range, time); - return time; - }; + Graph3d.prototype._redrawInfo = function () { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); - exports.getHiddenDurationBefore = function (hiddenDates, range, time) { - var timeOffset = 0; - time = moment(time).toDate().valueOf(); + ctx.font = "14px arial"; // TODO: put in options + ctx.lineStyle = "gray"; + ctx.fillStyle = "gray"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; - 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 = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); } - 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}} + * Redraw the axis */ - 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; - } - } - } + 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; - return hiddenDuration; - }; + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + "px arial"; + // calculate the length for the short grid lines + var gridLenX = 0.025 / this.scale.x; + var gridLenY = 0.025 / this.scale.y; + var textMargin = 5 / this.camera.getArmLength(); // px + var armAngle = this.camera.getArmRotation().horizontal; + // draw x-grid lines + ctx.lineWidth = 1; + prettyStep = this.defaultXStep === undefined; + step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); + step.start(); + if (step.getCurrent() < this.xMin) { + step.next(); + } + while (!step.end()) { + var x = step.getCurrent(); - /** - * 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; - } + 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 { - if (correctionEnabled == true) { - return isHidden.endDate + (time - isHidden.startDate) + 1; - } else { - return isHidden.endDate + 1; - } + from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + + from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); } - } else { - return time; - } - }; + 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); - /** - * 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; + step.next(); + } - if (time >= startDate && time < endDate) { - // if the start is entering a hidden zone - return { hidden: true, startDate: startDate, endDate: endDate }; - break; - } + // 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(); } - return { hidden: false, startDate: startDate, endDate: endDate }; - }; + 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(); -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { + 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(); + } - "use strict"; + 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); - /** - * @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; + step.next(); + } - this.autoScale = true; - this.stepIndex = 0; - this.step = 1; - this.scale = 1; + // 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(); - this.marginStart; - this.marginEnd; - this.deadSpace = 0; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(this.zValueLabel(step.getCurrent()) + " ", from.x - 5, from.y); - this.majorSteps = [1, 2, 5, 10]; - this.minorSteps = [0.25, 0.5, 1, 2]; + 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(); - this.alignZeros = alignZeros; + // 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(); - this.setRange(start, end, minimumStep, containerHeight, customRange); - } + // 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"; + } else { + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + } + ctx.fillStyle = this.colorAxis; + ctx.fillText(yLabel, text.x, text.y); + } + // draw z-label + var zLabel = this.zLabel; + if (zLabel.length > 0) { + offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis? + xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; + yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; + zText = (this.zMin + this.zMax) / 2; + text = this._convert3Dto2D(new Point3d(xText, yText, zText)); + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(zLabel, text.x - offset, text.y); + } + }; /** - * 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 + * 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 */ - 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; + Graph3d.prototype._hsv2rgb = function (H, S, V) { + var R, G, B, C, Hi, X; - if (this._start == this._end) { - this._start -= 0.75; - this._end += 1; - } + C = V * S; + Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(H / 60 % 2 - 1)); - if (this.autoScale == true) { - this.setMinimumStep(minimumStep, containerHeight); + switch (Hi) { + case 0: + R = C;G = X;B = 0;break; + case 1: + R = X;G = C;B = 0;break; + case 2: + R = 0;G = C;B = X;break; + case 3: + R = 0;G = X;B = C;break; + case 4: + R = X;G = 0;B = C;break; + case 5: + R = C;G = 0;B = X;break; + + default: + R = 0;G = 0;B = 0;break; } - this.setFirst(customRange); + return "RGB(" + parseInt(R * 255) + "," + parseInt(G * 255) + "," + parseInt(B * 255) + ")"; }; + /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds + * Draw all datapoints as a grid + * This function can be used when the style is 'grid' */ - DataStep.prototype.setMinimumStep = function (minimumStep, containerHeight) { - // round to floor - var size = this._end - this._start; - var safeSize = size * 1.2; - var minimumStepValue = minimumStep * (safeSize / containerHeight); - var orderOfMagnitude = Math.round(Math.log(safeSize) / Math.LN10); - - var minorStepIdx = -1; - var magnitudefactor = Math.pow(10, orderOfMagnitude); + Graph3d.prototype._redrawDataGrid = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext("2d"), + point, + right, + top, + cross, + i, + topSideVisible, + fillStyle, + strokeStyle, + lineWidth, + h, + s, + v, + zAvg; - var 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]; - }; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + // calculate the translations and screen position of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - /** - * 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 = {}; + // 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 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; + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; - this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; + 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; - // 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 && 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.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; - this.marginRange = this.marginEnd - this.marginStart; + 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 - this.current = this.marginEnd; - }; + 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; - 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]; + 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 { - return rounded; - } - }; + // grid style + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + if (point !== undefined) { + if (this.showPerspective) { + lineWidth = 2 / -point.trans.z; + } else { + lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); + } + } - /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date - */ - DataStep.prototype.hasNext = function () { - return this.current >= this.marginStart; - }; + if (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; - /** - * Do the next step - */ - DataStep.prototype.next = function () { - var prev = this.current; - this.current -= this.step; + 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(); + } - // safety mechanism: if current time is still unchanged, move to the end - if (this.current == prev) { - this.current = this._end; + if (point !== undefined && top !== undefined) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + top.point.z) / 2; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + + ctx.lineWidth = lineWidth; + ctx.strokeStyle = this._hsv2rgb(h, 1, 1); + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + ctx.lineTo(top.screen.x, top.screen.y); + ctx.stroke(); + } + } } }; + /** - * Do the next step + * Draw all datapoints as dots. + * This function can be used when the style is 'dot' or 'dot-line' */ - DataStep.prototype.previous = function () { - this.current += this.step; - this.marginEnd += this.step; - this.marginRange = this.marginEnd - this.marginStart; - }; + Graph3d.prototype._redrawDataDot = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + var i; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + // calculate the translations of all points + for (i = 0; i < this.dataPoints.length; i++) { + var trans = this._convertPointToTranslation(this.dataPoints[i].point); + var screen = this._convertTranslationToScreen(trans); + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - /** - * 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); + // calculate the distance from the point at the bottom to the camera + var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom); + this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z; + } - // If 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); + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); + + // 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(); } - 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; + + // 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; } - if (index > toPrecision.length) { - // We need to add zeros! - for (var cnt = index - toPrecision.length; cnt > 0; cnt--) { - toPrecision += "0"; - } + + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; } else { - // we need to remove characters - toPrecision = toPrecision.slice(0, index); + radius = size * -(this.eye.z / this.camera.getArmLength()); } - // 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; - } - } + if (radius < 0) { + radius = 0; } - } - return toPrecision; + 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); + } + + // 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(); + } }; /** - * 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. + * Draw all datapoints as bars. + * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - DataStep.prototype.isMajor = function () { - return this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0; - }; - - module.exports = DataStep; + Graph3d.prototype._redrawDataBar = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + var i, j, surface, corners; -/***/ }, -/* 17 */ -/***/ function(module, exports, __webpack_require__) { + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - "use strict"; + // 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; - var util = __webpack_require__(1); - var hammerUtil = __webpack_require__(47); - var moment = __webpack_require__(44); - var Component = __webpack_require__(25); - var DateUtil = __webpack_require__(15); + // 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; + } - /** - * @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 + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - this.body = body; - this.deltaDifference = 0; - this.scaleOffset = 0; - this.startToFront = false; - this.endToFront = true; + // 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]; - // 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); + // 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.props = { - touch: {} - }; - this.animateTimer = null; + // 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); + } - // 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)); + // 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) }]; - // ignore dragging when holding - this.body.emitter.on("press", this._onHold.bind(this)); + // 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); + }); - // mouse wheel for zooming - this.body.emitter.on("mousewheel", this._onMouseWheel.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; - // pinch to zoom - this.body.emitter.on("touch", this._onTouch.bind(this)); - this.body.emitter.on("pinch", this._onPinch.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}) + } - this.setOptions(options); - } + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; - Range.prototype = new Component(); + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; - /** - * 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); + // both are equal + return 0; + }); - if ("start" in options || "end" in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); + // 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(); } } }; - /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' - */ - function validateDirection(direction) { - if (direction != "horizontal" && direction != "vertical") { - throw new TypeError("Unknown direction \"" + direction + "\". " + "Choose \"horizontal\" or \"vertical\"."); - } - } /** - * Set a new start and end range - * @param {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] - * + * Draw a line through all datapoints. + * This function can be used when the style is 'line' */ - 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(); + Graph3d.prototype._redrawDataLine = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext("2d"), + point, + i; - 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; + 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); - 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.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; + } - 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 }); - } + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; - 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); - } - } - }; + ctx.lineWidth = 1; // TODO: make customizable + ctx.strokeStyle = "blue"; // TODO: make customizable + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + } - 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); - } + // 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(); } }; /** - * Stop an animation - * @private + * Start a moving operation inside the provided parent element + * @param {Event} event The event that occurred (required for + * retrieving the mouse position) */ - Range.prototype._cancelAnimation = function () { - if (this.animateTimer) { - clearTimeout(this.animateTimer); - this.animateTimer = null; + 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); } + + // 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); }; + /** - * 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 + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the 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; + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error("Invalid start \"" + start + "\""); + // 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); + + // 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 (isNaN(newEnd) || newEnd === null) { - throw new Error("Invalid end \"" + end + "\""); + if (Math.abs(Math.cos(horizontalNew)) < snapValue) { + horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; } - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; + // 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; } - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = min - newStart; - newStart += diff; - newEnd += diff; + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } - } + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit("cameraPositionChange", parameters); - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = newEnd - max; - newStart -= diff; - newEnd -= diff; + util.preventDefault(event); + }; - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } - } + + /** + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event + */ + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = "auto"; + this.leftButtonDown = false; + + // remove event listeners here + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); + util.preventDefault(event); + }; + + /** + * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point + * @param {Event} event A mouse move event + */ + Graph3d.prototype._onTooltip = function (event) { + var delay = 300; // ms + var boundingRect = this.frame.getBoundingClientRect(); + var mouseX = getMouseX(event) - boundingRect.left; + var mouseY = getMouseY(event) - boundingRect.top; + + if (!this.showTooltip) { + return; } - // 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 (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); } - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; + } - 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; + 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 { - // zoom to the maximum - diff = newEnd - newStart - zoomMax; - newStart += diff / 2; - newEnd -= diff / 2; + this._hideTooltip(); } } - } - - var changed = this.start != newStart || this.end != newEnd; + } else { + // tooltip is currently not visible + var me = this; + this.tooltipTimeout = setTimeout(function () { + me.tooltipTimeout = null; - // 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"); + // show a tooltip if we have a data point + var dataPoint = me._dataPointFromXY(mouseX, mouseY); + if (dataPoint) { + me._showTooltip(dataPoint); + } + }, delay); } - - this.start = newStart; - this.end = newEnd; - return changed; }; /** - * Retrieve the current range. - * @return {Object} An object with start and end properties + * Event handler for touchstart event on mobile devices */ - Range.prototype.getRange = function () { - return { - start: this.start, - end: this.end + Graph3d.prototype._onTouchStart = function (event) { + this.touchDown = true; + + var me = this; + this.ontouchmove = function (event) { + me._onTouchMove(event); + }; + this.ontouchend = function (event) { + me._onTouchEnd(event); }; + util.addEventListener(document, "touchmove", me.ontouchmove); + util.addEventListener(document, "touchend", me.ontouchend); + + this._onMouseDown(event); }; /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion + * Event handler for touchmove event on mobile devices */ - Range.prototype.conversion = function (width, totalHidden) { - return Range.conversion(this.start, this.end, width, totalHidden); + Graph3d.prototype._onTouchMove = function (event) { + this._onMouseMove(event); }; /** - * 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 + * Event handler for touchend event on mobile devices */ - 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 - }; - } + Graph3d.prototype._onTouchEnd = function (event) { + this.touchDown = false; + + util.removeEventListener(document, "touchmove", this.ontouchmove); + util.removeEventListener(document, "touchend", this.ontouchend); + + this._onMouseUp(event); }; + /** - * Start dragging horizontally or vertically - * @param {Event} event - * @private + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event */ - Range.prototype._onDragStart = function (event) { - this.deltaDifference = 0; - this.previousDelta = 0; - // only allow dragging when configured as movable - if (!this.options.moveable) return; + Graph3d.prototype._onWheel = function (event) { + if (!event) /* For IE. */ + event = window.event; - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + // retrieve delta + var delta = 0; + if (event.wheelDelta) { + /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { + /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; + } - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.dragging = true; + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + var oldLength = this.camera.getArmLength(); + var newLength = oldLength * (1 - delta / 10); - if (this.body.dom.root) { - this.body.dom.root.style.cursor = "move"; + this.camera.setArmLength(newLength); + this.redraw(); + + this._hideTooltip(); } - event.preventDefault(); + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit("cameraPositionChange", parameters); + + // Prevent default actions caused by mouse wheel. + // That might be ugly, but we handle scrolls somehow + // anyway, so don't bother here.. + util.preventDefault(event); }; /** - * Perform dragging operation - * @param {Event} event + * Test whether a point lies inside given 2D triangle + * @param {Point2d} point + * @param {Point2d[]} triangle + * @return {boolean} Returns true if given point lies inside or on the edge of the triangle * @private */ - 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; + Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; - // 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; + function sign(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; } - 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 - }); + 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)); - event.preventDefault(); + // 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); }; /** - * Stop dragging operation - * @param {event} event + * 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 */ - Range.prototype._onDragEnd = 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; + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, + // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); - this.props.touch.dragging = false; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = "auto"; - } + 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); - // fire a rangechanged event - this.body.emitter.emit("rangechanged", { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } + } + } + } + + + return closestDataPoint; }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event + * Display a tooltip for given data point + * @param {Object} dataPoint * @private */ - Range.prototype._onMouseWheel = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - // retrieve delta - var delta = 0; - if (event.wheelDelta) { - /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { - /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; - } + if (!this.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 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 + line = document.createElement("div"); + line.style.position = "absolute"; + line.style.height = "40px"; + line.style.width = "0"; + line.style.borderLeft = "1px solid #4d4d4d"; - // 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); - } + 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"; - // 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); + 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; + } - this.zoom(scale, pointerDate, delta); + this._hideTooltip(); + + this.tooltip.dataPoint = dataPoint; + if (typeof this.showTooltip === "function") { + content.innerHTML = this.showTooltip(dataPoint.point); + } else { + content.innerHTML = "" + "" + "" + "" + "
x:" + dataPoint.point.x + "
y:" + dataPoint.point.y + "
z:" + dataPoint.point.z + "
"; } - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); + content.style.left = "0"; + content.style.top = "0"; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); + + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; + + var left = dataPoint.screen.x - contentWidth / 2; + left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth); + + line.style.left = dataPoint.screen.x + "px"; + line.style.top = dataPoint.screen.y - lineHeight + "px"; + content.style.left = left + "px"; + content.style.top = dataPoint.screen.y - lineHeight - contentHeight + "px"; + dot.style.left = dataPoint.screen.x - dotWidth / 2 + "px"; + dot.style.top = dataPoint.screen.y - dotHeight / 2 + "px"; }; /** - * Start of a touch gesture + * Hide the tooltip when displayed * @private */ - Range.prototype._onTouch = function (event) { - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.allowDragging = true; - this.props.touch.center = null; - this.scaleOffset = 0; - this.deltaDifference = 0; + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; + + 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); + } + } + } + } }; + /**--------------------------------------------------------------------------**/ + + /** - * On start of a hold gesture - * @private + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x */ - Range.prototype._onHold = function () { - this.props.touch.allowDragging = false; - }; + function getMouseX(event) { + if ("clientX" in event) { + return event.clientX; + }return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + } /** - * Handle pinch event + * Get the vertical mouse position from a mouse event * @param {Event} event - * @private + * @return {Number} mouse y */ - Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + function getMouseY(event) { + if ("clientY" in event) { + return event.clientY; + }return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + } - this.props.touch.allowDragging = false; + module.exports = Graph3d; + // use use defaults - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.center, this.body.dom.center); - } +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { - var scale = 1 / (event.scale + this.scaleOffset); - var centerDate = this._pointerToDate(this.props.touch.center); + + /** + * Expose `Emitter`. + */ - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + module.exports = Emitter; - // 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; + /** + * Initialize a new `Emitter`. + * + * @api public + */ - // 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 + function Emitter(obj) { + if (obj) return mixin(obj); + }; - 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; - } + /** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ - this.setRange(newStart, newEnd, false, true); + function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; + } - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + /** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - event.preventDefault(); + Emitter.prototype.on = + Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; }; /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date - * @private + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; - validateDirection(direction); + Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; - 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; + function on() { + self.off(event, on); + fn.apply(this, arguments); } + + on.fn = fn; + this.on(event, on); + return this; }; /** - * 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 + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - function getPointer(touch, element) { - return { - x: touch.x - util.getAbsoluteLeft(element), - y: touch.y - util.getAbsoluteTop(element) - }; - } - /** - * Zoom the range the given scale in or out. Start and end date will - * be adjusted, and the timeline will be redrawn. You can optionally give a - * date around which to zoom. - * For example, try scale = 0.9 or 1.1 - * @param {Number} scale Scaling factor. Values above 1 will zoom out, - * values below 1 will zoom in. - * @param {Number} [center] Value representing a date around which will - * be zoomed. - */ - Range.prototype.zoom = function (scale, center, delta) { - // if centerDate is not provided, take it half between start Date and end Date - if (center == null) { - center = (this.start + this.end) / 2; - } + Emitter.prototype.off = + Emitter.prototype.removeListener = + Emitter.prototype.removeAllListeners = + Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } - // calculate new start and end - var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; - var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; - // 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; + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; } - this.setRange(newStart, newEnd, false, true); - - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + // 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; }; - - /** - * 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 + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} */ - Range.prototype.move = function (delta) { - // zoom start Date and end Date relative to the centerDate - var diff = this.end - this.start; - // apply new values - var newStart = this.start + diff * delta; - var newEnd = this.end + diff * delta; + Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; - // TODO: reckon with min and max range + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } - this.start = newStart; - this.end = newEnd; + return this; }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public */ - Range.prototype.moveTo = function (moveTo) { - var center = (this.start + this.end) / 2; - var diff = center - moveTo; + Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; + }; - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + /** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ - this.setRange(newStart, newEnd); + Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; }; - module.exports = Range; /***/ }, -/* 18 */ +/* 12 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - // 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 + * @prototype Point3d + * @param {Number} [x] + * @param {Number} [y] + * @param {Number} [z] */ - exports.orderByStart = function (items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); + function Point3d(x, y, z) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + this.z = z !== undefined ? z : 0; }; /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items + * Subtract the two provided points, returns a-b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a-b */ - 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; - }); + 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; }; /** - * 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 + * Add the two provided points, returns a+b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a+b */ - 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; - } - } + 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 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; + /** + * 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); + }; - 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; - } - } + /** + * 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(); - 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); - } - } + 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; }; /** - * 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. + * Rtrieve the length of the vector (or the distance from this point to the origin + * @return {Number} length */ - 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; - } - } + 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"; + /** - * 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 + * @prototype Point2d + * @param {Number} [x] + * @param {Number} [y] */ - exports.collision = function (a, b, margin) { - return a.left - margin.horizontal + EPSILON < b.left + b.width && a.left + a.width + margin.horizontal - EPSILON > b.left && a.top - margin.vertical + EPSILON < b.top + b.height && a.top + a.height + margin.vertical - EPSILON > b.top; - }; + function Point2d(x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + } + + module.exports = Point2d; /***/ }, -/* 19 */ +/* 14 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var moment = __webpack_require__(44); - var DateUtil = __webpack_require__(15); - var util = __webpack_require__(1); + var Point3d = __webpack_require__(12); /** - * @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 + * @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. * - * @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 + * Documentation: + * http://en.wikipedia.org/wiki/3D_projection */ - function TimeStep(start, end, minimumStep, hiddenDates) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); - - this.autoScale = true; - this.scale = "day"; - this.step = 1; - - // initialize the range - this.setRange(start, end, minimumStep); + function Camera() { + this.armLocation = new Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; - // hidden Dates options - this.switchedDay = false; - this.switchedMonth = false; - this.switchedYear = false; - this.hiddenDates = hiddenDates; - if (hiddenDates === undefined) { - this.hiddenDates = []; - } + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - this.format = TimeStep.FORMAT; // default formatting + 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: "" - } - }; - /** - * 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 + * 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 */ - TimeStep.prototype.setFormat = function (format) { - var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); - this.format = util.deepExtend(defaultFormat, format); + Camera.prototype.setArmLocation = function (x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; + + this.calculateCameraOrientation(); }; /** - * 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 + * 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. */ - TimeStep.prototype.setRange = function (start, end, minimumStep) { - if (!(start instanceof Date) || !(end instanceof Date)) { - throw "No legal start or end date in method setRange"; + Camera.prototype.setArmRotation = function (horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; } - this._start = start != undefined ? new Date(start.valueOf()) : new Date(); - this._end = end != undefined ? new Date(end.valueOf()) : new Date(); + 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; + } - if (this.autoScale) { - this.setMinimumStep(minimumStep); + if (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); } }; /** - * Set the range iterator to the start date. + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical */ - TimeStep.prototype.first = function () { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); + Camera.prototype.getArmRotation = function () { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; + + return rot; }; /** - * 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 the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 */ - 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 - } + Camera.prototype.setArmLength = function (length) { + if (length === undefined) return; - 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; - } - } - }; + this.armLength = length; - /** - * 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(); + // Radius must be larger than the corner of the graph, + // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the + // graph + if (this.armLength < 0.71) this.armLength = 0.71; + if (this.armLength > 5) this.armLength = 5; + + this.calculateCameraOrientation(); }; /** - * Do the next step + * Retrieve the arm length + * @return {Number} length */ - TimeStep.prototype.next = function () { - var prev = this.current.valueOf(); - - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case "millisecond": - - - this.current = new Date(this.current.valueOf() + this.step);break; - case "second": - this.current = new Date(this.current.valueOf() + this.step * 1000);break; - case "minute": - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60);break; - case "hour": - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - h % this.step); - break; - case "weekday": - // intentional fall through - case "day": - this.current.setDate(this.current.getDate() + this.step);break; - case "month": - this.current.setMonth(this.current.getMonth() + this.step);break; - case "year": - this.current.setFullYear(this.current.getFullYear() + this.step);break; - default: - break; - } - } else { - switch (this.scale) { - case "millisecond": - this.current = new Date(this.current.valueOf() + this.step);break; - case "second": - this.current.setSeconds(this.current.getSeconds() + this.step);break; - case "minute": - this.current.setMinutes(this.current.getMinutes() + this.step);break; - case "hour": - this.current.setHours(this.current.getHours() + this.step);break; - case "weekday": - // intentional fall through - case "day": - this.current.setDate(this.current.getDate() + this.step);break; - case "month": - this.current.setMonth(this.current.getMonth() + this.step);break; - case "year": - this.current.setFullYear(this.current.getFullYear() + this.step);break; - default: - break; - } - } - - 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; - } - } - - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); - } - - DateUtil.stepOverHiddenDates(this, prev); + Camera.prototype.getArmLength = function () { + return this.armLength; }; - /** - * Get the current datetime - * @return {Date} current The current date + * Retrieve the camera location + * @return {Point3d} cameraLocation */ - TimeStep.prototype.getCurrent = function () { - return this.current; + Camera.prototype.getCameraLocation = function () { + return this.cameraLocation; }; /** - * 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. + * Retrieve the camera rotation + * @return {Point3d} cameraRotation */ - 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; - } + Camera.prototype.getCameraRotation = function () { + return this.cameraRotation; }; /** - * Enable or disable autoscaling - * @param {boolean} enable If true, autoascaling is set true + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm */ - TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; + 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; }; + module.exports = Camera; - /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds - */ - TimeStep.prototype.setMinimumStep = function (minimumStep) { - if (minimumStep == undefined) { - return; - } +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { - //var b = asc + ds; + "use strict"; - 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; + var DataView = __webpack_require__(9); - // find the smallest step that is larger than the provided minimumStep - if (stepYear * 1000 > minimumStep) { - this.scale = "year";this.step = 1000; - } - if (stepYear * 500 > minimumStep) { - this.scale = "year";this.step = 500; - } - if (stepYear * 100 > minimumStep) { - this.scale = "year";this.step = 100; - } - if (stepYear * 50 > minimumStep) { - this.scale = "year";this.step = 50; - } - if (stepYear * 10 > minimumStep) { - this.scale = "year";this.step = 10; - } - if (stepYear * 5 > minimumStep) { - this.scale = "year";this.step = 5; - } - if (stepYear > minimumStep) { - this.scale = "year";this.step = 1; - } - if (stepMonth * 3 > minimumStep) { - this.scale = "month";this.step = 3; - } - if (stepMonth > minimumStep) { - this.scale = "month";this.step = 1; - } - if (stepDay * 5 > minimumStep) { - this.scale = "day";this.step = 5; - } - if (stepDay * 2 > minimumStep) { - this.scale = "day";this.step = 2; - } - if (stepDay > minimumStep) { - this.scale = "day";this.step = 1; - } - if (stepDay / 2 > minimumStep) { - this.scale = "weekday";this.step = 1; - } - if (stepHour * 4 > minimumStep) { - this.scale = "hour";this.step = 4; - } - if (stepHour > minimumStep) { - this.scale = "hour";this.step = 1; - } - if (stepMinute * 15 > minimumStep) { - this.scale = "minute";this.step = 15; - } - if (stepMinute * 10 > minimumStep) { - this.scale = "minute";this.step = 10; - } - if (stepMinute * 5 > minimumStep) { - this.scale = "minute";this.step = 5; - } - if (stepMinute > minimumStep) { - this.scale = "minute";this.step = 1; - } - if (stepSecond * 15 > minimumStep) { - this.scale = "second";this.step = 15; - } - if (stepSecond * 10 > minimumStep) { - this.scale = "second";this.step = 10; - } - if (stepSecond * 5 > minimumStep) { - this.scale = "second";this.step = 5; - } - if (stepSecond > minimumStep) { - this.scale = "second";this.step = 1; - } - if (stepMillisecond * 200 > minimumStep) { - this.scale = "millisecond";this.step = 200; - } - if (stepMillisecond * 100 > minimumStep) { - this.scale = "millisecond";this.step = 100; - } - if (stepMillisecond * 50 > minimumStep) { - this.scale = "millisecond";this.step = 50; - } - if (stepMillisecond * 10 > minimumStep) { - this.scale = "millisecond";this.step = 10; - } - if (stepMillisecond * 5 > minimumStep) { - this.scale = "millisecond";this.step = 5; + /** + * @class Filter + * + * @param {DataSet} data The google data table + * @param {Number} column The index of the column to be filtered + * @param {Graph} graph The graph + */ + function Filter(data, column, graph) { + this.data = data; + this.column = column; + this.graph = graph; // the parent graph + + 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); } - if (stepMillisecond > minimumStep) { - this.scale = "millisecond";this.step = 1; + + // create an array with the filtered datapoints. this will be loaded afterwards + this.dataPoints = []; + + this.loaded = false; + this.onLoadCallback = undefined; + + if (graph.animationPreload) { + this.loaded = false; + this.loadInBackground(); + } else { + this.loaded = true; } }; + /** - * 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 + * Return the label + * @return {string} label */ - TimeStep.snap = function (date, scale, step) { - var clone = new Date(date.valueOf()); + Filter.prototype.isLoaded = function () { + return this.loaded; + }; - 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); - } - 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; - } - } else if (scale == "millisecond") { - var _step = step > 5 ? step / 2 : 1; - clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); + /** + * 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++; } - return clone; + return Math.round(i / len * 100); }; + /** - * 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. + * Return the label + * @return {string} label */ - 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; - } - } - - 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; - } + Filter.prototype.getLabel = function () { + return this.graph.filterLabel; }; /** - * 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 + * Return the columnIndex of the filter + * @return {Number} columnIndex */ - TimeStep.prototype.getLabelMinor = function (date) { - if (date == undefined) { - date = this.current; - } + Filter.prototype.getColumn = function () { + return this.column; + }; - var format = this.format.minorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ""; + /** + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value + */ + Filter.prototype.getSelectedValue = function () { + if (this.index === undefined) return undefined; + + return this.values[this.index]; }; /** - * 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 + * Retrieve all values of the filter + * @return {Array} values */ - TimeStep.prototype.getLabelMajor = function (date) { - if (date == undefined) { - date = this.current; - } + Filter.prototype.getValues = function () { + return this.values; + }; - var format = this.format.majorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ""; + /** + * Retrieve one value of the filter + * @param {Number} index + * @return {*} value + */ + Filter.prototype.getValue = function (index) { + if (index >= this.values.length) throw "Error: index out of range"; + + return this.values[index]; }; - 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"; - } + /** + * 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; - 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 ""; - } + if (index === undefined) return []; - function currentWeek(date) { - return date.isSame(new Date(), "week") ? " current-week" : ""; - } + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; + } else { + var f = {}; + f.column = this.column; + f.value = this.values[index]; - function currentMonth(date) { - return date.isSame(new Date(), "month") ? " current-month" : ""; - } + var dataView = new DataView(this.data, { filter: function (item) { + return item[f.column] == f.value; + } }).get(); + dataPoints = this.graph._getDataPoints(dataView); - function currentYear(date) { - return date.isSame(new Date(), "year") ? " current-year" : ""; + this.dataPoints[index] = dataPoints; } - switch (this.scale) { - case "millisecond": - return even(date.milliseconds()).trim(); + return dataPoints; + }; - case "second": - return even(date.seconds()).trim(); - case "minute": - return even(date.minutes()).trim(); - case "hour": - var hours = date.hours(); - if (this.step == 4) { - hours = hours + "-h" + (hours + 4); - } - return "h" + hours + today(date) + even(date.hours()); + /** + * Set a callback function when the filter is fully loaded. + */ + Filter.prototype.setOnLoadCallback = function (callback) { + this.onLoadCallback = callback; + }; - case "weekday": - return date.format("dddd").toLowerCase() + today(date) + currentWeek(date) + even(date.date()); - case "day": - var day = date.date(); - var month = date.format("MMMM").toLowerCase(); - return "day" + day + " " + month + currentMonth(date) + even(day - 1); + /** + * 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"; - case "month": - return date.format("MMMM").toLowerCase() + currentMonth(date) + even(date.month()); + this.index = index; + this.value = this.values[index]; + }; - case "year": - var year = date.year(); - return "year" + year + currentYear(date) + even(year); + /** + * 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; - default: - return ""; + 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(); } }; - module.exports = TimeStep; + module.exports = Filter; /***/ }, -/* 20 */ +/* 16 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Hammer = __webpack_require__(45); var util = __webpack_require__(1); /** - * @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 + * @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 Item(data, conversion, options) { - this.id = null; - this.parent = null; - this.data = data; - this.dom = null; - this.conversion = conversion || {}; - this.options = options || {}; + 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; - this.selected = false; - this.displayed = false; - this.dirty = 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.top = null; - this.left = null; - this.width = null; - this.height = null; - } + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); - Item.prototype.stack = true; + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); - /** - * Select current item - */ - Item.prototype.select = function () { - this.selected = true; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); - /** - * Unselect current item - */ - Item.prototype.unselect = function () { - this.selected = false; - this.dirty = true; - if (this.displayed) this.redraw(); - }; + 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); + }; + } + + this.onChangeCallback = undefined; + + this.values = []; + this.index = undefined; - /** - * 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(); - }; + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } /** - * Set a parent for the item - * @param {ItemSet | Group} parent + * Select the previous index */ - Item.prototype.setParent = function (parent) { - if (this.displayed) { - this.hide(); - this.parent = parent; - if (this.parent) { - this.show(); - } - } else { - this.parent = parent; + Slider.prototype.prev = function () { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(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 + * Select the next index */ - Item.prototype.isVisible = function (range) { - // Should be implemented by Item implementations - return false; + Slider.prototype.next = function () { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); + } }; /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed + * Select the next index */ - Item.prototype.show = function () { - return false; - }; + Slider.prototype.playNext = function () { + var start = new Date(); - /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed - */ - Item.prototype.hide = function () { - return false; - }; + 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); + } - /** - * Repaint the item - */ - Item.prototype.redraw = function () {}; + var end = new Date(); + var diff = end - start; - /** - * Reposition the Item horizontally - */ - Item.prototype.repositionX = function () {}; + // 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); + }; /** - * Reposition the Item vertically + * Toggle start or stop playing */ - Item.prototype.repositionY = function () {}; + Slider.prototype.togglePlay = function () { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } + }; /** - * Repaint a delete button on the top right of the item when the item is selected - * @param {HTMLElement} anchor - * @protected + * Start playing */ - 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"; + Slider.prototype.play = function () { + // Test whether already playing + if (this.playTimeout) return; - // TODO: be able to destroy the delete button - new Hammer(deleteButton).on("tap", function (event) { - me.parent.removeFromDataSet(me); - event.stopPropagation(); - event.preventDefault(); - }); + this.playNext(); - anchor.appendChild(deleteButton); - this.dom.deleteButton = deleteButton; - } else if (!this.selected && this.dom.deleteButton) { - // remove button - if (this.dom.deleteButton.parentNode) { - this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); - } - this.dom.deleteButton = null; + if (this.frame) { + this.frame.play.value = "Stop"; } }; /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private + * Stop playing */ - Item.prototype._updateContents = function (element) { - var content; - if (this.options.template) { - var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset - content = this.options.template(itemData); - } else { - content = this.data.content; - } - - if (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); - } - } + Slider.prototype.stop = function () { + clearInterval(this.playTimeout); + this.playTimeout = undefined; - this.content = content; + if (this.frame) { + this.frame.play.value = "Play"; } }; /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private + * Set a callback function which will be triggered when the value of the + * slider bar has changed. */ - Item.prototype._updateTitle = function (element) { - if (this.data.title != null) { - element.title = this.data.title || ""; - } else { - element.removeAttribute("title"); - } + Slider.prototype.setOnChangeCallback = function (callback) { + this.onChangeCallback = callback; }; /** - * 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 + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds */ - Item.prototype._updateDataAttributes = function (element) { - if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { - var attributes = []; - - if (Array.isArray(this.options.dataAttributes)) { - attributes = this.options.dataAttributes; - } else if (this.options.dataAttributes == "all") { - attributes = Object.keys(this.data); - } else { - return; - } - - for (var i = 0; i < attributes.length; i++) { - var name = attributes[i]; - var value = this.data[name]; - - if (value != null) { - element.setAttribute("data-" + name, value); - } else { - element.removeAttribute("data-" + name); - } - } - } + Slider.prototype.setPlayInterval = function (interval) { + this.playInterval = interval; }; /** - * Update custom styles of the element - * @param element - * @private + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds */ - 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; - } + Slider.prototype.getPlayInterval = function (interval) { + return this.playInterval; }; - module.exports = Item; - // should be implemented by the item - // should be implemented by the item - // should be implemented by the item - -/***/ }, -/* 21 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var Hammer = __webpack_require__(45); - var Item = __webpack_require__(20); - var BackgroundGroup = __webpack_require__(31); - var RangeItem = __webpack_require__(24); - /** - * @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 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. */ - // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation - function BackgroundItem(data, conversion, options) { - this.props = { - content: { - width: 0 - } - }; - this.overflow = false; // if contents can overflow (css styling), this flag is set to true - - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data.id); - } - if (data.end == undefined) { - throw new Error("Property \"end\" missing in item " + data.id); - } - } - - Item.call(this, data, conversion, options); - - this.emptyContent = false; - } - - BackgroundItem.prototype = new Item(null, null, null); + Slider.prototype.setPlayLoop = function (doLoop) { + this.playLoop = doLoop; + }; - BackgroundItem.prototype.baseClassName = "item background"; - BackgroundItem.prototype.stack = false; /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible + * Execute the onchange callback function */ - BackgroundItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; + Slider.prototype.onChange = function () { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } }; /** - * Repaint the item + * redraw the slider on the correct place */ - BackgroundItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; - - // background box - dom.box = document.createElement("div"); - // className is updated in redraw() - - // contents box - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); - - // Note: we do NOT attach this item as attribute to the DOM, - // such that background items cannot be selected - //dom.box['timeline-item'] = this; - - this.dirty = true; - } + 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"; - // 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); + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = left + "px"; } - this.displayed = true; - - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.content); - this._updateDataAttributes(this.dom.content); - this._updateStyle(this.dom.box); - - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = this.baseClassName + className; + }; - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; - // recalculate size - this.props.content.width = this.dom.content.offsetWidth; - this.height = 0; // set height zero, so this item will be ignored when stacking items + /** + * 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; - this.dirty = false; - } + if (this.values.length > 0) this.setIndex(0);else this.index = undefined; }; /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * Select a value by its index + * @param {Number} index */ - BackgroundItem.prototype.show = RangeItem.prototype.show; + Slider.prototype.setIndex = function (index) { + if (index < this.values.length) { + this.index = index; - /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed - */ - BackgroundItem.prototype.hide = RangeItem.prototype.hide; + this.redraw(); + this.onChange(); + } else { + throw "Error: index out of range"; + } + }; /** - * Reposition the item horizontally - * @Override + * retrieve the index of the currently selected vaue + * @return {Number} index */ - BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; + Slider.prototype.getIndex = function () { + return this.index; + }; + /** - * Reposition the item vertically - * @Override + * retrieve the currently selected value + * @return {*} value */ - 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; - } - } - } - - // 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"; + Slider.prototype.get = function () { + return this.values[this.index]; }; - module.exports = BackgroundItem; -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { + 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; - "use strict"; + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); - var Item = __webpack_require__(20); - var util = __webpack_require__(1); + this.frame.style.cursor = "move"; - /** - * @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 - } + // 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); + }; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data); - } - } - Item.call(this, data, conversion, options); - } + Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + var x = left - 3; - BoxItem.prototype = new Item(null, null, null); + 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; - /** - * 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; + return index; }; - /** - * Repaint the item - */ - BoxItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - // create main box - dom.box = document.createElement("DIV"); + var x = index / (this.values.length - 1) * width; + var left = x + 3; - // contents box (inside the background box). used for making margins - dom.content = document.createElement("DIV"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); + return left; + }; - // line to axis - dom.line = document.createElement("DIV"); - dom.line.className = "line"; - // dot on axis - dom.dot = document.createElement("DIV"); - dom.dot.className = "dot"; - // attach this item as attribute - dom.box["timeline-item"] = this; + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; - this.dirty = true; - } + var index = this.leftToIndex(x); - // append DOM to parent DOM - if (!this.parent) { - throw new Error("Cannot redraw item: no parent attached"); - } - if (!dom.box.parentNode) { - var foreground = this.parent.dom.foreground; - if (!foreground) throw new Error("Cannot redraw item: parent has no foreground container element"); - foreground.appendChild(dom.box); - } - if (!dom.line.parentNode) { - var background = this.parent.dom.background; - if (!background) throw new Error("Cannot redraw item: parent has no background container element"); - background.appendChild(dom.line); - } - if (!dom.dot.parentNode) { - var axis = this.parent.dom.axis; - if (!background) throw new Error("Cannot redraw item: parent has no axis container element"); - axis.appendChild(dom.dot); - } - this.displayed = true; + this.setIndex(index); - // 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); + util.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; - // 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; + Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = "auto"; - this.dirty = false; - } + // remove event listeners + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); - this._repaintDeleteButton(dom.box); + util.preventDefault(); }; + module.exports = Slider; + +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + /** - * Show the item in the DOM (when not already displayed). The items DOM will - * be created when needed. + * @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, ...) */ - BoxItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); - } + 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); }; /** - * Hide the item from the DOM (when visible) + * 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, ...) */ - BoxItem.prototype.hide = function () { - if (this.displayed) { - var dom = this.dom; + StepNumber.prototype.setRange = function (start, end, step, prettyStep) { + this._start = start ? start : 0; + this._end = end ? end : 0; - 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.setStep(step, prettyStep); + }; - this.top = null; - this.left = null; + /** + * 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; - this.displayed = false; - } + if (prettyStep !== undefined) this.prettyStep = prettyStep; + + if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; }; /** - * Reposition the item horizontally - * @Override + * 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 */ - 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; + StepNumber.calculatePrettyStep = function (step) { + var log10 = function (x) { + return Math.log(x) / Math.LN10; + }; - // 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; - } + // 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))); - // reposition box - box.style.left = this.left + "px"; + // 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; - // reposition line - line.style.left = start - this.props.line.width / 2 + "px"; + // for safety + if (prettyStep <= 0) { + prettyStep = 1; + } - // reposition dot - dot.style.left = start - this.props.dot.width / 2 + "px"; + return prettyStep; }; /** - * Reposition the item vertically - * @Override + * returns the current value of the step + * @return {Number} current value */ - BoxItem.prototype.repositionY = function () { - var orientation = this.options.orientation; - var box = this.dom.box; - var line = this.dom.line; - var dot = this.dom.dot; + StepNumber.prototype.getCurrent = function () { + return parseFloat(this._current.toPrecision(this.precision)); + }; - if (orientation == "top") { - box.style.top = (this.top || 0) + "px"; + /** + * returns the current step size + * @return {Number} current step size + */ + StepNumber.prototype.getStep = function () { + return this._step; + }; - 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; + /** + * 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; + }; - box.style.top = (this.parent.height - this.top - this.height || 0) + "px"; - line.style.top = itemSetHeight - lineHeight + "px"; - line.style.bottom = "0"; - } + /** + * Do a step, add the step size to the current value + */ + StepNumber.prototype.next = function () { + this._current += this._step; + }; - dot.style.top = -this.props.dot.height / 2 + "px"; + /** + * 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 = BoxItem; + module.exports = StepNumber; /***/ }, -/* 23 */ +/* 18 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Item = __webpack_require__(20); + 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); /** - * @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 | 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 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); - } + function Timeline(container, items, groups, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError("Constructor must be called with the new operator"); } - Item.call(this, data, conversion, options); - } - - PointItem.prototype = new Item(null, null, null); - - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - PointItem.prototype.isVisible = function (range) { - // determine visibility - // TODO: account for the real width of the item. Right now we just add 1/4 to the window - var interval = (range.end - range.start) / 4; - return this.data.start > range.start - interval && this.data.start < range.end + interval; - }; + // 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; + } - /** - * Repaint the item - */ - PointItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + var me = this; + this.defaultOptions = { + start: null, + end: null, - // background box - dom.point = document.createElement("div"); - // className is updated in redraw() + autoResize: true, - // contents box, right from the dot - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.point.appendChild(dom.content); + orientation: "bottom", + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - // dot at start - dom.dot = document.createElement("div"); - dom.point.appendChild(dom.dot); + // Create the DOM, props, and emitter + this._create(container); - // attach this item as attribute - dom.point["timeline-item"] = this; + // all components listed here will be repainted automatically + this.components = []; - this.dirty = true; - } + 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; + }, - // 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"); + 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) } - 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; + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - // 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; + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + "px"; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - dom.dot.style.top = (this.height - this.props.dot.height) / 2 + "px"; - dom.dot.style.left = this.props.dot.width / 2 + "px"; + // custom time bar + // Note: time bar will be attached in this.setOptions when selected + this.customTime = new CustomTime(this.body); + this.components.push(this.customTime); - this.dirty = false; - } + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); - this._repaintDeleteButton(dom.point); - }; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - /** - * 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(); + // apply options + if (options) { + this.setOptions(options); } - }; - - /** - * Hide the item from the DOM (when visible) - */ - PointItem.prototype.hide = function () { - if (this.displayed) { - if (this.dom.point.parentNode) { - this.dom.point.parentNode.removeChild(this.dom.point); - } - this.top = null; - this.left = null; - - this.displayed = false; + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); } - }; - /** - * Reposition the item horizontally - * @Override - */ - PointItem.prototype.repositionX = function () { - var start = this.conversion.toScreen(this.data.start); + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } + } - this.left = start - this.props.dot.width; + // Extend the functionality from Core + Timeline.prototype = new Core(); - // reposition point - this.dom.point.style.left = this.left + "px"; + /** + * 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(); }; /** - * Reposition the item vertically - * @Override + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - PointItem.prototype.repositionY = function () { - var orientation = this.options.orientation, - point = this.dom.point; + Timeline.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - if (orientation == "top") { - point.style.top = this.top + "px"; + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; } else { - point.style.top = this.parent.height - this.top - this.height + "px"; + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: "Date", + end: "Date" + } + }); } - }; - module.exports = PointItem; + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); -/***/ }, -/* 24 */ -/***/ function(module, exports, __webpack_require__) { + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + if (this.options.start == undefined || this.options.end == undefined) { + var dataRange = this._getDataRange(); + } - "use strict"; + var start = this.options.start != undefined ? this.options.start : dataRange.start; + var end = this.options.end != undefined ? this.options.end : dataRange.end; - var Hammer = __webpack_require__(45); - var Item = __webpack_require__(20); + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); + } + } + }; /** - * @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 + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - 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); - } + 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); } - Item.call(this, data, conversion, options); - } + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); + }; - RangeItem.prototype = new Item(null, null, null); + /** + * 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); - RangeItem.prototype.baseClassName = "item range"; + if (options && options.focus) { + this.focus(ids, options); + } + }; /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ - RangeItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; + Timeline.prototype.getSelection = function () { + return this.itemSet && this.itemSet.getSelection() || []; }; /** - * Repaint the item + * 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 */ - RangeItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; - - // background box - dom.box = document.createElement("div"); - // className is updated in redraw() - - // contents box - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); - - // attach this item as attribute - dom.box["timeline-item"] = this; + Timeline.prototype.focus = function (id, options) { + if (!this.itemsData || id == undefined) return; - this.dirty = true; - } + var ids = Array.isArray(id) ? id : [id]; - // 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"); + // get the specified item(s) + var itemsData = this.itemsData.getDataSet().get(ids, { + type: { + start: "Date", + end: "Date" } - foreground.appendChild(dom.box); - } - this.displayed = true; + }); - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); + // 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(); - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = this.baseClassName + className; + if (start === null || s < start) { + start = s; + } - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; + if (end === null || e > end) { + end = e; + } + }); - // recalculate size - // turn off max-width to be able to calculate the real width - // this causes an extra browser repaint/reflow, but so be it - this.dom.content.style.maxWidth = "none"; - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; - this.dom.content.style.maxWidth = ""; + if (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); - this.dirty = false; + var animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(middle - interval / 2, middle + interval / 2, animate); } - - this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); }; /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * 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 */ - RangeItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); + 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 + }; }; - /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed - */ - RangeItem.prototype.hide = function () { - if (this.displayed) { - var box = this.dom.box; - if (box.parentNode) { - box.parentNode.removeChild(box); - } + module.exports = Timeline; - this.top = null; - this.left = null; +/***/ }, +/* 19 */ +/***/ function(module, exports, __webpack_require__) { - this.displayed = false; - } - }; + "use strict"; - /** - * Reposition the item horizontally - * @Override - */ - 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; + // 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."); + }; + } - // 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); +/***/ }, +/* 20 */ +/***/ function(module, exports, __webpack_require__) { - if (this.overflow) { - this.left = start; - this.width = boxWidth + this.props.content.width; - contentWidth = this.props.content.width; + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - // 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; + (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 { - this.left = start; - this.width = boxWidth; - contentWidth = Math.min(end - start - 2 * this.options.padding, this.props.content.width); + // Browser globals (root is window) + window.propagating = factory(); } + }(function () { + // will contain the target element where the gesture started + var _firstTarget = null; // singleton - this.dom.box.style.left = this.left + "px"; - this.dom.box.style.width = boxWidth + "px"; + /** + * 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; - switch (this.options.align) { - case "left": - this.dom.content.style.left = "0"; - break; + 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)); + }; - case "right": - this.dom.content.style.left = Math.max(boxWidth - contentWidth - 2 * this.options.padding, 0) + "px"; - break; + return PropagatingHammer; + } - case "center": - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + "px"; - break; + // attach to DOM element + var element = hammer.element; + element.hammer = hammer; - 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"; - } - }; + // move the original functions that we will wrap + hammer._on = hammer.on; + hammer._off = hammer.off; + hammer._emit = hammer.emit; + hammer._destroy = hammer.destroy; - /** - * Reposition the item vertically - * @Override - */ - RangeItem.prototype.repositionY = function () { - var orientation = this.options.orientation, - box = this.dom.box; + /** @type {Object.>} */ + hammer._handlers = {}; - if (orientation == "top") { - box.style.top = this.top + "px"; - } else { - box.style.top = this.parent.height - this.top - this.height + "px"; - } - }; + // 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; + } + }); - /** - * 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; + /** + * 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 = []; - //// TODO: this should be redundant? - //Hammer(dragLeft, { - // preventDefault: true - //}).on('drag', function () { - // //console.log('drag left') - // }); + // register the static, propagated handler + hammer._on(event, propagatedHandler); + } + _handlers.push(handler); + }); - 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; - } - }; + return hammer; + }; - /** - * 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; + /** + * 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]; + } + } + }); - //// TODO: this should be redundant? - //Hammer(dragRight, { - // preventDefault: true - //}).on('drag', function () { - // //console.log('drag right') - //}); + return hammer; + }; - 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; - } - }; + /** + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event + */ + hammer.emit = function(eventType, event) { + _firstTarget = event.target; + hammer._emit(eventType, event); + }; - module.exports = RangeItem; + hammer.destroy = function () { + // Detach from DOM element + var element = hammer.element; + delete element.hammer; -/***/ }, -/* 25 */ -/***/ function(module, exports, __webpack_require__) { + // clear all handlers + hammer._handlers = {}; - "use strict"; + // call original hammer destroy + hammer._destroy(); + }; - /** - * 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; - } + // split a string with space separated words + function split(events) { + return events.match(/[^ ]+/g); + } - /** - * 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); - } - }; + /** + * 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; + } + } - /** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ - Component.prototype.redraw = function () { - // should be implemented by the component - return false; - }; + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - /** - * Destroy the component. Cleanup DOM and event listeners - */ - Component.prototype.destroy = function () {}; + // attach firstTarget property to the event + event.firstTarget = _firstTarget; - /** - * Test whether the component is resized since the last time _isResized() was - * called. - * @return {Boolean} Returns true if the component is resized - * @protected - */ - Component.prototype._isResized = function () { - var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; + // 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); + } + } - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; + elem = elem.parentNode; + } + } - return resized; - }; + return hammer; + }; + })); - module.exports = Component; - // should be implemented by the component /***/ }, -/* 26 */ +/* 21 */ /***/ function(module, exports, __webpack_require__) { - "use strict"; - - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var moment = __webpack_require__(44); - var locales = __webpack_require__(48); - - /** - * A current time bar - * @param {{range: Range, dom: Object, domProps: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCurrentTime] - * @constructor CurrentTime - * @extends Component - */ - function CurrentTime(body, options) { - this.body = body; + var __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'; - // default options - this.defaultOptions = { - showCurrentTime: true, + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); - locales: locales, - locale: "en" - }; - this.options = util.extend({}, this.defaultOptions); - this.offset = 0; + var TYPE_FUNCTION = 'function'; - this._create(); + var round = Math.round; + var abs = Math.abs; + var now = Date.now; - this.setOptions(options); + /** + * 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); } - CurrentTime.prototype = new Component(); - /** - * Create the HTML DOM for the current time bar - * @private + * 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} */ - CurrentTime.prototype._create = function () { - var bar = document.createElement("div"); - bar.className = "currenttime"; - bar.style.position = "absolute"; - bar.style.top = "0px"; - bar.style.height = "100%"; - - this.bar = bar; - }; + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; + } + return false; + } /** - * Destroy the CurrentTime bar + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context */ - CurrentTime.prototype.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing + function each(obj, iterator, context) { + var i; - this.body = null; - }; + 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); + } + } + } /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] + * 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 */ - CurrentTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(["showCurrentTime", "locale", "locales"], this.options, options); - } - }; + 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; + } /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * 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 */ - 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(); - } + function merge(dest, src) { + return extend(dest, src, true); + } - var now = new Date(new Date().valueOf() + this.offset); - var x = this.body.util.toScreen(now); + /** + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] + */ + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - 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); + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - 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 (properties) { + extend(childP, properties); } - this.stop(); - } - - return false; - }; + } /** - * Start auto refreshing the current time bar + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} */ - CurrentTime.prototype.start = function () { - var me = this; - - function update() { - me.stop(); - - // determine interval to refresh - var scale = me.body.range.conversion(me.body.domProps.center.width).scale; - var interval = 1 / scale / 10; - if (interval < 30) interval = 30; - if (interval > 1000) interval = 1000; - - me.redraw(); - - // start a renderTimer to adjust for the new time - me.currentTimeTimer = setTimeout(update, interval); - } - - update(); - }; + function bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } /** - * Stop auto refreshing the current time bar + * 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} */ - CurrentTime.prototype.stop = function () { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; - } - }; + function boolOrFn(val, args) { + if (typeof val == TYPE_FUNCTION) { + return val.apply(args ? args[0] || undefined : undefined, args); + } + return val; + } /** - * 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. + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} */ - CurrentTime.prototype.setCurrentTime = function (time) { - var t = util.convert(time, "Date").valueOf(); - var now = new Date().valueOf(); - this.offset = t - now; - this.redraw(); - }; + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } /** - * Get the current time. - * @return {Date} Returns the current time. + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - CurrentTime.prototype.getCurrentTime = function () { - return new Date(new Date().valueOf() + this.offset); - }; - - module.exports = CurrentTime; - -/***/ }, -/* 27 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var Hammer = __webpack_require__(45); - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var moment = __webpack_require__(44); - var locales = __webpack_require__(48); + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } /** - * A custom time bar - * @param {{range: Range, dom: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCustomTime] - * @constructor CustomTime - * @extends Component + * removeEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - - 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); + function removeEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.removeEventListener(type, handler, false); + }); } - CustomTime.prototype = new Component(); - /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCustomTime] + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found */ - CustomTime.prototype.setOptions = function (options) { - if (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(); + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; + } + node = node.parentNode; } - } - }; + return false; + } /** - * Create the DOM for the custom time - * @private + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found */ - 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); + function inStr(str, find) { + return str.indexOf(find) > -1; + } - // 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(); - }); - }; + /** + * split string on whitespace + * @param {String} str + * @returns {Array} words + */ + function splitStr(str) { + return str.trim().split(/\s+/g); + } /** - * Destroy the CustomTime bar + * 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 */ - 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; - }; + 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; + } + } /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} */ - 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); - } + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } - var x = this.body.util.toScreen(this.customTime); + /** + * 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 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); + 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++; + } - 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 (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); + } } - } - return false; - }; + return results; + } /** - * Set custom time. - * @param {Date | number | string} time + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ - CustomTime.prototype.setCustomTime = function (time) { - this.customTime = util.convert(time, "Date"); - this.redraw(); - }; + 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; + } /** - * Retrieve the current custom time. - * @return {Date} customTime + * get a unique id + * @returns {number} uniqueId */ - CustomTime.prototype.getCustomTime = function () { - return new Date(this.customTime.valueOf()); - }; + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } /** - * Start moving horizontally - * @param {Event} event - * @private + * get the window object of an element + * @param {HTMLElement} element + * @returns {DocumentView|Window} */ - CustomTime.prototype._onDragStart = function (event) { - this.eventParams.dragging = true; - this.eventParams.customTime = this.customTime; + function getWindowForElement(element) { + var doc = element.ownerDocument; + return (doc.defaultView || doc.parentWindow); + } - event.stopPropagation(); - event.preventDefault(); - }; + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - /** - * Perform moving operating. - * @param {Event} event - * @private - */ - CustomTime.prototype._onDrag = function (event) { - if (!this.eventParams.dragging) return; + 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 x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; - var time = this.body.util.toTime(x); + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - this.setCustomTime(time); + var COMPUTE_INTERVAL = 25; - // fire a timechange event - this.body.emitter.emit("timechange", { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; - event.stopPropagation(); - event.preventDefault(); - }; + 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']; /** - * Stop moving operating. - * @param {Event} event - * @private + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} + * @constructor */ - CustomTime.prototype._onDragEnd = function (event) { - if (!this.eventParams.dragging) return; + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; - // fire a timechanged event - this.body.emitter.emit("timechanged", { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + // 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); + } + }; - event.stopPropagation(); - event.preventDefault(); - }; + this.init(); - module.exports = CustomTime; + } -/***/ }, -/* 28 */ -/***/ 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 util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var Component = __webpack_require__(25); - var DataStep = __webpack_require__(16); + /** + * 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); + } + }; /** - * A horizontal time axis - * @param {Object} [options] See DataAxis.setOptions for the available - * options. - * @constructor DataAxis - * @extends Component - * @param body + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} */ - function DataAxis(body, options, svg, linegraphOptions) { - this.id = util.randomUUID(); - this.body = body; + function createInputInstance(manager) { + var Type; + var inputClass = manager.options.inputClass; - 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 } + 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; } - }; - - this.linegraphOptions = linegraphOptions; - this.linegraphSVG = svg; - this.props = {}; - this.DOMelements = { // dynamic elements - lines: {}, - labels: {}, - title: {} - }; + return new (Type)(manager, inputHandler); + } - this.dom = {}; + /** + * 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)); - this.range = { start: 0, end: 0 }; + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; - this.options = util.extend({}, this.defaultOptions); - this.conversionFactor = 1; + if (isFirst) { + manager.session = {}; + } - this.setOptions(options); - this.width = Number(("" + this.options.width).replace("px", "")); - this.minWidth = this.width; - this.height = this.linegraphSVG.offsetHeight; - this.hidden = false; + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - this.stepPixels = 25; - this.stepPixelsForced = 25; - this.zeroCrossing = -1; + // compute scale, rotation etc + computeInputData(manager, input); - this.lineOffset = 0; - this.master = true; - this.svgElements = {}; - this.iconsRemoved = false; + // emit secret event + manager.emit('hammer.input', input); + manager.recognize(input); + manager.session.prevInput = input; + } - this.groups = {}; - this.amountOfGroups = 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; - // create the HTML DOM - this._create(); + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); + } - var me = this; - this.body.emitter.on("verticalDrag", function () { - me.dom.lineContainer.style.top = me.body.domProps.scrollTop + "px"; - }); - } + // 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; + } - DataAxis.prototype = new Component(); + 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; - DataAxis.prototype.addGroup = function (label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; - } - this.amountOfGroups += 1; - }; + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); - DataAxis.prototype.updateGroup = function (label, graphOptions) { - this.groups[label] = graphOptions; - }; + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - DataAxis.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; - } - }; + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; + computeIntervalInputData(session, input); - DataAxis.prototype.setOptions = function (options) { - if (options) { - var redraw = false; - if (this.options.orientation != options.orientation && options.orientation !== undefined) { - redraw = true; + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; } - var fields = ["orientation", "showMinorLabels", "showMajorLabels", "icons", "majorLinesOffset", "minorLinesOffset", "labelOffsetX", "labelOffsetY", "iconWidth", "width", "visible", "customRange", "title", "format", "alignZeros"]; - util.selectiveExtend(fields, this.options, options); + input.target = target; + } - this.minWidth = Number(("" + this.options.width).replace("px", "")); + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; - if (redraw == true && this.dom.frame) { - this.hide(); - this.show(); + 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); + } /** - * Create the HTML DOM for the DataAxis + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input */ - 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; + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - 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"; + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - // 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); - }; + 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++; + } - DataAxis.prototype._redrawGroupIcons = function () { - DOMutil.prepareElements(this.svgElements); + return { + timeStamp: now(), + pointers: pointers, + center: getCenter(pointers), + deltaX: input.deltaX, + deltaY: input.deltaY + }; + } - var x; - var iconWidth = this.options.iconWidth; - var iconHeight = 15; - var iconOffset = 4; - var y = iconOffset + 0.5 * iconHeight; + /** + * 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; - if (this.options.orientation == "left") { - x = iconOffset; - } else { - x = this.width - iconWidth - iconOffset; - } + // no need to loop when only one touch + if (pointersLength === 1) { + return { + x: round(pointers[0].clientX), + y: round(pointers[0].clientY) + }; + } - 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 x = 0, y = 0, i = 0; + while (i < pointersLength) { + x += pointers[i].clientX; + y += pointers[i].clientY; + i++; } - } - DOMutil.cleanupElements(this.svgElements); - this.iconsRemoved = false; - }; + return { + x: round(x / pointersLength), + y: round(y / pointersLength) + }; + } - DataAxis.prototype._cleanupIcons = function () { - if (this.iconsRemoved == false) { - DOMutil.prepareElements(this.svgElements); - DOMutil.cleanupElements(this.svgElements); - this.iconsRemoved = true; - } - }; + /** + * 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 + }; + } /** - * Create the HTML DOM for the DataAxis + * get the direction between two points + * @param {Number} x + * @param {Number} y + * @return {Number} direction */ - 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); + function getDirection(x, y) { + if (x === y) { + return DIRECTION_NONE; } - } - if (!this.dom.lineContainer.parentNode) { - this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); - } - }; + if (abs(x) >= abs(y)) { + return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; + } + return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; + } /** - * Create the HTML DOM for the DataAxis + * 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 */ - DataAxis.prototype.hide = function () { - this.hidden = true; - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + 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]]; - if (this.dom.lineContainer.parentNode) { - this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); - } - }; + return Math.sqrt((x * x) + (y * y)); + } /** - * Set a range (start and end) - * @param end - * @param start - * @param end + * calculate the angle between two coordinates + * @param {Object} p1 + * @param {Object} p2 + * @param {Array} [props] containing x and y keys + * @return {Number} angle */ - DataAxis.prototype.setRange = function (start, end) { - if (this.master == false && this.options.alignZeros == true && this.zeroCrossing != -1) { - if (start > 0) { - start = 0; + function getAngle(p1, p2, props) { + if (!props) { + props = PROPS_XY; } - } - this.range.start = start; - this.range.end = end; - }; + var x = p2[props[0]] - p1[props[0]], + y = p2[props[1]] - p1[props[1]]; + return Math.atan2(y, x) * 180 / Math.PI; + } /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * calculate the rotation degrees between two pointersets + * @param {Array} start array of pointers + * @param {Array} end array of pointers + * @return {Number} rotation */ - DataAxis.prototype.redraw = function () { - var resized = false; - var activeGroups = 0; + function getRotation(start, end) { + return getAngle(end[1], end[0], PROPS_CLIENT_XY) - getAngle(start[1], start[0], PROPS_CLIENT_XY); + } - // Make sure the line container adheres to the vertical scrolling. - this.dom.lineContainer.style.top = this.body.domProps.scrollTop + "px"; + /** + * 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); + } - 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", "")); + var MOUSE_INPUT_MAP = { + mousedown: INPUT_START, + mousemove: INPUT_MOVE, + mouseup: INPUT_END + }; - // 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 MOUSE_ELEMENT_EVENTS = 'mousedown'; + var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; - var props = this.props; - var frame = this.dom.frame; + /** + * Mouse events input + * @constructor + * @extends Input + */ + function MouseInput() { + this.evEl = MOUSE_ELEMENT_EVENTS; + this.evWin = MOUSE_WINDOW_EVENTS; - // update classname - frame.className = "dataaxis"; + this.allow = true; // used by Input.TouchMouse to disable mouse events + this.pressed = false; // mousedown state - // calculate character width and height - this._calculateCharSize(); + Input.apply(this, arguments); + } - var orientation = this.options.orientation; - var showMinorLabels = this.options.showMinorLabels; - var showMajorLabels = this.options.showMajorLabels; + inherit(MouseInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function MEhandler(ev) { + var eventType = MOUSE_INPUT_MAP[ev.type]; - // determine the width and height of the elements for the axis - props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; - props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + // on start we want to have the left mouse button down + if (eventType & INPUT_START && ev.button === 0) { + this.pressed = 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 (eventType & INPUT_MOVE && ev.which !== 1) { + eventType = INPUT_END; + } - // 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; - } + // mouse must be down, and mouse events are allowed (see the TouchMouse input) + if (!this.pressed || !this.allow) { + return; + } - resized = this._redrawLabels(); - resized = this._isResized() || resized; + if (eventType & INPUT_END) { + this.pressed = false; + } - if (this.options.icons == true) { - this._redrawGroupIcons(); - } else { - this._cleanupIcons(); + this.callback(this.manager, eventType, { + pointers: [ev], + changedPointers: [ev], + pointerType: INPUT_TYPE_MOUSE, + srcEvent: ev + }); } + }); - this._redrawTitle(orientation); - } - return resized; + var POINTER_INPUT_MAP = { + pointerdown: INPUT_START, + pointermove: INPUT_MOVE, + pointerup: INPUT_END, + pointercancel: INPUT_CANCEL, + pointerout: INPUT_CANCEL }; - /** - * Repaint major and minor text labels and vertical grid lines - * @private - */ - DataAxis.prototype._redrawLabels = function () { - var resized = false; - DOMutil.prepareElements(this.DOMelements.lines); - DOMutil.prepareElements(this.DOMelements.labels); - - var orientation = this.options.orientation; - - // 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; + // 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 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 - ); + var POINTER_ELEMENT_EVENTS = 'pointerdown'; + var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - 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); + // IE10 has prefixed support, and case-sensitive + if (window.MSPointerEvent) { + POINTER_ELEMENT_EVENTS = 'MSPointerDown'; + POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; + } - this.stepPixels = stepPixels; + /** + * Pointer events input + * @constructor + * @extends Input + */ + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - var amountOfSteps = this.height / stepPixels; - var stepDifference = 0; + Input.apply(this, arguments); - // 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; + this.store = (this.manager.session.pointerEvents = []); + } - 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 (zeroStepDifference < 0) { - for (var i = 0; i < -zeroStepDifference; i++) { - step.previous(); - } - } - } - } else { - amountOfSteps += 0.25; - } + 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; - this.valueAtZero = step.marginEnd; - var marginStartPos = 0; + var isTouch = (pointerType == INPUT_TYPE_TOUCH); - // do not draw the first label - var max = 1; + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - // Get the number of decimal places - var decimals; - if (this.options.format[orientation] !== undefined) { - decimals = this.options.format[orientation].decimals; - } + // 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; + } - 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(); + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; + } - 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); - } + // update the event in the store + store[storeIndex] = ev; - 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); - } + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); - if (this.master == true && step.current == 0) { - this.zeroCrossing = max; + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); + } } + }); - max++; - } + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL + }; - if (this.master == false) { - this.conversionFactor = y / (this.valueAtZero - step.current); - } else { - this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; - } + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; - // 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; + /** + * Touch events input + * @constructor + * @extends Input + */ + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; - // 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; - } + Input.apply(this, arguments); + } - return resized; - }; + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - DataAxis.prototype.convertValue = function (value) { - var invertedValue = this.valueAtZero - value; - var convertedValue = invertedValue * this.conversionFactor; - return convertedValue; - }; + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; + } - /** - * 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.started) { + return; + } - label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + "px"; + var touches = normalizeSingleTouches.call(this, ev, type); - text += ""; + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; + } - var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); - if (this.maxLabelSize < text.length * largestWidth) { - this.maxLabelSize = text.length * largestWidth; - } - }; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); /** - * Create a minor line for the axis at position y - * @param y - * @param orientation - * @param className - * @param offset - * @param width + * @this {TouchInput} + * @param {Object} ev + * @param {Number} type flag + * @returns {undefined|Array} [all, changed] */ - 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 = ""; + function normalizeSingleTouches(ev, type) { + var all = toArray(ev.touches); + var changed = toArray(ev.changedTouches); - if (orientation == "left") { - line.style.left = this.width - offset + "px"; - } else { - line.style.right = this.width - offset + "px"; + if (type & (INPUT_END | INPUT_CANCEL)) { + all = uniqueArray(all.concat(changed), 'identifier', true); } - line.style.width = width + "px"; - line.style.top = y + "px"; - } + 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'; + /** - * Create a title for the axis - * @private - * @param orientation + * Multi-user touch events input + * @constructor + * @extends Input */ - DataAxis.prototype._redrawTitle = function (orientation) { - DOMutil.prepareElements(this.DOMelements.title); + function TouchInput() { + this.evTarget = TOUCH_TARGET_EVENTS; + this.targetIds = {}; - // 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; + Input.apply(this, arguments); + } - // Add style - if provided - if (this.options.title[orientation].style !== undefined) { - util.addCssText(title, this.options.title[orientation].style); - } + inherit(TouchInput, Input, { + handler: function MTEhandler(ev) { + var type = TOUCH_INPUT_MAP[ev.type]; + var touches = getTouches.call(this, ev, type); + if (!touches) { + return; + } - if (orientation == "left") { - title.style.left = this.props.titleCharHeight + "px"; - } else { - title.style.right = this.props.titleCharHeight + "px"; + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); } + }); - title.style.width = this.height + "px"; - } + /** + * @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; - // we need to clean up in case we did not use all elements. - DOMutil.cleanupElements(this.DOMelements.title); - }; + // 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++; + } + } - /** - * 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); + // 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]); + } - this.props.minorCharHeight = measureCharMinor.clientHeight; - this.props.minorCharWidth = measureCharMinor.clientWidth; + // cleanup removed touches + if (type & (INPUT_END | INPUT_CANCEL)) { + delete targetIds[changedTouches[i].identifier]; + } + i++; + } - this.dom.frame.removeChild(measureCharMinor); - } + if (!changedTargetTouches.length) { + return; + } - 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); + return [ + // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel' + uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true), + changedTargetTouches + ]; + } - this.props.majorCharHeight = measureCharMajor.clientHeight; - this.props.majorCharWidth = measureCharMajor.clientWidth; + /** + * 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.dom.frame.removeChild(measureCharMajor); - } + var handler = bindFn(this.handler, this); + this.touch = new TouchInput(this.manager, handler); + this.mouse = new MouseInput(this.manager, handler); + } - 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); + 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.props.titleCharHeight = measureCharTitle.clientHeight; - this.props.titleCharWidth = measureCharTitle.clientWidth; + // 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; + } - this.dom.frame.removeChild(measureCharTitle); - } - }; + // reset the allowMouse when we're done + if (inputEvent & (INPUT_END | INPUT_CANCEL)) { + this.mouse.allow = true; + } - module.exports = DataAxis; + this.callback(manager, inputEvent, inputData); + }, -/***/ }, -/* 29 */ -/***/ function(module, exports, __webpack_require__) { + /** + * remove the event listeners + */ + destroy: function destroy() { + this.touch.destroy(); + this.mouse.destroy(); + } + }); - "use strict"; + var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); + var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var Line = __webpack_require__(49); - var Bar = __webpack_require__(50); - var Points = __webpack_require__(51); + // 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'; /** - * /** - * @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 + * Touch Action + * sets the touchAction property or uses the js alternative + * @param {Manager} manager + * @param {String} value * @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; + 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(); + } + }; /** - * this loads a reference to all items in this group into this group. - * @param {array} items + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} */ - 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; - }); + 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; } - } else { - this.itemsData = []; - } - }; + return TOUCH_ACTION_AUTO; + } /** - * this is used for plotting barcharts, this way, we only have to calculate it once. - * @param pos + * 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 */ - GraphGroup.prototype.setZeroPosition = function (pos) { - this.zeroPosition = pos; - }; - + 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; /** - * set the options of the graph group over the default options. - * @param options + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options */ - GraphGroup.prototype.setOptions = function (options) { - if (options !== undefined) { - var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart"]; - util.selectiveDeepExtend(fields, this.options, options); + function Recognizer(options) { + this.id = uniqueId(); - util.mergeOptions(this.options, options, "catmullRom"); - util.mergeOptions(this.options, options, "drawPoints"); - util.mergeOptions(this.options, options, "shaded"); + 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; + } - 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; - } + 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; } - } - } - } - 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); - } - }; + 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; + } - /** - * 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); - }; + 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; + }, - /** - * 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; + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, - 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"); + /** + * 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; - 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); - } + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); + } - 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"); - } + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); + } - 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); + emit(); // simple 'eventName' events - var offset = Math.round((iconWidth - 2 * barWidth) / 3); + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); + } + }, - 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); - } - }; + /** + * 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; + }, - /** - * 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 }; - }; + /** + * 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); - GraphGroup.prototype.getYRange = function (groupData) { - return this.type.getYRange(groupData); - }; + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; + } - GraphGroup.prototype.draw = function (dataset, group, framework) { - this.type.draw(dataset, group, framework); - }; + // 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); - module.exports = GraphGroup; + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); + } + }, -/***/ }, -/* 30 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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 - "use strict"; + /** + * return the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, - var util = __webpack_require__(1); - var stack = __webpack_require__(18); - var RangeItem = __webpack_require__(24); + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } + }; /** - * @constructor Group - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state */ - 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 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'; } - }; - 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); + return ''; } /** - * Create DOM elements for the group - * @private + * direction cons to string + * @param {Const} direction + * @returns {String} */ - 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); - }; + 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 ''; + } /** - * Set the group data for this group - * @param {Object} data Group data, can contain properties content and className + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} */ - Group.prototype.setData = function (data) { - // update contents - var content = data && data.content; - if (content instanceof Element) { - this.dom.inner.appendChild(content); - } else if (content !== undefined && content !== null) { - this.dom.inner.innerHTML = content; - } else { - this.dom.inner.innerHTML = this.groupId || ""; // groupId can be null - } - - // update title - this.dom.label.title = data && data.title || ""; - - if (!this.dom.inner.firstChild) { - util.addClassName(this.dom.inner, "hidden"); - } else { - util.removeClassName(this.dom.inner, "hidden"); - } - - // update className - var className = data && data.className || null; - if (className != this.className) { - if (this.className) { - util.removeClassName(this.dom.label, this.className); - util.removeClassName(this.dom.foreground, this.className); - util.removeClassName(this.dom.background, this.className); - util.removeClassName(this.dom.axis, this.className); + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); } - util.addClassName(this.dom.label, className); - util.addClassName(this.dom.foreground, className); - util.addClassName(this.dom.background, className); - util.addClassName(this.dom.axis, className); - this.className = className; - } - - // update style - if (this.style) { - util.removeCssText(this.dom.label, this.style); - this.style = null; - } - if (data && data.style) { - util.addCssText(this.dom.label, data.style); - this.style = data.style; - } - }; + return otherRecognizer; + } /** - * Get the width of the group label - * @return {number} width + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer */ - Group.prototype.getLabelWidth = function () { - return this.props.label.width; - }; + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, - /** - * 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; + /** + * 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; + }, - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + /** + * 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; - // 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 isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); + // 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; + } + }); - restack = true; - } + /** + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - // 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.pX = null; + this.pY = null; + } + + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - // recalculate the height of the group - var height = this._calculateHeight(margin); + 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; + }, - // 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; + 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; - // 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; + // 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; + }, - // apply new height - this.dom.background.style.height = height + "px"; - this.dom.foreground.style.height = height + "px"; - this.dom.label.style.height = height + "px"; + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - // 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); - } + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; - return resized; - }; + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - /** - * recalculate the height of the group - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @returns {number} Returns the height - * @private - */ - Group.prototype._calculateHeight = function (margin) { - // recalculate the height of the group - var height; - var visibleItems = this.visibleItems; - //var visibleSubgroups = []; - //this.visibleSubgroups = 0; - this.resetSubgroups(); - var me = this; - if (visibleItems.length) { - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, item.top + item.height); - if (item.data.subgroup !== undefined) { - me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); - me.subgroups[item.data.subgroup].visible = true; - //if (visibleSubgroups.indexOf(item.data.subgroup) == -1){ - // visibleSubgroups.push(item.data.subgroup); - // me.visibleSubgroups += 1; - //} - } - }); - if (min > margin.axis) { - // there is an empty gap between the lowest item and the axis - var offset = min - margin.axis; - max -= offset; - util.forEach(visibleItems, function (item) { - item.top -= offset; - }); + this._super.emit.call(this, input); } - height = max + margin.item.vertical / 2; - } else { - height = margin.axis + margin.item.vertical; - } - height = Math.max(height, this.props.label.height); - - return height; - }; + }); /** - * Show this group: attach to the DOM + * Pinch + * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). + * @constructor + * @extends AttrRecognizer */ - Group.prototype.show = function () { - if (!this.dom.label.parentNode) { - this.itemSet.dom.labelSet.appendChild(this.dom.label); - } + function PinchRecognizer() { + AttrRecognizer.apply(this, arguments); + } - if (!this.dom.foreground.parentNode) { - this.itemSet.dom.foreground.appendChild(this.dom.foreground); - } + inherit(PinchRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PinchRecognizer + */ + defaults: { + event: 'pinch', + threshold: 0, + pointers: 2 + }, - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, - if (!this.dom.axis.parentNode) { - this.itemSet.dom.axis.appendChild(this.dom.axis); - } - }; + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); + }, + + 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); + } + } + }); /** - * Hide this group: remove from the DOM + * Press + * Recognized when the pointer is down for x ms without any movement. + * @constructor + * @extends Recognizer */ - Group.prototype.hide = function () { - var label = this.dom.label; - if (label.parentNode) { - label.parentNode.removeChild(label); - } + function PressRecognizer() { + Recognizer.apply(this, arguments); - var foreground = this.dom.foreground; - if (foreground.parentNode) { - foreground.parentNode.removeChild(foreground); - } + this._timer = null; + this._input = null; + } - var background = this.dom.background; - if (background.parentNode) { - background.parentNode.removeChild(background); - } + 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 + }, - var axis = this.dom.axis; - if (axis.parentNode) { - axis.parentNode.removeChild(axis); - } - }; + getTouchAction: function() { + return [TOUCH_ACTION_AUTO]; + }, - /** - * Add an item to the group - * @param {Item} item - */ - Group.prototype.add = function (item) { - this.items[item.id] = item; - item.setParent(this); + process: function(input) { + var options = this.options; + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTime = input.deltaTime > options.time; - // add 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(); + this._input = input; - 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); - } - }; + // 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; + }, - 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); - } + reset: function() { + clearTimeout(this._timer); + }, - if (sortArray.length > 0) { - for (var i = 0; i < sortArray.length; i++) { - this.subgroups[sortArray[i].subgroup].index = i; - } - } - } - }; + emit: function(input) { + if (this.state !== STATE_RECOGNIZED) { + return; + } - Group.prototype.resetSubgroups = function () { - for (var subgroup in this.subgroups) { - if (this.subgroups.hasOwnProperty(subgroup)) { - this.subgroups[subgroup].visible = false; + 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); + } } - } - }; + }); /** - * Remove an item from the group - * @param {Item} item + * Rotate + * Recognized when two or more pointer are moving in a circular motion. + * @constructor + * @extends AttrRecognizer */ - Group.prototype.remove = function (item) { - delete this.items[item.id]; - item.setParent(null); + function RotateRecognizer() { + AttrRecognizer.apply(this, arguments); + } - // remove from visible items - var index = this.visibleItems.indexOf(item); - if (index != -1) this.visibleItems.splice(index, 1); + inherit(RotateRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof RotateRecognizer + */ + defaults: { + event: 'rotate', + threshold: 0, + pointers: 2 + }, - // TODO: also remove from ordered items? - }; + getTouchAction: function() { + return [TOUCH_ACTION_NONE]; + }, + attrTest: function(input) { + return this._super.attrTest.call(this, input) && + (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + } + }); /** - * Remove an item from the corresponding DataSet - * @param {Item} item + * Swipe + * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction. + * @constructor + * @extends AttrRecognizer */ - Group.prototype.removeFromDataSet = function (item) { - this.itemSet.removeItem(item.id); - }; - + function SwipeRecognizer() { + AttrRecognizer.apply(this, arguments); + } - /** - * Reorder the items - */ - Group.prototype.order = function () { - var array = util.toArray(this.items); - var startArray = []; - var endArray = []; + inherit(SwipeRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof SwipeRecognizer + */ + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - 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 - }; + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - stack.orderByStart(this.orderedItems.byStart); - stack.orderByEnd(this.orderedItems.byEnd); - }; + 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; + } - /** - * Update the visible items - * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date - * @param {Item[]} visibleItems The previously visible items. - * @param {{start: number, end: number}} range Visible range - * @return {Item[]} visibleItems The new visible items. - * @private - */ - Group.prototype._updateVisibleItems = function (orderedItems, oldVisibleItems, range) { - var visibleItems = []; - var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems - var interval = (range.end - range.start) / 4; - var lowerBound = range.start - interval; - var upperBound = range.end + interval; - var item, i; + return this._super.attrTest.call(this, input) && + direction & input.direction && + input.distance > this.options.threshold && + abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + }, - // 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; - } - }; + emit: function(input) { + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } - // 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); + this.manager.emit(this.options.event, input); } - } - - // we do a binary search for the items that have only start values. - var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, "data", "start"); - - // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. - this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { - return item.data.start < lowerBound || item.data.start > upperBound; - }); + }); - // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. - // We therefore have to brute force check all items in the byEnd list - if (this.checkRangedItems == true) { - this.checkRangedItems = false; - for (i = 0; i < orderedItems.byEnd.length; i++) { - this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); - } - } else { - // we do a binary search for the items that have defined end times. - var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, "data", "end"); + /** + * 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); - // 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; - }); - } + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; + this._timer = null; + this._input = null; + this.count = 0; + } - // 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(); - } + 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 + }, - // 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" : "") - // } - //} + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - return visibleItems; - }; + process: function(input) { + var options = this.options; - Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { - var item; - var i; + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; - 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); - } - } - } + this.reset(); - 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); + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); } - } - } - } - }; + // we only allow little movement + // and we've reached an end event, so a tap is possible + if (validMovement && validTouchTime && validPointers) { + if (input.eventType != INPUT_END) { + return this.failTimeout(); + } - /** - * this function is very similar to the _checkIfInvisible() but it does not - * return booleans, hides the item if it should not be seen and always adds to - * the visibleItems. - * this one is for brute forcing and hiding. - * - * @param {Item} item - * @param {Array} visibleItems - * @param {{start:number, end:number}} range - * @private - */ - Group.prototype._checkIfVisible = function (item, visibleItems, range) { - if (item.isVisible(range)) { - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); - visibleItems.push(item); - } else { - if (item.displayed) item.hide(); - } - }; - + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - /** - * this function is very similar to the _checkIfInvisible() but it does not - * return booleans, hides the item if it should not be seen and always adds to - * the visibleItems. - * this one is for brute forcing and hiding. - * - * @param {Item} item - * @param {Array} visibleItems - * @param {{start:number, end:number}} range - * @private - */ - Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { - if (item.isVisible(range)) { - if (visibleItemsLookup[item.id] === undefined) { - visibleItemsLookup[item.id] = true; - visibleItems.push(item); - } - } else { - if (item.displayed) item.hide(); - } - }; + this.pTime = input.timeStamp; + this.pCenter = input.center; + if (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; + } + this._input = input; - module.exports = Group; + // 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; + }, -/***/ }, -/* 31 */ -/***/ 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 util = __webpack_require__(1); - var Group = __webpack_require__(30); + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); + } + } + }); /** - * @constructor BackgroundGroup - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor */ - function BackgroundGroup(groupId, data, itemSet) { - Group.call(this, groupId, data, itemSet); - - this.width = 0; - this.height = 0; - this.top = 0; - this.left = 0; + function Hammer(element, options) { + options = options || {}; + options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); + return new Manager(element, options); } - BackgroundGroup.prototype = Object.create(Group.prototype); + /** + * @const {string} + */ + Hammer.VERSION = '2.0.4'; /** - * 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 + * default settings + * @namespace */ - BackgroundGroup.prototype.redraw = function (range, margin, restack) { - var resized = false; + 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.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + /** + * 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, - // calculate actual size - this.width = this.dom.background.offsetWidth; + /** + * @type {Boolean} + * @default true + */ + enable: true, - // apply new height (just always zero for BackgroundGroup - this.dom.background.style.height = "0"; + /** + * 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, - // 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); - } + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, - return resized; - }; + /** + * 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] + ], - /** - * Show this group: attach to the DOM - */ - BackgroundGroup.prototype.show = function () { - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } - }; + /** + * 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', - module.exports = BackgroundGroup; + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', -/***/ }, -/* 32 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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', - "use strict"; + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', - var Hammer = __webpack_require__(45); - var util = __webpack_require__(1); - 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__(31); - var BoxItem = __webpack_require__(22); - var PointItem = __webpack_require__(23); - var RangeItem = __webpack_require__(24); - var BackgroundItem = __webpack_require__(21); + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in iOS. This property obeys the alpha value, if specified. + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' + } + }; - var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items - var BACKGROUND = "__background__"; // reserved group id for background items without group + var STOP = 1; + var FORCED_STOP = 2; /** - * An ItemSet holds a set of items and ranges which can be displayed in a - * range. The width is determined by the parent of the ItemSet, and the height - * is determined by the size of the items. - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body - * @param {Object} [options] See ItemSet.setOptions for the available options. - * @constructor ItemSet - * @extends Component + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor */ - function ItemSet(body, options) { - this.body = body; - - this.defaultOptions = { - type: null, // 'box', 'point', 'range', 'background' - orientation: "bottom", // 'top' or 'bottom' - align: "auto", // alignment of box items - stack: true, - groupOrder: null, - - selectable: true, - editable: { - updateTime: false, - updateGroup: false, - add: false, - remove: false - }, - - snap: TimeStep.snap, + function Manager(element, options) { + options = options || {}; - onAdd: function (item, callback) { - callback(item); - }, - onUpdate: function (item, callback) { - callback(item); - }, - onMove: function (item, callback) { - callback(item); - }, - onRemove: function (item, callback) { - callback(item); - }, - onMoving: function (item, callback) { - callback(item); - }, + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - margin: { - item: { - horizontal: 10, - vertical: 10 - }, - axis: 20 - }, - padding: 5 - }; + this.handlers = {}; + this.session = {}; + this.recognizers = []; - // options is shared by this ItemSet and all its items - this.options = util.extend({}, this.defaultOptions); + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); - // options for getting items from the DataSet with the correct type - this.itemOptions = { - type: { start: "Date", end: "Date" } - }; + toggleCssProps(this, true); - this.conversion = { - toScreen: body.util.toScreen, - toTime: body.util.toTime - }; - this.dom = {}; - this.props = {}; - this.hammer = null; + 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 me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + Manager.prototype = { + /** + * set options + * @param {Object} options + * @returns {Manager} + */ + set: function(options) { + extend(this.options, options); - // 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); + // 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; }, - remove: function (event, params, senderId) { - me._onRemove(params.items); - } - }; - // listeners for the DataSet of the groups - this.groupListeners = { - add: function (event, params, senderId) { - me._onAddGroups(params.items); - }, - update: function (event, params, senderId) { - me._onUpdateGroups(params.items); + /** + * 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; }, - remove: function (event, params, senderId) { - me._onRemoveGroups(params.items); - } - }; - - this.items = {}; // object with an Item for every data item - this.groups = {}; // Group object for every group - this.groupIds = []; - - this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw - this.touchParams = {}; // stores properties while dragging - // create the HTML DOM + /** + * 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; + } - this._create(); + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); - this.setOptions(options); - } + var recognizer; + var recognizers = this.recognizers; - ItemSet.prototype = new Component(); + // 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; - // available item types will be registered here - ItemSet.types = { - background: BackgroundItem, - box: BoxItem, - range: RangeItem, - point: PointItem - }; + // 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; + } - /** - * Create the HTML DOM for the ItemSet - */ - ItemSet.prototype._create = function () { - var frame = document.createElement("div"); - frame.className = "itemset"; - frame["timeline-itemset"] = this; - this.dom.frame = frame; + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; - // create background panel - var background = document.createElement("div"); - background.className = "background"; - frame.appendChild(background); - this.dom.background = background; + // 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(); + } - // create foreground panel - var foreground = document.createElement("div"); - foreground.className = "foreground"; - frame.appendChild(foreground); - this.dom.foreground = foreground; + // 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++; + } + }, - // create axis panel - var axis = document.createElement("div"); - axis.className = "axis"; - this.dom.axis = axis; + /** + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} + */ + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; + } - // create labelset - var labelSet = document.createElement("div"); - labelSet.className = "labelset"; - this.dom.labelSet = labelSet; + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } + } + return null; + }, - // create ungrouped Group - this._updateUngrouped(); + /** + * 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; + } - // create background Group - var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); - backgroundGroup.show(); - this.groups[BACKGROUND] = backgroundGroup; + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); + } - // attach event listeners - // Note: we bind to the centerContainer for the case where the height - // of the center container is larger than of the ItemSet, so we - // can click in the empty area to create a new item or deselect an item. - this.hammer = new Hammer(this.body.dom.centerContainer); + this.recognizers.push(recognizer); + recognizer.manager = this; - // drag items when selected - this.hammer.on("hammer.input", (function (event) { - if (event.isFirst) { - this._onTouch(event); - } - }).bind(this)); - this.hammer.on("panstart", this._onDragStart.bind(this)); - this.hammer.on("panmove", this._onDrag.bind(this)); - this.hammer.on("panend", this._onDragEnd.bind(this)); + this.touchAction.update(); + return recognizer; + }, - // single select (or unselect) when tapping an item - this.hammer.on("tap", this._onSelectItem.bind(this)); + /** + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} + */ + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on("press", this._onMultiSelectItem.bind(this)); + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - // add item on doubletap - this.hammer.on("doubletap", this._onAddItem.bind(this)); + this.touchAction.update(); + return this; + }, - // attach to the DOM - this.show(); - }; + /** + * 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; + }, - /** - * Set options for the ItemSet. Existing options will be extended/overwritten. - * @param {Object} [options] The following options are available: - * {String} type - * Default type for the items. Choose from 'box' - * (default), 'point', 'range', or 'background'. - * The default style can be overwritten by - * individual items. - * {String} align - * Alignment for the items, only applicable for - * BoxItem. Choose 'center' (default), 'left', or - * 'right'. - * {String} orientation - * Orientation of the item set. Choose 'top' or - * 'bottom' (default). - * {Function} groupOrder - * A sorting function for ordering groups - * {Boolean} stack - * If true (deafult), items will be stacked on - * top of each other. - * {Number} margin.axis - * Margin between the axis and the items in pixels. - * Default is 20. - * {Number} margin.item.horizontal - * Horizontal margin between items in pixels. - * Default is 10. - * {Number} margin.item.vertical - * Vertical Margin between items in pixels. - * Default is 10. - * {Number} margin.item - * Margin between items in pixels in both horizontal - * and vertical direction. Default is 10. - * {Number} margin - * Set margin for both axis and items in pixels. - * {Number} padding - * Padding of the contents of an item in pixels. - * Must correspond with the items css. Default is 5. - * {Boolean} selectable - * If true (default), items can be selected. - * {Boolean} editable - * Set all editable options to true or false - * {Boolean} editable.updateTime - * Allow dragging an item to an other moment in time - * {Boolean} editable.updateGroup - * Allow dragging an item to an other group - * {Boolean} editable.add - * Allow creating new items on double tap - * {Boolean} editable.remove - * Allow removing items by clicking the delete button - * top right of a selected item. - * {Function(item: Item, callback: Function)} onAdd - * Callback function triggered when an item is about to be added: - * when the user double taps an empty space in the Timeline. - * {Function(item: Item, callback: Function)} onUpdate - * Callback function fired when an item is about to be updated. - * This function typically has to show a dialog where the user - * change the item. If not implemented, nothing happens. - * {Function(item: Item, callback: Function)} onMove - * Fired when an item has been moved. If not implemented, - * the move action will be accepted. - * {Function(item: Item, callback: Function)} onRemove - * Fired when an item is about to be deleted. - * If not implemented, the item will be always removed. - */ - ItemSet.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - var fields = ["type", "align", "orientation", "padding", "stack", "selectable", "groupOrder", "dataAttributes", "template", "hide", "snap"]; - util.selectiveExtend(fields, this.options, options); + /** + * 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; + }, - if ("margin" in options) { - if (typeof options.margin === "number") { - this.options.margin.axis = options.margin; - this.options.margin.item.horizontal = options.margin; - this.options.margin.item.vertical = options.margin; - } else if (typeof options.margin === "object") { - util.selectiveExtend(["axis"], this.options.margin, options.margin); - if ("item" in options.margin) { - if (typeof options.margin.item === "number") { - this.options.margin.item.horizontal = options.margin.item; - this.options.margin.item.vertical = options.margin.item; - } else if (typeof options.margin.item === "object") { - util.selectiveExtend(["horizontal", "vertical"], this.options.margin.item, options.margin.item); - } + /** + * 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); } - } - } - - if ("editable" in options) { - if (typeof options.editable === "boolean") { - this.options.editable.updateTime = options.editable; - this.options.editable.updateGroup = options.editable; - this.options.editable.add = options.editable; - this.options.editable.remove = options.editable; - } else if (typeof options.editable === "object") { - util.selectiveExtend(["updateTime", "updateGroup", "add", "remove"], this.options.editable, options.editable); - } - } - // callback functions - var addCallback = (function (name) { - var fn = options[name]; - if (fn) { - if (!(fn instanceof Function)) { - throw new Error("option " + name + " must be a function " + name + "(item, callback)"); + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; } - this.options[name] = fn; - } - }).bind(this); - ["onAdd", "onUpdate", "onRemove", "onMove", "onMoving"].forEach(addCallback); - // force the itemSet to refresh: options like orientation and margins may be changed - this.markDirty(); - } - }; + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; - /** - * Mark the ItemSet dirty so it will refresh everything with next redraw. - * Optionally, all items can be marked as dirty and be refreshed. - * @param {{refreshItems: boolean}} [options] - */ - ItemSet.prototype.markDirty = function (options) { - this.groupIds = []; - this.stackDirty = true; + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; + } + }, - if (options && options.refreshItems) { - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); - } + /** + * 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; + } }; /** - * Destroy the ItemSet + * add/remove the css properties as defined in manager.options.cssProps + * @param {Manager} manager + * @param {Boolean} add */ - ItemSet.prototype.destroy = function () { - this.hide(); - this.setItems(null); - this.setGroups(null); - - this.hammer = null; - - this.body = null; - this.conversion = null; - }; + function toggleCssProps(manager, add) { + var element = manager.element; + each(manager.options.cssProps, function(value, name) { + element.style[prefixed(element.style, name)] = add ? value : ''; + }); + } /** - * Hide the component from the DOM + * trigger dom event + * @param {String} event + * @param {Object} data */ - ItemSet.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + function triggerDomEvent(event, data) { + var gestureEvent = document.createEvent('Event'); + gestureEvent.initEvent(event, true, true); + gestureEvent.gesture = data; + data.target.dispatchEvent(gestureEvent); + } - // remove the axis with dots - if (this.dom.axis.parentNode) { - this.dom.axis.parentNode.removeChild(this.dom.axis); - } + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, - // remove the labelset containing all group labels - if (this.dom.labelSet.parentNode) { - this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); - } - }; + 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, - /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed - */ - ItemSet.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } + 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, - // show axis with dots - if (!this.dom.axis.parentNode) { - this.body.dom.backgroundVertical.appendChild(this.dom.axis); - } + Manager: Manager, + Input: Input, + TouchAction: TouchAction, - // show labelset containing labels - if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); - } - }; + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, - /** - * Set selected items by their id. Replaces the current selection - * Unknown id's are silently ignored. - * @param {string[] | string} [ids] An array with zero or more id's of the items to be - * selected, or a single item id. If ids is undefined - * or an empty array, all items will be unselected. - */ - ItemSet.prototype.setSelection = function (ids) { - var i, ii, id, item; + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, - if (ids == undefined) ids = []; - if (!Array.isArray(ids)) ids = [ids]; + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); - // unselect currently selected items - for (i = 0, ii = this.selection.length; i < ii; i++) { - id = this.selection[i]; - item = this.items[id]; - if (item) item.unselect(); - } + 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; + } - // select items - this.selection = []; - for (i = 0, ii = ids.length; i < ii; i++) { - id = ids[i]; - item = this.items[id]; - if (item) { - this.selection.push(id); - item.select(); - } - } - }; + })(window, document, 'Hammer'); - /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items - */ - ItemSet.prototype.getSelection = function () { - return this.selection.concat([]); - }; - /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items - */ - ItemSet.prototype.getVisibleItems = function () { - var range = this.body.range.getRange(); - var left = this.body.util.toScreen(range.start); - var right = this.body.util.toScreen(range.end); +/***/ }, +/* 22 */ +/***/ function(module, exports, __webpack_require__) { - var ids = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - var group = this.groups[groupId]; - var rawVisibleItems = group.visibleItems; + /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; + + /* WEBPACK VAR INJECTION */}.call(exports, {})) - // filter the "raw" set with visibleItems into a set which is really - // visible by pixels - for (var i = 0; i < rawVisibleItems.length; i++) { - var item = rawVisibleItems[i]; - // TODO: also check whether visible vertically - if (item.left < right && item.left + item.width > left) { - ids.push(item.id); - } - } - } - } +/***/ }, +/* 23 */ +/***/ function(module, exports, __webpack_require__) { - return ids; - }; + "use strict"; - /** - * Deselect a selected item - * @param {String | Number} id - * @private - */ - ItemSet.prototype._deselect = function (id) { - var selection = this.selection; - for (var i = 0, ii = selection.length; i < ii; i++) { - if (selection[i] == id) { - // non-strict comparison! - selection.splice(i, 1); - break; - } - } - }; + 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); /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * @constructor Range + * A Range controls a numeric range with a start and end value. + * The Range adjusts the range based on mouse events or programmatic changes, + * and triggers events when the range is changing or has been changed. + * @param {{dom: Object, domProps: Object, emitter: Emitter}} body + * @param {Object} [options] See description at Range.setOptions */ - ItemSet.prototype.redraw = function () { - var margin = this.options.margin, - range = this.body.range, - asSize = util.option.asSize, - options = this.options, - orientation = options.orientation, - resized = false, - frame = this.dom.frame, - editable = options.editable.updateTime || options.editable.updateGroup; - - // recalculate absolute position (before redrawing groups) - this.props.top = this.body.domProps.top.height + this.body.domProps.border.top; - this.props.left = this.body.domProps.left.width + this.body.domProps.border.left; - - // update class name - frame.className = "itemset" + (editable ? " editable" : ""); - - // reorder the groups (if needed) - resized = this._orderGroups() || resized; + 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 - // check whether zoomed (in that case we need to re-stack everything) - // TODO: would be nicer to get this as a trigger from Range - var visibleInterval = range.end - range.start; - var zoomed = visibleInterval != this.lastVisibleInterval || this.props.width != this.props.lastWidth; - if (zoomed) this.stackDirty = true; - this.lastVisibleInterval = visibleInterval; - this.props.lastWidth = this.props.width; + this.body = body; + this.deltaDifference = 0; + this.scaleOffset = 0; + this.startToFront = false; + this.endToFront = true; - var restack = this.stackDirty; - var firstGroup = this._firstGroup(); - var firstMargin = { - item: margin.item, - axis: margin.axis - }; - var nonFirstMargin = { - item: margin.item, - axis: margin.item.vertical / 2 + // 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 }; - var height = 0; - var minHeight = margin.axis + margin.item.vertical; + this.options = util.extend({}, this.defaultOptions); - // redraw the background group - this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); + this.props = { + touch: {} + }; + this.animateTimer = null; - // redraw all regular groups - util.forEach(this.groups, function (group) { - var groupMargin = group == firstGroup ? firstMargin : nonFirstMargin; - var groupResized = group.redraw(range, groupMargin, restack); - resized = groupResized || resized; - height += group.height; - }); - height = Math.max(height, minHeight); - this.stackDirty = false; + // 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)); - // update frame height - frame.style.height = asSize(height); + // ignore dragging when holding + this.body.emitter.on("press", this._onHold.bind(this)); - // calculate actual size - this.props.width = frame.offsetWidth; - this.props.height = height; + // mouse wheel for zooming + this.body.emitter.on("mousewheel", this._onMouseWheel.bind(this)); - // reposition axis - this.dom.axis.style.top = asSize(orientation == "top" ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); - this.dom.axis.style.left = "0"; + // pinch to zoom + this.body.emitter.on("touch", this._onTouch.bind(this)); + this.body.emitter.on("pinch", this._onPinch.bind(this)); - // check if this component is resized - resized = this._isResized() || resized; + this.setOptions(options); + } - return resized; - }; + Range.prototype = new Component(); /** - * Get the first group, aligned with the axis - * @return {Group | null} firstGroup - * @private + * Set options for the range controller + * @param {Object} options Available options: + * {Number | Date | String} start Start date for the range + * {Number | Date | String} end End date for the range + * {Number} min Minimum value for start + * {Number} max Maximum value for end + * {Number} zoomMin Set a minimum value for + * (end - start). + * {Number} zoomMax Set a maximum value for + * (end - start). + * {Boolean} moveable Enable moving of the range + * by dragging. True by default + * {Boolean} zoomable Enable zooming of the range + * by pinching/scrolling. True by default */ - ItemSet.prototype._firstGroup = function () { - var firstGroupIndex = this.options.orientation == "top" ? 0 : this.groupIds.length - 1; - var firstGroupId = this.groupIds[firstGroupIndex]; - var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; + 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); - return firstGroup || null; + if ("start" in options || "end" in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); + } + } }; /** - * Create or delete the group holding all ungrouped items. This group is used when - * there are no groups specified. - * @protected + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - ItemSet.prototype._updateUngrouped = function () { - var ungrouped = this.groups[UNGROUPED]; - var background = this.groups[BACKGROUND]; - var item, itemId; + function validateDirection(direction) { + if (direction != "horizontal" && direction != "vertical") { + throw new TypeError("Unknown direction \"" + direction + "\". " + "Choose \"horizontal\" or \"vertical\"."); + } + } - if (this.groupsData) { - // remove the group holding all ungrouped items - if (ungrouped) { - ungrouped.hide(); - delete this.groups[UNGROUPED]; + /** + * 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] + * + */ + 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(); - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - item.parent && item.parent.remove(item); - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - group && group.add(item) || item.hide(); + 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; + + 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); + + 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 }); } - } - } - } else { - // create a group holding all (unfiltered) items - if (!ungrouped) { - var id = null; - var data = null; - ungrouped = new Group(id, data, this); - this.groups[UNGROUPED] = ungrouped; - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - ungrouped.add(item); + 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); } } + }; - ungrouped.show(); + 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); } } }; /** - * Get the element for the labelset - * @return {HTMLElement} labelSet + * Stop an animation + * @private */ - ItemSet.prototype.getLabelSet = function () { - return this.dom.labelSet; + Range.prototype._cancelAnimation = function () { + if (this.animateTimer) { + clearTimeout(this.animateTimer); + this.animateTimer = null; + } }; /** - * Set items - * @param {vis.DataSet | null} items + * Set a new start and end range. This method is the same as setRange, but + * does not trigger a range change and range changed event, and it returns + * true when the range is changed + * @param {Number} [start] + * @param {Number} [end] + * @return {Boolean} changed + * @private */ - ItemSet.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; + 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; - // 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"); + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error("Invalid start \"" + start + "\""); } - - 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); + if (isNaN(newEnd) || newEnd === null) { + throw new Error("Invalid end \"" + end + "\""); } - 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); - - // update the group holding all ungrouped items - this._updateUngrouped(); + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; } - }; - /** - * Get the current items - * @returns {vis.DataSet | null} - */ - ItemSet.prototype.getItems = function () { - return this.itemsData; - }; + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = min - newStart; + newStart += diff; + newEnd += diff; - /** - * Set groups - * @param {vis.DataSet} groups - */ - ItemSet.prototype.setGroups = function (groups) { - var me = this, - ids; + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } + } + } - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = newEnd - max; + newStart -= diff; + newEnd -= diff; - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } + } } - // 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"); + // 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 (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); + 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 the group holding all ungrouped items - this._updateUngrouped(); + var changed = this.start != newStart || this.end != newEnd; - // update the order of all items in each group - this._order(); + // if the new range does NOT overlap with the old range, emit checkRangedItems to avoid not showing ranged items (ranged meaning has end time, not necessarily of type Range) + if (!(newStart >= this.start && newStart <= this.end || newEnd >= this.start && newEnd <= this.end) && !(this.start >= newStart && this.start <= newEnd || this.end >= newStart && this.end <= newEnd)) { + this.body.emitter.emit("checkRangedItems"); + } - this.body.emitter.emit("change", { queue: true }); + this.start = newStart; + this.end = newEnd; + return changed; }; /** - * Get the current groups - * @returns {vis.DataSet | null} groups + * Retrieve the current range. + * @return {Object} An object with start and end properties */ - ItemSet.prototype.getGroups = function () { - return this.groupsData; + Range.prototype.getRange = function () { + return { + start: this.start, + end: this.end + }; }; /** - * Remove an item by its id - * @param {String | Number} id + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion */ - ItemSet.prototype.removeItem = function (id) { - var item = this.itemsData.get(id), - dataset = this.itemsData.getDataSet(); + Range.prototype.conversion = function (width, totalHidden) { + return Range.conversion(this.start, this.end, width, totalHidden); + }; - if (item) { - // confirm deletion - this.options.onRemove(item, function (item) { - if (item) { - // remove by id here, it is possible that an item has no id defined - // itself, so better not delete by the item itself - dataset.remove(id); - } - }); + /** + * 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 + }; } }; /** - * Get the time of an item based on it's data and options.type - * @param {Object} itemData - * @returns {string} Returns the type + * Start dragging horizontally or vertically + * @param {Event} event * @private */ - ItemSet.prototype._getType = function (itemData) { - return itemData.type || this.options.type || (itemData.end ? "range" : "box"); - }; + 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; + + if (this.body.dom.root) { + this.body.dom.root.style.cursor = "move"; + } + event.preventDefault(); + }; /** - * Get the group id for an item - * @param {Object} itemData - * @returns {string} Returns the groupId + * Perform dragging operation + * @param {Event} event * @private */ - ItemSet.prototype._getGroupId = function (itemData) { - var type = this._getType(itemData); - if (type == "background" && itemData.group == undefined) { - return BACKGROUND; - } else { - return this.groupsData ? itemData.group : UNGROUPED; + 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(); }; /** - * Handle updated items - * @param {Number[]} ids - * @protected + * Stop dragging operation + * @param {event} event + * @private */ - ItemSet.prototype._onUpdate = function (ids) { - var me = this; - - ids.forEach(function (id) { - var itemData = me.itemsData.get(id, me.itemOptions); - var item = me.items[id]; - var type = me._getType(itemData); + Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - var constructor = ItemSet.types[type]; + // TODO: this may be redundant in hammerjs2 + // refuse to drag when we where pinching to prevent the timeline make a jump + // when releasing the fingers in opposite order from the touch screen + if (!this.props.touch.allowDragging) return; - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, delete the item and recreate it - me._removeItem(item); - item = null; - } else { - me._updateItem(item, itemData); - } - } + this.props.touch.dragging = false; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = "auto"; + } - if (!item) { - // create item - if (constructor) { - item = new constructor(itemData, me.conversion, me.options); - item.id = id; // TODO: not so nice setting id afterwards - me._addItem(item); - } else if (type == "rangeoverflow") { - // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day - throw new TypeError("Item type \"rangeoverflow\" is deprecated. Use css styling instead: " + ".vis.timeline .item.range .content {overflow: visible;}"); - } else { - throw new TypeError("Unknown item type \"" + type + "\""); - } - } + // fire a rangechanged event + this.body.emitter.emit("rangechanged", { + start: new Date(this.start), + end: new Date(this.end), + byUser: true }); - - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit("change", { queue: true }); }; /** - * Handle added items - * @param {Number[]} ids - * @protected + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event + * @private */ - ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; + Range.prototype._onMouseWheel = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - /** - * Handle removed items - * @param {Number[]} ids - * @protected - */ - ItemSet.prototype._onRemove = function (ids) { - var count = 0; - var me = this; - ids.forEach(function (id) { - var item = me.items[id]; - if (item) { - count++; - me._removeItem(item); + // retrieve delta + var delta = 0; + if (event.wheelDelta) { + /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { + /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; + } + + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // perform the zoom action. Delta is normally 1 or -1 + + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - delta / 5; + } else { + scale = 1 / (1 + delta / 5); } - }); - if (count) { - // update order - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit("change", { queue: true }); + // 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); + + this.zoom(scale, pointerDate, delta); } + + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); }; /** - * Update the order of item in all groups + * Start of a touch gesture * @private */ - ItemSet.prototype._order = function () { - // reorder the items in all groups - // TODO: optimization: only reorder groups affected by the changed items - util.forEach(this.groups, function (group) { - group.order(); - }); + Range.prototype._onTouch = function (event) { + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.allowDragging = true; + this.props.touch.center = null; + this.scaleOffset = 0; + this.deltaDifference = 0; }; /** - * Handle updated groups - * @param {Number[]} ids + * On start of a hold gesture * @private */ - ItemSet.prototype._onUpdateGroups = function (ids) { - this._onAddGroups(ids); + Range.prototype._onHold = function () { + this.props.touch.allowDragging = false; }; /** - * Handle changed groups (added or updated) - * @param {Number[]} ids + * Handle pinch event + * @param {Event} event * @private */ - ItemSet.prototype._onAddGroups = function (ids) { - var me = this; - - ids.forEach(function (id) { - var groupData = me.groupsData.get(id); - var group = me.groups[id]; + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - if (!group) { - // check for reserved ids - if (id == UNGROUPED || id == BACKGROUND) { - throw new Error("Illegal group id. " + id + " is a reserved id."); - } + this.props.touch.allowDragging = false; - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null - }); + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.center, this.body.dom.center); + } - group = new Group(id, groupData, me); - me.groups[id] = group; + var scale = 1 / (event.scale + this.scaleOffset); + var centerDate = this._pointerToDate(this.props.touch.center); - // add items with this groupId to the new group - for (var itemId in me.items) { - if (me.items.hasOwnProperty(itemId)) { - var item = me.items[itemId]; - if (item.data.group == id) { - group.add(item); - } - } - } + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - group.order(); - group.show(); - } else { - // update group - group.setData(groupData); - } - }); + // 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.body.emitter.emit("change", { queue: true }); - }; + // 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 - /** - * Handle removed groups - * @param {Number[]} ids - * @private - */ - ItemSet.prototype._onRemoveGroups = function (ids) { - var groups = this.groups; - ids.forEach(function (id) { - var group = groups[id]; + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, 1 - scale, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, scale - 1, true); + if (safeStart != newStart || safeEnd != newEnd) { + this.props.touch.start = safeStart; + this.props.touch.end = safeEnd; + this.scaleOffset = 1 - event.scale; + newStart = safeStart; + newEnd = safeEnd; + } - if (group) { - group.hide(); - delete groups[id]; - } - }); + this.setRange(newStart, newEnd, false, true); - this.markDirty(); + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default - this.body.emitter.emit("change", { queue: true }); + event.preventDefault(); }; /** - * Reorder the groups if needed - * @return {boolean} changed + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date * @private */ - ItemSet.prototype._orderGroups = function () { - if (this.groupsData) { - // reorder the groups - var groupIds = this.groupsData.getIds({ - order: this.options.groupOrder - }); - - var changed = !util.equalArray(groupIds, this.groupIds); - if (changed) { - // hide all groups, removes them from the DOM - var groups = this.groups; - groupIds.forEach(function (groupId) { - groups[groupId].hide(); - }); - - // show the groups again, attach them to the DOM in correct order - groupIds.forEach(function (groupId) { - groups[groupId].show(); - }); + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - this.groupIds = groupIds; - } + validateDirection(direction); - return changed; + if (direction == "horizontal") { + return this.body.util.toTime(pointer.x).valueOf(); } else { - return false; + var height = this.body.domProps.center.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; } }; /** - * Add a new item - * @param {Item} item + * Get the pointer location relative to the location of the dom element + * @param {{x: Number, y: Number}} touch + * @param {Element} element HTML DOM element + * @return {{x: Number, y: Number}} pointer * @private */ - ItemSet.prototype._addItem = function (item) { - this.items[item.id] = item; - - // add to group - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); - }; + function getPointer(touch, element) { + return { + x: touch.x - util.getAbsoluteLeft(element), + y: touch.y - util.getAbsoluteTop(element) + }; + } /** - * Update an existing item - * @param {Item} item - * @param {Object} itemData - * @private + * Zoom the range the given scale in or out. Start and end date will + * be adjusted, and the timeline will be redrawn. You can optionally give a + * date around which to zoom. + * For example, try scale = 0.9 or 1.1 + * @param {Number} scale Scaling factor. Values above 1 will zoom out, + * values below 1 will zoom in. + * @param {Number} [center] Value representing a date around which will + * be zoomed. */ - ItemSet.prototype._updateItem = function (item, itemData) { - var oldGroupId = item.data.group; + 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; + } - // update the items data (will redraw the item when displayed) - item.setData(itemData); + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - // update group - if (oldGroupId != item.data.group) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); + // calculate new start and end + var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; + var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); + // snapping times away from hidden zones + this.startToFront = delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + this.endToFront = -delta > 0 ? false : true; // used to do the right autocorrection with periodic hidden times + var safeStart = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newStart, delta, true); + var safeEnd = DateUtil.snapAwayFromHidden(this.body.hiddenDates, newEnd, -delta, true); + if (safeStart != newStart || safeEnd != newEnd) { + newStart = safeStart; + newEnd = safeEnd; } + + this.setRange(newStart, newEnd, false, true); + + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default }; + + /** - * Delete an item from the ItemSet: remove it from the DOM, from the map - * with items, and from the map with visible items, and from the selection - * @param {Item} item - * @private + * Move the range with a given delta to the left or right. Start and end + * value will be adjusted. For example, try delta = 0.1 or -0.1 + * @param {Number} delta Moving amount. Positive value will move right, + * negative value will move left */ - ItemSet.prototype._removeItem = function (item) { - // remove from DOM - item.hide(); + Range.prototype.move = function (delta) { + // zoom start Date and end Date relative to the centerDate + var diff = this.end - this.start; - // remove from items - delete this.items[item.id]; + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; - // remove from selection - var index = this.selection.indexOf(item.id); - if (index != -1) this.selection.splice(index, 1); + // TODO: reckon with min and max range - // remove from group - item.parent && item.parent.remove(item); + this.start = newStart; + this.end = newEnd; }; /** - * Create an array containing all items being a range (having an end date) - * @param array - * @returns {Array} - * @private + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ - ItemSet.prototype._constructByEndArray = function (array) { - var endArray = []; + Range.prototype.moveTo = function (moveTo) { + var center = (this.start + this.end) / 2; - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof RangeItem) { - endArray.push(array[i]); - } - } - return endArray; - }; + var diff = center - moveTo; - /** - * Register the clicked item on touch, before dragStart is initiated. - * - * dragStart is initiated from a mousemove event, AFTER the mouse/touch is - * already moving. Therefore, the mouse/touch can sometimes be above an other - * DOM element than the item itself. - * - * @param {Event} event - * @private - */ - ItemSet.prototype._onTouch = function (event) { - // store the touched item, used in _onDragStart - this.touchParams.item = ItemSet.itemFromTarget(event); - this.touchParams.dragLeftItem = event.target.dragLeftItem || false; - this.touchParams.dragRightItem = event.target.dragRightItem || false; - this.touchParams.itemProps = null; + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); }; - /** - * Start dragging the selected events - * @param {Event} event - * @private - */ - ItemSet.prototype._onDragStart = function (event) { - if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { - return; - } + module.exports = Range; - var item = this.touchParams.item || null; - var me = this; - var props; +/***/ }, +/* 24 */ +/***/ function(module, exports, __webpack_require__) { - if (item && item.selected) { - var dragLeftItem = this.touchParams.dragLeftItem; - var dragRightItem = this.touchParams.dragRightItem; + "use strict"; - if (dragLeftItem) { - props = { - item: dragLeftItem, - initialX: event.center.x - }; + var Hammer = __webpack_require__(19); - if (me.options.editable.updateTime) { - props.start = item.data.start.valueOf(); - } - if (me.options.editable.updateGroup) { - if ("group" in item.data) props.group = item.data.group; - } + /** + * 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); + } + }; - this.touchParams.itemProps = [props]; - } else if (dragRightItem) { - props = { - item: dragRightItem, - initialX: event.center.x - }; + hammer.on("hammer.input", callback.inputHandler); + }; - if (me.options.editable.updateTime) { - props.end = item.data.end.valueOf(); - } - if (me.options.editable.updateGroup) { - if ("group" in item.data) props.group = item.data.group; - } + /** + * Register a release event, taking place after a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) + */ + exports.onRelease = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFinal) { + callback(event); + } + }; - this.touchParams.itemProps = [props]; - } else { - this.touchParams.itemProps = this.getSelection().map(function (id) { - var item = me.items[id]; - var props = { - item: item, - initialX: event.center.x - }; + return hammer.on("hammer.input", callback.inputHandler); + }; - if (me.options.editable.updateTime) { - if ("start" in item.data) { - props.start = item.data.start.valueOf(); + /** + * 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); + }; + + /** + * 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; - if ("end" in item.data) { - // we store a duration here in order not to change the width - // of the item when moving it. - props.duration = item.data.end.valueOf() - props.start; - } - } - } - if (me.options.editable.updateGroup) { - if ("group" in item.data) props.group = item.data.group; - } +/***/ }, +/* 25 */ +/***/ function(module, exports, __webpack_require__) { - return props; - }); - } + "use strict"; - event.stopPropagation(); - event.preventDefault(); + /** + * 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; + } + + /** + * 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); } }; /** - * Drag selected items - * @param {Event} event - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - ItemSet.prototype._onDrag = function (event) { - event.preventDefault(); - - if (this.touchParams.itemProps) { - var me = this; - var snap = this.options.snap || null; - var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; - var scale = this.body.util.getScale(); - var step = this.body.util.getStep(); + Component.prototype.redraw = function () { + // should be implemented by the component + return false; + }; - // move - this.touchParams.itemProps.forEach(function (props) { - var newProps = {}; - var current = me.body.util.toTime(event.center.x - xOffset); - var initial = me.body.util.toTime(props.initialX - xOffset); - var offset = current - initial; + /** + * Destroy the component. Cleanup DOM and event listeners + */ + Component.prototype.destroy = function () {}; - if ("start" in props) { - var start = new Date(props.start + offset); - newProps.start = snap ? snap(start, scale, step) : start; - } + /** + * Test whether the component is resized since the last time _isResized() was + * called. + * @return {Boolean} Returns true if the component is resized + * @protected + */ + Component.prototype._isResized = function () { + var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - if ("end" in props) { - var end = new Date(props.end + offset); - newProps.end = snap ? snap(end, scale, step) : end; - } else if ("duration" in props) { - newProps.end = new Date(newProps.start.valueOf() + props.duration); - } + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - if ("group" in props) { - // drag from one group to another - var group = me.groupFromTarget(event); - newProps.group = group && group.groupId; - } + return resized; + }; - // confirm moving the item - var itemData = util.extend({}, props.item.data, newProps); - me.options.onMoving(itemData, function (itemData) { - if (itemData) { - me._updateItemProps(props.item, itemData); - } - }); - }); + module.exports = Component; + // should be implemented by the component - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit("change"); +/***/ }, +/* 26 */ +/***/ function(module, exports, __webpack_require__) { - event.stopPropagation(); - } - }; + "use strict"; /** - * Update an items properties - * @param {Item} item - * @param {Object} props Can contain properties start, end, and group. - * @private + * Created by Alex on 10/3/2014. */ - ItemSet.prototype._updateItemProps = function (item, props) { - // TODO: copy all properties from props to item? (also new ones) - if ("start" in props) { - item.data.start = props.start; - } - if ("end" in props) { - item.data.end = props.end; - } else if ("duration" in props) { - item.data.end = new Date(props.start.valueOf() + props.duration); - } - if ("group" in props && item.data.group != props.group) { - this._moveToGroup(item, props.group); - } - }; + var moment = __webpack_require__(2); + /** - * Move an item to another group - * @param {Item} item - * @param {String | Number} groupId - * @private + * used in Core to convert the options into a volatile variable + * + * @param Core */ - ItemSet.prototype._moveToGroup = function (item, groupId) { - var group = this.groups[groupId]; - if (group && group.groupId != item.data.group) { - var oldGroup = item.parent; - oldGroup.remove(item); - oldGroup.order(); - group.add(item); - group.order(); - - item.data.group = group.groupId; + 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 + } } }; + /** - * End of dragging selected items - * @param {Event} event - * @private + * create new entrees for the repeating hidden dates + * @param body + * @param hiddenDates */ - ItemSet.prototype._onDragEnd = function (event) { - if (this.touchParams.itemProps) { - // prepare a change set for the changed items - var changes = [], - me = this, - dataset = this.itemsData.getDataSet(); + exports.updateHiddenDates = function (body, hiddenDates) { + if (hiddenDates && body.domProps.centerContainer.width !== undefined) { + exports.convertHiddenOptions(body, hiddenDates); - var itemProps = this.touchParams.itemProps; - this.touchParams.itemProps = null; - itemProps.forEach(function (props) { - var id = props.item.id, - itemData = me.itemsData.get(id, me.itemOptions); + var start = moment(body.range.start); + var end = moment(body.range.end); - var changed = false; - if ("start" in props.item.data) { - changed = props.start != props.item.data.start.valueOf(); - itemData.start = util.convert(props.item.data.start, dataset._options.type && dataset._options.type.start || "Date"); - } - if ("end" in props.item.data) { - changed = changed || props.end != props.item.data.end.valueOf(); - itemData.end = util.convert(props.item.data.end, dataset._options.type && dataset._options.type.end || "Date"); - } - if ("group" in props.item.data) { - changed = changed || props.group != props.item.data.group; - itemData.group = props.item.data.group; - } + var totalRange = body.range.end - body.range.start; + var pixelTime = totalRange / body.domProps.centerContainer.width; - // only apply changes when start or end is actually changed - if (changed) { - me.options.onMove(itemData, function (itemData) { - if (itemData) { - // apply changes - itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) - changes.push(itemData); - } else { - // restore original values - me._updateItemProps(props.item, props); + 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); - me.stackDirty = true; // force re-stacking of all items next redraw - me.body.emitter.emit("change"); + if (startDate._d == "Invalid Date") { + throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); + } + if (endDate._d == "Invalid Date") { + throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); + } + + var duration = endDate - startDate; + if (duration >= 4 * pixelTime) { + var offset = 0; + var runUntil = end.clone(); + switch (hiddenDates[i].repeat) { + case "daily": + // case of time + if (startDate.day() != endDate.day()) { + offset = 1; + } + startDate.dayOfYear(start.dayOfYear()); + startDate.year(start.year()); + startDate.subtract(7, "days"); + + endDate.dayOfYear(start.dayOfYear()); + endDate.year(start.year()); + endDate.subtract(7 - offset, "days"); + + runUntil.add(1, "weeks"); + break; + case "weekly": + var dayOffset = endDate.diff(startDate, "days"); + var day = startDate.day(); + + // set the start date to the range.start + startDate.date(start.date()); + startDate.month(start.month()); + startDate.year(start.year()); + endDate = startDate.clone(); + + // 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() }); + } } - }); - - // apply the changes to the data (if there are changes) - if (changes.length) { - dataset.update(changes); } - - event.stopPropagation(); + // 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); + } } }; + /** - * Handle selecting/deselecting an item when tapping it - * @param {Event} event - * @private + * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. + * Scales with N^2 + * @param body */ - ItemSet.prototype._onSelectItem = function (event) { - if (!this.options.selectable) return; - - var ctrlKey = event.srcEvent && event.srcEvent.ctrlKey; - var shiftKey = event.srcEvent && event.srcEvent.shiftKey; - if (ctrlKey || shiftKey) { - this._onMultiSelectItem(event); - return; + exports.removeDuplicates = function (body) { + var hiddenDates = body.hiddenDates; + var safeDates = []; + for (var i = 0; i < hiddenDates.length; i++) { + for (var j = 0; j < hiddenDates.length; j++) { + if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { + // j inside i + if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[j].remove = true; + } + // j start inside i + else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { + hiddenDates[i].end = hiddenDates[j].end; + hiddenDates[j].remove = true; + } + // j end inside i + else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[i].start = hiddenDates[j].start; + hiddenDates[j].remove = true; + } + } + } } - var oldSelection = this.getSelection(); - - var item = ItemSet.itemFromTarget(event); - var selection = item ? [item.id] : []; - this.setSelection(selection); + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].remove !== true) { + safeDates.push(hiddenDates[i]); + } + } - var newSelection = this.getSelection(); + body.hiddenDates = safeDates; + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time + }; - // emit a select event, - // except when old selection is empty and new selection is still empty - if (newSelection.length > 0 || oldSelection.length > 0) { - this.body.emitter.emit("select", { - items: newSelection - }); + 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); } }; /** - * Handle creation and updates of an item on double tap - * @param event - * @private + * Used in TimeStep to avoid the hidden times. + * @param timeStep + * @param previousTime */ - ItemSet.prototype._onAddItem = function (event) { - if (!this.options.selectable) return; - if (!this.options.editable.add) return; + 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; + } + } - var me = this, - snap = this.options.snap || null, - item = ItemSet.itemFromTarget(event); + if (stepInHidden == true && currentValue < timeStep._end.valueOf() && currentValue != previousTime) { + var prevValue = moment(previousTime); + var newValue = moment(endDate); + //check if the next step should be major + if (prevValue.year() != newValue.year()) { + timeStep.switchedYear = true; + } else if (prevValue.month() != newValue.month()) { + timeStep.switchedMonth = true; + } else if (prevValue.dayOfYear() != newValue.dayOfYear()) { + timeStep.switchedDay = true; + } - if (item) { - // update item + timeStep.current = newValue.toDate(); + } + }; - // execute async handler to update the item (or cancel it) - var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset - this.options.onUpdate(itemData, function (itemData) { - if (itemData) { - me.itemsData.getDataSet().update(itemData); - } - }); - } else { - // add item - var xAbs = util.getAbsoluteLeft(this.dom.frame); - var x = event.center.x - xAbs; - var start = this.body.util.toTime(x); - var scale = this.body.util.getScale(); - var step = this.body.util.getStep(); - var newItem = { - start: snap ? snap(start, scale, step) : start, - content: "new item" - }; + ///** + // * 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(); + // } + //}; - // when default type is a range, add a default end date to the new item - if (this.options.type === "range") { - var end = this.body.util.toTime(x + this.props.width / 5); - newItem.end = snap ? snap(end, scale, step) : end; + /** + * 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; } - newItem[this.itemsData._fieldId] = util.randomUUID(); - - var group = this.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; - } + var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); - // 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 conversion = Core.range.conversion(width, duration); + return (time.valueOf() - conversion.offset) * conversion.scale; } }; + /** - * Handle selecting/deselecting multiple items when holding an item - * @param {Event} event - * @private + * Replaces the core toTime methods + * @param body + * @param range + * @param x + * @param width + * @returns {Date} */ - 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 + 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); - // determine the selection range - selection.push(item.id); - var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); + return newTime; + } + }; - // 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); - } + /** + * Support function + * + * @param hiddenDates + * @param range + * @returns {number} + */ + exports.getHiddenDurationBetween = function (hiddenDates, start, end) { + var duration = 0; + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= start && endDate < end) { + duration += endDate - startDate; } - - this.setSelection(selection); - - this.body.emitter.emit("select", { - items: this.getSelection() - }); } + return duration; }; + /** - * 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 + * Support function + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} */ - ItemSet._getItemRange = function (itemsData) { - var max = null; - var min = null; + exports.correctTimeForHidden = function (hiddenDates, range, time) { + time = moment(time).toDate().valueOf(); + time -= exports.getHiddenDurationBefore(hiddenDates, range, time); + return time; + }; - itemsData.forEach(function (data) { - if (min == null || data.start < min) { - min = data.start; - } + exports.getHiddenDurationBefore = function (hiddenDates, range, time) { + var timeOffset = 0; + time = moment(time).toDate().valueOf(); - if (data.end != undefined) { - if (max == null || data.end > max) { - max = data.end; - } - } else { - if (max == null || data.start > max) { - max = data.start; + 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 { - min: min, - max: max - }; + } + return timeOffset; }; /** - * 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 + * 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}} */ - ItemSet.itemFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty("timeline-item")) { - return target["timeline-item"]; + 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; + } } - target = target.parentNode; } - return null; + return hiddenDuration; }; - /** - * 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; + /** + * 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 { - if (i === 0 && clientY < top + foreground.offset) { - return group; + if (correctionEnabled == true) { + return isHidden.endDate + (time - isHidden.startDate) + 1; + } else { + return isHidden.endDate + 1; } } + } else { + return time; } - - 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 + * Check if a time is hidden + * + * @param time + * @param hiddenDates + * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} */ - ItemSet.itemSetFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty("timeline-itemset")) { - return target["timeline-itemset"]; + 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; } - target = target.parentNode; } - - return null; + return { hidden: false, startDate: startDate, endDate: endDate }; }; - module.exports = ItemSet; - /***/ }, -/* 33 */ +/* 27 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var Emitter = __webpack_require__(11); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var Component = __webpack_require__(25); + 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); /** - * Legend for Graph2d + * 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 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; - }; + function Core() {} - Legend.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; - } - }; + // turn Core into an event emitter + Emitter(Core.prototype); - 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"; + /** + * 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 = {}; - this.dom.textArea = document.createElement("div"); - this.dom.textArea.className = "legendText"; - this.dom.textArea.style.position = "relative"; - this.dom.textArea.style.top = "0px"; + this.dom.root = document.createElement("div"); + this.dom.background = document.createElement("div"); + this.dom.backgroundVertical = document.createElement("div"); + this.dom.backgroundHorizontal = document.createElement("div"); + this.dom.centerContainer = document.createElement("div"); + this.dom.leftContainer = document.createElement("div"); + this.dom.rightContainer = document.createElement("div"); + this.dom.center = document.createElement("div"); + this.dom.left = document.createElement("div"); + this.dom.right = document.createElement("div"); + this.dom.top = document.createElement("div"); + this.dom.bottom = document.createElement("div"); + this.dom.shadowTop = document.createElement("div"); + this.dom.shadowBottom = document.createElement("div"); + this.dom.shadowTopLeft = document.createElement("div"); + this.dom.shadowBottomLeft = document.createElement("div"); + this.dom.shadowTopRight = document.createElement("div"); + this.dom.shadowBottomRight = document.createElement("div"); - this.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.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"; - this.dom.frame.appendChild(this.svg); - this.dom.frame.appendChild(this.dom.textArea); - }; + 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); - /** - * 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); - } - }; + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); - /** - * 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.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); - Legend.prototype.setOptions = function (options) { - var fields = ["enabled", "orientation", "icons", "left", "right"]; - util.selectiveDeepExtend(fields, this.options, options); - }; + this.on("rangechange", this.redraw.bind(this)); - 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++; + 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[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 = ""; + // redraw immediately + me._redraw(); } + }); - 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(); - } + // 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 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 + "
"; - } + 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); } - this.dom.textArea.innerHTML = content; - this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + "px"; } + this.dom.root.addEventListener("mousewheel", onMouseWheel); + this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel); + + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; + + this.redrawCount = 0; + + // attach the root panel to the provided container + if (!container) throw new Error("No container provided"); + container.appendChild(this.dom.root); }; - 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; + /** + * 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); - this.svg.style.width = iconWidth + 5 + iconOffset + "px"; + if ("hiddenDates" in this.options) { + DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); + } - 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; + 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; } } } - DOMutil.cleanupElements(this.svgElements); + // enable/disable autoResize + this._initAutoResize(); } - }; - - module.exports = Legend; -/***/ }, -/* 34 */ -/***/ function(module, exports, __webpack_require__) { + // propagate options to all components + this.components.forEach(function (component) { + return component.setOptions(options); + }); - "use strict"; + // 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."); + } - 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__(50); + // redraw everything + this._redraw(); + }; - var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items + /** + * Returns true when the Timeline is active. + * @returns {boolean} + */ + Core.prototype.isActive = function () { + return !this.activator || this.activator.active; + }; /** - * This is the constructor of the LineGraph. It requires a Timeline body and options. - * - * @param body - * @param options - * @constructor + * Destroy the Core, clean up all DOM elements and event listeners. */ - function LineGraph(body, options) { - this.id = util.randomUUID(); - this.body = body; + Core.prototype.destroy = function () { + // unbind datasets + this.clear(); - 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: {} - } - }; + // remove all event listeners + this.off(); - // 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; + // stop checking for changed size + this._stopAutoResize(); - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); + } + this.dom = null; - // 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); - } - }; + // remove Activator + if (this.activator) { + this.activator.destroy(); + delete this.activator; + } - // 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); + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; } - }; - - 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.listeners = null; + this.hammer = null; - 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); + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + return component.destroy(); }); - // 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.body = null; + }; - LineGraph.prototype = new Component(); /** - * Create the HTML DOM for the ItemSet + * Set a custom time bar + * @param {Date} time + * @param {int} id */ - LineGraph.prototype._create = function () { - var frame = document.createElement("div"); - frame.className = "LineGraph"; - this.dom.frame = frame; - - // create svg element for graph drawing. - this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - this.svg.style.position = "relative"; - this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; - this.svg.style.display = "block"; - frame.appendChild(this.svg); - - // data axis - this.options.dataAxis.orientation = "left"; - this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - - this.options.dataAxis.orientation = "right"; - this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - delete this.options.dataAxis.orientation; + Core.prototype.setCustomTime = function (time, id) { + if (!this.customTime) { + throw new Error("Cannot get custom time: Custom time bar is not enabled"); + } - // 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 barId = id || 0; - this.show(); + this.components.forEach(function (element, index, components) { + if (element instanceof CustomTime && element.options.id === barId) { + element.setCustomTime(time); + } + }); }; /** - * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. - * @param {object} options + * Retrieve the current custom time. + * @return {Date} customTime + * @param {int} id */ - 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"); - - 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; - } - } - } - } - - if (this.yAxisLeft) { - if (options.dataAxis !== undefined) { - this.yAxisLeft.setOptions(this.options.dataAxis); - this.yAxisRight.setOptions(this.options.dataAxis); - } - } + Core.prototype.getCustomTime = function (id) { + if (!this.customTime) { + throw new Error("Cannot get custom time: Custom time bar is not enabled"); + } - if (this.legendLeft) { - if (options.legend !== undefined) { - this.legendLeft.setOptions(this.options.legend); - this.legendRight.setOptions(this.options.legend); - } - } + var barId = id || 0, + customTime = this.customTime.getCustomTime(); - if (this.groups.hasOwnProperty(UNGROUPED)) { - this.groups[UNGROUPED].setOptions(options); + this.components.forEach(function (element, index, components) { + if (element instanceof CustomTime && element.options.id === barId) { + customTime = element.getCustomTime(); } - } + }); - // this is used to redraw the graph if the visibility of the groups is changed. - if (this.dom.frame) { - this.redraw(true); - } + return customTime; }; /** - * Hide the component from the DOM + * 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 */ - LineGraph.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); + Core.prototype.addCustomTime = function (time, id) { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); } - }; - - /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed - */ - LineGraph.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); + if (time === undefined) { + throw new Error("Time parameter for the custom bar must be provided"); } - }; + var ts = util.convert(time, "Date").valueOf(), + numIds, + customTime, + customBarId; - /** - * Set items - * @param {vis.DataSet | null} items - */ - LineGraph.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; - - // replace the dataset - if (!items) { - this.itemsData = null; - } else if (items instanceof DataSet || items instanceof DataView) { - this.itemsData = items; - } else { - throw new TypeError("Data must be an instance of DataSet or DataView"); + // 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]; } - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); + // 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); }); - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } - - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); + 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"); + } }); - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + customBarId = id; } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); - }; + this.customBarIds.push(customBarId); + + customTime = new CustomTime(this.body, { + showCustomTime: true, + time: ts, + id: customBarId + }); + + this.components.push(customTime); + this.redraw(); + + return customBarId; + }; /** - * Set groups - * @param {vis.DataSet} groups + * 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 */ - LineGraph.prototype.setGroups = function (groups) { + Core.prototype.removeCustomTime = function (id) { var me = this; - var ids; - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + 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(); + } + } + }); + }; + + + /** + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items + */ + Core.prototype.getVisibleItems = function () { + return this.itemSet && this.itemSet.getVisibleItems() || []; + }; + + - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw + /** + * 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); } - // 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"); + // clear groups + if (!what || what.groups) { + this.setGroups(null); } - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + return component.setOptions(component.defaultOptions); }); - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); + this.setOptions(this.defaultOptions); // this will also do a redraw } - this._onUpdate(); }; - /** - * Update the data - * @param [ids] - * @private + * 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. */ - 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]); + 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; } - //this._updateGraph(); - this.redraw(true); - }; - LineGraph.prototype._onAddGroups = function (groupIds) { - this._onUpdateGroups(groupIds); + var animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(range.start, range.end, animate); }; - /** - * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph - * @param {Array} groupIds - * @private + * Calculate the data range of the items and applies a 5% window around it. + * @returns {{start: Date | null, end: Date | null}} + * @protected */ - 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]]; + Core.prototype._getDataRange = function () { + // apply the data range as range + var dataRange = this.getItemRange(); + + // add 5% space on both sides + var start = dataRange.min; + var end = dataRange.max; + if (start != null && end != null) { + var interval = end.valueOf() - start.valueOf(); + if (interval <= 0) { + // prevent an empty interval + interval = 24 * 60 * 60 * 1000; // 1 day } + start = new Date(start.valueOf() - interval * 0.05); + end = new Date(end.valueOf() + interval * 0.05); } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); - }; + return { + start: start, + end: end + }; + }; /** - * update a group object with the group dataset entree + * Set the visible window. Both parameters are optional, you can change only + * start or only end. Syntax: * - * @param group - * @param groupId - * @private + * 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. */ - 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]); - } + 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 { - 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]); - } + animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(start, end, animate); } - this.legendLeft.redraw(); - this.legendRight.redraw(); }; - /** - * this updates all groups, it is used when there is an update the the itemset. - * - * @private + * 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. */ - 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]); - } - } - } - }; + Core.prototype.moveTo = function (time, options) { + var interval = this.range.end - this.range.start; + var t = util.convert(time, "Date").valueOf(); + + var start = t - interval / 2; + var end = t + interval / 2; + var animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(start, end, animate); + }; /** - * 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 + * Get the visible window + * @return {{start: Date, end: Date}} Visible range */ - 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); - } - - this.legendLeft.redraw(); - this.legendRight.redraw(); + Core.prototype.getWindow = function () { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; }; + /** + * Force a redraw. Can be overridden by implementations of Core + */ + Core.prototype.redraw = function () { + this._redraw(); + }; /** - * Redraw the component, mandatory function - * @return {boolean} Returns true if the component is resized + * Redraw for internal use. Redraws all components. See also the public + * method redraw. + * @protected */ - LineGraph.prototype.redraw = function (forceGraphUpdate) { + Core.prototype._redraw = function () { var resized = false; + var options = this.options; + var props = this.props; + var dom = this.dom; - // 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 (!dom) return; // when destroyed - // check if this component is resized - resized = this._isResized() || resized; + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - // 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; + // update class names + if (options.orientation == "top") { + util.addClassName(dom.root, "top"); + util.removeClassName(dom.root, "bottom"); + } else { + util.removeClassName(dom.root, "top"); + util.addClassName(dom.root, "bottom"); + } + // 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, ""); - // 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); + // 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; - // 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; - } + // 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 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 (dom.root.clientHeight === 0) { + borderRootWidth = borderRootHeight; } - // 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"; - } - } - } + // 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; - this.legendLeft.redraw(); - this.legendRight.redraw(); - return resized; - }; + // 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; - /** - * 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; + // 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; - // 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); + // 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"; - // apply sampling, if disabled, it will pass through this function. - this._applySampling(groupIds, groupsData); + 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"; - // we transform the X coordinates to detect collisions - for (i = 0; i < groupIds.length; i++) { - preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); - } + // 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"; - // now all needed data has been collected we start the processing. - this._getYRanges(groupIds, preprocessedGroupData, groupRanges); + // 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(); - // 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; + // 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"; - // 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); - } + // 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; - // 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); - } + // 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; } - // cleanup unused svg elements - DOMutil.cleanupElements(this.svgElements); - return false; + this.emit("finishedRedraw"); }; + // TODO: deprecated since version 1.1.0, remove some day + Core.prototype.repaint = function () { + throw new Error("Function repaint is deprecated. Use redraw instead."); + }; /** - * 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 + * 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. */ - 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); - } - } - } - } - } + Core.prototype.setCurrentTime = function (time) { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); } + + this.currentTime.setCurrentTime(time); }; + /** + * 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"); + } + + return this.currentTime.getCurrentTime(); + }; /** - * - * @param groupIds - * @param groupsData + * 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 */ - 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; + // TODO: move this function to Range + Core.prototype._toTime = function (x) { + return DateUtil.toTime(this, x, this.props.center.width); + }; - // 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))); + /** + * 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 + */ + // 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); + }; - var sampledData = []; - for (var j = 0; j < amountOfPoints; j += increment) { - sampledData.push(dataContainer[j]); - } - groupsData[groupIds[i]] = sampledData; - } - } - } - } + /** + * 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); }; + /** - * - * - * @param {array} groupIds - * @param {object} groupsData - * @param {object} groupRanges | this is being filled here + * 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 */ - 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]); - } - } - } + // 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; + }; - // 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"); + + /** + * Initialize watching when option autoResize is true + * @private + */ + Core.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } else { + this._stopAutoResize(); } }; - /** - * 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 + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. * @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; - } - } + Core.prototype._startAutoResize = function () { + var me = this; - // 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; + this._stopAutoResize(); - 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; - } - } - } + this._onResize = function () { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; } - 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; + 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; - 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; + me.emit("change"); + } } + }; - 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; - } - - // clean the accumulated lists - if (groupIds.indexOf("__barchartLeft") != -1) { - groupIds.splice(groupIds.indexOf("__barchartLeft"), 1); - } - if (groupIds.indexOf("__barchartRight") != -1) { - groupIds.splice(groupIds.indexOf("__barchartRight"), 1); - } + // add event listener to window resize + util.addEventListener(window, "resize", this._onResize); - return resized; + this.watchTimer = setInterval(this._onResize, 1000); }; - /** - * 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} + * Stop watching for a resize of the frame. * @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; - } + Core.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; } - return changed; - }; + // remove event listener on window.resize + util.removeEventListener(window, "resize", this._onResize); + this._onResize = null; + }; /** - * 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} + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ - LineGraph.prototype._convertXcoordinates = function (datapoints) { - var extractedData = []; - var xValue, yValue; - var toScreen = this.body.util.toScreen; - - for (var i = 0; i < datapoints.length; i++) { - xValue = toScreen(datapoints[i].x) + this.props.width; - yValue = datapoints[i].y; - extractedData.push({ x: xValue, y: yValue }); - } - - return extractedData; + Core.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; }; - /** - * 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} + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop * @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; - } - - 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 }); + 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; } - group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); + // 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 extractedData; + return this.props.scrollTop; }; + /** + * Get the current scrollTop + * @returns {number} scrollTop + * @private + */ + Core.prototype._getScrollTop = function () { + return this.props.scrollTop; + }; - module.exports = LineGraph; + module.exports = Core; /***/ }, -/* 35 */ +/* 28 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var Hammer = __webpack_require__(19); var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); + var TimeStep = __webpack_require__(29); var Component = __webpack_require__(25); - var TimeStep = __webpack_require__(19); - var DateUtil = __webpack_require__(15); - var moment = __webpack_require__(44); + 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 UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items + var BACKGROUND = "__background__"; // reserved group id for background items without group /** - * A horizontal time axis + * An ItemSet holds a set of items and ranges which can be displayed in a + * range. The width is determined by the parent of the ItemSet, and the height + * is determined by the size of the items. * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body - * @param {Object} [options] See TimeAxis.setOptions for the available - * options. - * @constructor TimeAxis + * @param {Object} [options] See ItemSet.setOptions for the available options. + * @constructor ItemSet * @extends Component */ - function TimeAxis(body, options) { - this.dom = { - foreground: null, - lines: [], - majorTexts: [], - minorTexts: [], - redundant: { - lines: [], - majorTexts: [], - minorTexts: [] - } + function ItemSet(body, options) { + this.body = body; + + this.defaultOptions = { + type: null, // 'box', 'point', 'range', 'background' + orientation: "bottom", // 'top' or 'bottom' + align: "auto", // alignment of box items + stack: true, + groupOrder: null, + + selectable: true, + editable: { + updateTime: false, + updateGroup: false, + add: false, + remove: false + }, + + snap: TimeStep.snap, + + onAdd: function (item, callback) { + callback(item); + }, + onUpdate: function (item, callback) { + callback(item); + }, + onMove: function (item, callback) { + callback(item); + }, + onRemove: function (item, callback) { + callback(item); + }, + onMoving: function (item, callback) { + callback(item); + }, + + margin: { + item: { + horizontal: 10, + vertical: 10 + }, + axis: 20 + }, + padding: 5 + }; + + // options is shared by this ItemSet and all its items + this.options = util.extend({}, this.defaultOptions); + + // options for getting items from the DataSet with the correct type + this.itemOptions = { + type: { start: "Date", end: "Date" } + }; + + this.conversion = { + toScreen: body.util.toScreen, + toTime: body.util.toTime }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 + this.dom = {}; + this.props = {}; + this.hammer = null; + + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // listeners for the DataSet of the items + this.itemListeners = { + add: function (event, params, senderId) { + me._onAdd(params.items); }, - lineTop: 0 + update: function (event, params, senderId) { + me._onUpdate(params.items); + }, + remove: function (event, params, senderId) { + me._onRemove(params.items); + } }; - this.defaultOptions = { - orientation: "bottom", // supported: 'top', 'bottom' - // TODO: implement timeaxis orientations 'left' and 'right' - showMinorLabels: true, - showMajorLabels: true, - format: null, - timeAxis: null + // 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.options = util.extend({}, this.defaultOptions); - this.body = body; + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group + this.groupIds = []; + + this.selection = []; // list with the ids of all selected nodes + this.stackDirty = true; // if true, all items will be restacked on next redraw + this.touchParams = {}; // stores properties while dragging // create the HTML DOM + this._create(); this.setOptions(options); } - 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"); + ItemSet.prototype = new Component(); - this.dom.foreground.className = "timeaxis foreground"; - this.dom.background.className = "timeaxis background"; + // available item types will be registered here + ItemSet.types = { + background: BackgroundItem, + box: BoxItem, + range: RangeItem, + point: PointItem }; /** - * Destroy the TimeAxis + * Create the HTML DOM for the ItemSet */ - 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); - } + ItemSet.prototype._create = function () { + var frame = document.createElement("div"); + frame.className = "itemset"; + frame["timeline-itemset"] = this; + this.dom.frame = frame; - this.body = null; - }; + // create background panel + var background = document.createElement("div"); + background.className = "background"; + frame.appendChild(background); + this.dom.background = background; - /** - * 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; + // create foreground panel + var foreground = document.createElement("div"); + foreground.className = "foreground"; + frame.appendChild(foreground); + this.dom.foreground = foreground; - // determine the correct parent DOM element (depending on option orientation) - var parent = options.orientation == "top" ? this.body.dom.top : this.body.dom.bottom; - var parentChanged = foreground.parentNode !== parent; + // create axis panel + var axis = document.createElement("div"); + axis.className = "axis"; + this.dom.axis = axis; - // calculate character width and height - this._calculateCharSize(); + // create labelset + var labelSet = document.createElement("div"); + labelSet.className = "labelset"; + this.dom.labelSet = labelSet; - // 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; + // create ungrouped Group + this._updateUngrouped(); - // 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; + // create background Group + var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); + backgroundGroup.show(); + this.groups[BACKGROUND] = backgroundGroup; - 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 + // attach event listeners + // Note: we bind to the centerContainer for the case where the height + // of the center container is larger than of the ItemSet, so we + // can click in the empty area to create a new item or deselect an item. + this.hammer = new Hammer(this.body.dom.centerContainer); - // 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); + // drag items when selected + this.hammer.on("hammer.input", (function (event) { + if (event.isFirst) { + this._onTouch(event); + } + }).bind(this)); + this.hammer.on("panstart", this._onDragStart.bind(this)); + this.hammer.on("panmove", this._onDrag.bind(this)); + this.hammer.on("panend", this._onDragEnd.bind(this)); - foreground.style.height = this.props.height + "px"; + // single select (or unselect) when tapping an item + this.hammer.on("tap", this._onSelectItem.bind(this)); - this._repaintLabels(); + // multi select when holding mouse/touch, or on ctrl+click + this.hammer.on("press", this._onMultiSelectItem.bind(this)); - // 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); - } + // add item on doubletap + this.hammer.on("doubletap", this._onAddItem.bind(this)); - return this._isResized() || parentChanged; + // attach to the DOM + this.show(); }; /** - * Repaint major and minor text labels and vertical grid lines - * @private + * Set options for the ItemSet. Existing options will be extended/overwritten. + * @param {Object} [options] The following options are available: + * {String} type + * Default type for the items. Choose from 'box' + * (default), 'point', 'range', or 'background'. + * The default style can be overwritten by + * individual items. + * {String} align + * Alignment for the items, only applicable for + * BoxItem. Choose 'center' (default), 'left', or + * 'right'. + * {String} orientation + * Orientation of the item set. Choose 'top' or + * 'bottom' (default). + * {Function} groupOrder + * A sorting function for ordering groups + * {Boolean} stack + * If true (deafult), items will be stacked on + * top of each other. + * {Number} margin.axis + * Margin between the axis and the items in pixels. + * Default is 20. + * {Number} margin.item.horizontal + * Horizontal margin between items in pixels. + * Default is 10. + * {Number} margin.item.vertical + * Vertical Margin between items in pixels. + * Default is 10. + * {Number} margin.item + * Margin between items in pixels in both horizontal + * and vertical direction. Default is 10. + * {Number} margin + * Set margin for both axis and items in pixels. + * {Number} padding + * Padding of the contents of an item in pixels. + * Must correspond with the items css. Default is 5. + * {Boolean} selectable + * If true (default), items can be selected. + * {Boolean} editable + * Set all editable options to true or false + * {Boolean} editable.updateTime + * Allow dragging an item to an other moment in time + * {Boolean} editable.updateGroup + * Allow dragging an item to an other group + * {Boolean} editable.add + * Allow creating new items on double tap + * {Boolean} editable.remove + * Allow removing items by clicking the delete button + * top right of a selected item. + * {Function(item: Item, callback: Function)} onAdd + * Callback function triggered when an item is about to be added: + * when the user double taps an empty space in the Timeline. + * {Function(item: Item, callback: Function)} onUpdate + * Callback function fired when an item is about to be updated. + * This function typically has to show a dialog where the user + * change the item. If not implemented, nothing happens. + * {Function(item: Item, callback: Function)} onMove + * Fired when an item has been moved. If not implemented, + * the move action will be accepted. + * {Function(item: Item, callback: Function)} onRemove + * Fired when an item is about to be deleted. + * If not implemented, the item will be always removed. */ - 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(); + ItemSet.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + var fields = ["type", "align", "orientation", "padding", "stack", "selectable", "groupOrder", "dataAttributes", "template", "hide", "snap"]; + util.selectiveExtend(fields, this.options, options); - xPrev = x; - x = this.body.util.toScreen(cur); - width = x - xPrev; - if (prevLine) { - prevLine.style.width = width + "px"; + if ("margin" in options) { + if (typeof options.margin === "number") { + this.options.margin.axis = options.margin; + this.options.margin.item.horizontal = options.margin; + this.options.margin.item.vertical = options.margin; + } else if (typeof options.margin === "object") { + util.selectiveExtend(["axis"], this.options.margin, options.margin); + if ("item" in options.margin) { + if (typeof options.margin.item === "number") { + this.options.margin.item.horizontal = options.margin.item; + this.options.margin.item.vertical = options.margin.item; + } else if (typeof options.margin.item === "object") { + util.selectiveExtend(["horizontal", "vertical"], this.options.margin.item, options.margin.item); + } + } + } } - if (this.options.showMinorLabels) { - this._repaintMinorText(x, step.getLabelMinor(), orientation, className); + if ("editable" in options) { + if (typeof options.editable === "boolean") { + this.options.editable.updateTime = options.editable; + this.options.editable.updateGroup = options.editable; + this.options.editable.add = options.editable; + this.options.editable.remove = options.editable; + } else if (typeof options.editable === "object") { + util.selectiveExtend(["updateTime", "updateGroup", "add", "remove"], this.options.editable, options.editable); + } } - if (isMajor && this.options.showMajorLabels) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; + // callback functions + var addCallback = (function (name) { + var fn = options[name]; + if (fn) { + if (!(fn instanceof Function)) { + throw new Error("option " + name + " must be a function " + name + "(item, callback)"); } - this._repaintMajorText(x, step.getLabelMajor(), orientation, className); + this.options[name] = fn; } - 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 + }).bind(this); + ["onAdd", "onUpdate", "onRemove", "onMove", "onMoving"].forEach(addCallback); - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText, orientation, className); - } + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); } - - // Cleanup leftover DOM elements from the redundant list - util.forEach(this.dom.redundant, function (arr) { - while (arr.length) { - var elem = arr.pop(); - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } - } - }); }; /** - * Create a minor label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className - * @private + * Mark the ItemSet dirty so it will refresh everything with next redraw. + * Optionally, all items can be marked as dirty and be refreshed. + * @param {{refreshItems: boolean}} [options] */ - TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); + ItemSet.prototype.markDirty = function (options) { + this.groupIds = []; + this.stackDirty = true; - if (!label) { - // create new label - var content = document.createTextNode(""); - label = document.createElement("div"); - label.appendChild(content); - this.dom.foreground.appendChild(label); + if (options && options.refreshItems) { + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); } - this.dom.minorTexts.push(label); - - label.childNodes[0].nodeValue = text; - - label.style.top = orientation == "top" ? this.props.majorLabelHeight + "px" : "0"; - label.style.left = x + "px"; - label.className = "text minor " + className; - //label.title = title; // TODO: this is a heavy operation }; /** - * Create a Major label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @param {String} className - * @private + * Destroy the ItemSet */ - TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); - - if (!label) { - // create label - var content = document.createTextNode(text); - label = document.createElement("div"); - label.appendChild(content); - this.dom.foreground.appendChild(label); - } - this.dom.majorTexts.push(label); + ItemSet.prototype.destroy = function () { + this.hide(); + this.setItems(null); + this.setGroups(null); - label.childNodes[0].nodeValue = text; - label.className = "text major " + className; - //label.title = title; // TODO: this is a heavy operation + this.hammer = null; - label.style.top = orientation == "top" ? "0" : this.props.minorLabelHeight + "px"; - label.style.left = x + "px"; + this.body = null; + this.conversion = null; }; /** - * 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 + * Hide the component from the DOM */ - 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); + ItemSet.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); } - this.dom.lines.push(line); - var props = this.props; - if (orientation == "top") { - line.style.top = props.majorLabelHeight + "px"; - } else { - line.style.top = this.body.domProps.top.height + "px"; + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); } - line.style.height = props.minorLineHeight + "px"; - line.style.left = x - props.minorLineWidth / 2 + "px"; - - line.className = "grid vertical minor " + className; - return line; + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + } }; /** - * 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 - */ - 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); + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed + */ + ItemSet.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); } - this.dom.lines.push(line); - var props = this.props; - if (orientation == "top") { - line.style.top = "0"; - } else { - line.style.top = this.body.domProps.top.height + "px"; + // show axis with dots + if (!this.dom.axis.parentNode) { + this.body.dom.backgroundVertical.appendChild(this.dom.axis); } - line.style.left = x - props.majorLineWidth / 2 + "px"; - line.style.height = props.majorLineHeight + "px"; - - line.className = "grid vertical major " + className; - return line; + // show labelset containing labels + if (!this.dom.labelSet.parentNode) { + this.body.dom.left.appendChild(this.dom.labelSet); + } }; /** - * 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 + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {string[] | string} [ids] An array with zero or more id's of the items to be + * selected, or a single item id. If ids is undefined + * or an empty array, all items will be unselected. */ - TimeAxis.prototype._calculateCharSize = function () { - // Note: We calculate char size with every redraw. Size may change, for - // example when any of the timelines parents had display:none for example. + ItemSet.prototype.setSelection = function (ids) { + var i, ii, id, item; - // 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 (ids == undefined) ids = []; + if (!Array.isArray(ids)) ids = [ids]; - this.dom.measureCharMinor.appendChild(document.createTextNode("0")); - this.dom.foreground.appendChild(this.dom.measureCharMinor); + // unselect currently selected items + for (i = 0, ii = this.selection.length; i < ii; i++) { + id = this.selection[i]; + item = this.items[id]; + if (item) item.unselect(); } - this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; - this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; - - // determine the char width and height on the major axis - if (!this.dom.measureCharMajor) { - this.dom.measureCharMajor = document.createElement("DIV"); - this.dom.measureCharMajor.className = "text major measure"; - this.dom.measureCharMajor.style.position = "absolute"; - this.dom.measureCharMajor.appendChild(document.createTextNode("0")); - this.dom.foreground.appendChild(this.dom.measureCharMajor); + // select items + this.selection = []; + for (i = 0, ii = ids.length; i < ii; i++) { + id = ids[i]; + item = this.items[id]; + if (item) { + this.selection.push(id); + item.select(); + } } - this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; - this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; }; - module.exports = TimeAxis; - -/***/ }, -/* 36 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; + /** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ + ItemSet.prototype.getSelection = function () { + return this.selection.concat([]); + }; - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(45); - var keycharm = __webpack_require__(63); - var util = __webpack_require__(1); - var hammerUtil = __webpack_require__(47); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var dotparser = __webpack_require__(42); - var gephiParser = __webpack_require__(43); - var Groups = __webpack_require__(38); - var Images = __webpack_require__(39); - var Node = __webpack_require__(40); - var Edge = __webpack_require__(37); - var Popup = __webpack_require__(41); - var MixinLoader = __webpack_require__(52); - var Activator = __webpack_require__(53); - var locales = __webpack_require__(54); + /** + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items + */ + ItemSet.prototype.getVisibleItems = function () { + var range = this.body.range.getRange(); + var left = this.body.util.toScreen(range.start); + var right = this.body.util.toScreen(range.end); - // Load custom shapes into CanvasRenderingContext2D - __webpack_require__(55); + var ids = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + var group = this.groups[groupId]; + var rawVisibleItems = group.visibleItems; - var PhysicsEngine = __webpack_require__(56).PhysicsEngine; - var ClusterEngine = __webpack_require__(57).ClusterEngine; - var CanvasRenderer = __webpack_require__(58).CanvasRenderer; - var Canvas = __webpack_require__(59).Canvas; - var View = __webpack_require__(60).View; - var TouchEventHandler = __webpack_require__(61).TouchEventHandler; + // filter the "raw" set with visibleItems into a set which is really + // visible by pixels + for (var i = 0; i < rawVisibleItems.length; i++) { + var item = rawVisibleItems[i]; + // TODO: also check whether visible vertically + if (item.left < right && item.left + item.width > left) { + ids.push(item.id); + } + } + } + } + return ids; + }; /** - * @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 + * Deselect a selected item + * @param {String | Number} id + * @private */ - function Network(container, data, options) { - if (!(this instanceof Network)) { - throw new SyntaxError("Constructor must be called with the new operator"); + ItemSet.prototype._deselect = function (id) { + var selection = this.selection; + for (var i = 0, ii = selection.length; i < ii; i++) { + if (selection[i] == id) { + // non-strict comparison! + selection.splice(i, 1); + break; + } } + }; - this._initializeMixinLoaders(); - + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + ItemSet.prototype.redraw = function () { + var margin = this.options.margin, + range = this.body.range, + asSize = util.option.asSize, + options = this.options, + orientation = options.orientation, + resized = false, + frame = this.dom.frame, + editable = options.editable.updateTime || options.editable.updateGroup; - // render and calculation settings - this.initializing = true; + // recalculate absolute position (before redrawing groups) + this.props.top = this.body.domProps.top.height + this.body.domProps.border.top; + this.props.left = this.body.domProps.left.width + this.body.domProps.border.left; - this.triggerFunctions = { add: null, edit: null, editEdge: null, connect: null, del: null }; + // update class name + frame.className = "itemset" + (editable ? " editable" : ""); - var 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); - } - }; + // reorder the groups (if needed) + resized = this._orderGroups() || resized; - // set constant values - this.defaultOptions = { - nodes: { - customScalingFunction: customScalingFunction, - mass: 1, - radiusMin: 10, - radiusMax: 30, - radius: 10, - shape: "ellipse", - image: undefined, - widthMin: 16, // px - widthMax: 64, // px - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - fontFill: undefined, - fontStrokeWidth: 0, // px - fontStrokeColor: "#ffffff", - fontDrawThreshold: 3, - scaleFontWithValue: false, - fontSizeMin: 14, - fontSizeMax: 30, - fontSizeMaxVisible: 30, - value: 1, - level: -1, - color: { - border: "#2B7CE9", - background: "#97C2FC", - highlight: { - border: "#2B7CE9", - background: "#D2E5FF" - }, - hover: { - border: "#2B7CE9", - background: "#D2E5FF" - } - }, - group: undefined, - borderWidth: 1, - borderWidthSelected: undefined - }, - edges: { - customScalingFunction: customScalingFunction, - widthMin: 1, // - widthMax: 15, // - width: 1, - widthSelectionMultiplier: 2, - hoverWidth: 1.5, - value: 1, - style: "line", - color: { - color: "#848484", - highlight: "#848484", - hover: "#848484" - }, - opacity: 1, - fontColor: "#343434", - fontSize: 14, // px - fontFace: "arial", - fontFill: "white", - fontStrokeWidth: 0, // px - fontStrokeColor: "white", - labelAlignment: "horizontal", - arrowScaleFactor: 1, - dash: { - length: 10, - gap: 5, - altLength: undefined - }, - inheritColor: "from", // to, from, false, true (== from) - useGradients: false // release in 4.0 - }, - configurePhysics: false, - navigation: { - enabled: false - }, - keyboard: { - enabled: false, - speed: { x: 10, y: 10, zoom: 0.02 }, - bindToWindow: true - }, - dataManipulation: { - enabled: false, - initiallyVisible: false - }, - hierarchicalLayout: { - enabled: false, - levelSeparation: 150, - nodeSpacing: 100, - direction: "UD", // UD, DU, LR, RL - layout: "hubsize" // hubsize, directed - }, + // check whether zoomed (in that case we need to re-stack everything) + // TODO: would be nicer to get this as a trigger from Range + var visibleInterval = range.end - range.start; + var zoomed = visibleInterval != this.lastVisibleInterval || this.props.width != this.props.lastWidth; + if (zoomed) this.stackDirty = true; + this.lastVisibleInterval = visibleInterval; + this.props.lastWidth = this.props.width; - smoothCurves: { - enabled: true, - dynamic: true, - type: "continuous", - roundness: 0.5 - }, - locale: "en", - locales: locales, - tooltip: { - delay: 300, - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - color: { - border: "#666", - background: "#FFFFC6" - } - }, - dragNetwork: true, - dragNodes: true, - zoomable: true, - hover: false, - hideEdgesOnDrag: false, - hideNodesOnDrag: false, - width: "100%", - height: "100%", - selectable: true, - useDefaultGroups: true + var restack = this.stackDirty; + var firstGroup = this._firstGroup(); + var firstMargin = { + item: margin.item, + axis: margin.axis }; - this.constants = util.extend({}, this.defaultOptions); - - // containers for nodes and edges - this.body = { - nodes: {}, - nodeIndices: [], - supportNodes: {}, - supportNodeIndices: [], - edges: {}, - data: { - nodes: null, // A DataSet or DataView - edges: null // A DataSet or DataView - }, - functions: { - createNode: this._createNode.bind(this), - createEdge: this._createEdge.bind(this) - }, - 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 + var nonFirstMargin = { + item: margin.item, + axis: margin.item.vertical / 2 }; + var height = 0; + var minHeight = margin.axis + margin.item.vertical; - // modules - this.view = new View(this.body); - this.renderer = new CanvasRenderer(this.body); - this.clustering = new ClusterEngine(this.body); - this.physics = new PhysicsEngine(this.body); - this.canvas = new Canvas(this.body); - this.touchHandler = new TouchEventHandler(this.body); - - this.renderer.setCanvas(this.canvas); - this.view.setCanvas(this.canvas); - this.touchHandler.setCanvas(this.canvas); - - this.hoverObj = { nodes: {}, edges: {} }; - this.controlNodesActive = false; - this.navigationHammers = []; - this.manipulationHammers = []; + // redraw the background group + this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); - // Node variables - var me = this; - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function (status) { - me._requestRedraw(); + // redraw all regular groups + util.forEach(this.groups, function (group) { + var groupMargin = group == firstGroup ? firstMargin : nonFirstMargin; + var groupResized = group.redraw(range, groupMargin, restack); + resized = groupResized || resized; + height += group.height; }); + height = Math.max(height, minHeight); + this.stackDirty = false; - // keyboard navigation variables - this.xIncrement = 0; - this.yIncrement = 0; - this.zoomIncrement = 0; - - // loading all the mixins: - // load the force calculation functions, grouped under the physics system. - //this._loadPhysicsSystem(); - // create a frame and canvas - // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) - // load the selection system. (mandatory, required by Network) - this._loadSelectionSystem(); - // load the selection system. (mandatory, required by Network) - //this._loadHierarchySystem(); - - // apply options - this.setOptions(options); - - // other vars - this.cachedFunctions = {}; - this.startedStabilization = false; - this.stabilized = false; - this.stabilizationIterations = null; - this.draggingNodes = false; + // update frame height + frame.style.height = asSize(height); - // position and scale variables and objects + // calculate actual size + this.props.width = frame.offsetWidth; + this.props.height = height; - this.pointerPosition = { x: 0, y: 0 }; // coordinates of the bottom right of the canvas. they will be set during _redraw - this.scale = 1; // defining the global scale variable in the constructor + // reposition axis + this.dom.axis.style.top = asSize(orientation == "top" ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); + this.dom.axis.style.left = "0"; - // create event listeners used to subscribe on the DataSets of the nodes and edges - this.nodesListeners = { - add: function (event, params) { - me._addNodes(params.items); - me.start(); - }, - update: function (event, params) { - me._updateNodes(params.items, params.data); - me.start(); - }, - remove: function (event, params) { - me._removeNodes(params.items); - me.start(); - } - }; - this.edgesListeners = { - add: function (event, params) { - me._addEdges(params.items); - me.start(); - }, - update: function (event, params) { - me._updateEdges(params.items); - me.start(); - }, - remove: function (event, params) { - me._removeEdges(params.items); - me.start(); - } - }; + // check if this component is resized + resized = this._isResized() || resized; + return resized; + }; - // properties for the animation - this.moving = true; - this.renderTimer = undefined; // Scheduling function. Is definded in this.start(); + /** + * Get the first group, aligned with the axis + * @return {Group | null} firstGroup + * @private + */ + ItemSet.prototype._firstGroup = function () { + var firstGroupIndex = this.options.orientation == "top" ? 0 : this.groupIds.length - 1; + var firstGroupId = this.groupIds[firstGroupIndex]; + var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; - // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data, this.constants.hierarchicalLayout.enabled); + return firstGroup || null; + }; - // hierarchical layout - if (this.constants.hierarchicalLayout.enabled == true) { - this._setupHierarchicalLayout(); - } else { - // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here. - if (this.constants.stabilize == false) { - this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); - } - } + /** + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. + * @protected + */ + ItemSet.prototype._updateUngrouped = function () { + var ungrouped = this.groups[UNGROUPED]; + var background = this.groups[BACKGROUND]; + var item, itemId; - if (this.constants.stabilize == false) { - this.initializing = false; - } + if (this.groupsData) { + // remove the group holding all ungrouped items + if (ungrouped) { + ungrouped.hide(); + delete this.groups[UNGROUPED]; - var me = this; - // this event will trigger a rebuilding of the cache of colors, nodes etc. - this.on("_dataChanged", function () { - me._updateNodeIndexList(); - me.physics._updateCalculationNodes(); - me._markAllEdgesAsDirty(); - if (me.initializing !== true) { - me.moving = true; - me.start(); + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + item.parent && item.parent.remove(item); + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + group && group.add(item) || item.hide(); + } + } } - }); - - this.on("_newEdgesCreated", this._createBezierNodes.bind(this)); - //this.on("stabilizationIterationsDone", function () {me.initializing = false; me.start();}.bind(this)); - } - - // Extend Network with an Emitter mixin - Emitter(Network.prototype); - - - Network.prototype._createNode = function (properties) { - return new Node(properties, this.images, this.groups, this.constants); - }; - - Network.prototype._createEdge = function (properties) { - return new Edge(properties, this.body, this.constants); - }; - + } else { + // create a group holding all (unfiltered) items + if (!ungrouped) { + var id = null; + var data = null; + ungrouped = new Group(id, data, this); + this.groups[UNGROUPED] = ungrouped; + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + ungrouped.add(item); + } + } + ungrouped.show(); + } + } + }; /** - * Update the this.body.nodeIndices with the most recent node index list - * @private + * Get the element for the labelset + * @return {HTMLElement} labelSet */ - Network.prototype._updateNodeIndexList = function () { - this.body.supportNodeIndices = Object.keys(this.body.supportNodes); - this.body.nodeIndices = Object.keys(this.body.nodes); + ItemSet.prototype.getLabelSet = function () { + return this.dom.labelSet; }; - /** - * 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. + * Set items + * @param {vis.DataSet | null} items */ - Network.prototype.setData = function (data, disableStart) { - if (disableStart === undefined) { - disableStart = false; - } + ItemSet.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; - // unselect all to ensure no selections from old data are carried over. - this._unselectAll(true); + // 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"); + } - // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added. - this.initializing = true; + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - 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."); + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); } - // clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button. - if (this.constants.dataManipulation.enabled == true) { - this._createManipulatorBar(); - } + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - // 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._setNodes(data && data.nodes); - this._setEdges(data && data.edges); - } + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); - if (disableStart == false) { - if (this.constants.hierarchicalLayout.enabled == true) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } else { - // find a stable position or start animating to a stable position - this.physics.startSimulation(); - } - } else { - this.initializing = false; + // update the group holding all ungrouped items + this._updateUngrouped(); } }; /** - * Set options - * @param {Object} options + * Get the current items + * @returns {vis.DataSet | null} */ - Network.prototype.setOptions = function (options) { - if (options) { - var prop; - var fields = ["nodes", "edges", "smoothCurves", "hierarchicalLayout", "navigation", "keyboard", "dataManipulation", "onAdd", "onEdit", "onEditEdge", "onConnect", "onDelete", "clickToUse"]; - // extend all but the values in fields - util.selectiveNotDeepExtend(fields, this.constants, options); - util.selectiveNotDeepExtend(["color"], this.constants.nodes, options.nodes); - util.selectiveNotDeepExtend(["color", "length"], this.constants.edges, options.edges); - - this.groups.useDefaultGroups = this.constants.useDefaultGroups; + ItemSet.prototype.getItems = function () { + return this.itemsData; + }; - this.physics.setOptions(options.physics); - this.canvas.setOptions(this.constants); + /** + * Set groups + * @param {vis.DataSet} groups + */ + ItemSet.prototype.setGroups = function (groups) { + var me = this, + ids; + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - if (options.onAdd) { - this.triggerFunctions.add = options.onAdd; - } - if (options.onEdit) { - this.triggerFunctions.edit = options.onEdit; - } - if (options.onEditEdge) { - this.triggerFunctions.editEdge = options.onEditEdge; - } - if (options.onConnect) { - this.triggerFunctions.connect = options.onConnect; - } - if (options.onDelete) { - this.triggerFunctions.del = options.onDelete; - } + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - util.mergeOptions(this.constants, options, "smoothCurves"); - util.mergeOptions(this.constants, options, "hierarchicalLayout"); - util.mergeOptions(this.constants, options, "clustering"); - util.mergeOptions(this.constants, options, "navigation"); - util.mergeOptions(this.constants, options, "keyboard"); - util.mergeOptions(this.constants, options, "dataManipulation"); + // replace the dataset + if (!groups) { + this.groupsData = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + this.groupsData = groups; + } else { + throw new TypeError("Data must be an instance of DataSet or DataView"); + } + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); - if (options.dataManipulation) { - this.editMode = this.constants.dataManipulation.initiallyVisible; - } + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } + // update the group holding all ungrouped items + this._updateUngrouped(); - // TODO: work out these options and document them - if (options.edges) { - if (options.edges.color !== undefined) { - if (util.isString(options.edges.color)) { - this.constants.edges.color = {}; - this.constants.edges.color.color = options.edges.color; - this.constants.edges.color.highlight = options.edges.color; - this.constants.edges.color.hover = options.edges.color; - } else { - if (options.edges.color.color !== undefined) { - this.constants.edges.color.color = options.edges.color.color; - } - if (options.edges.color.highlight !== undefined) { - this.constants.edges.color.highlight = options.edges.color.highlight; - } - if (options.edges.color.hover !== undefined) { - this.constants.edges.color.hover = options.edges.color.hover; - } - } - this.constants.edges.inheritColor = false; - } + // update the order of all items in each group + this._order(); - if (!options.edges.fontColor) { - if (options.edges.color !== undefined) { - if (util.isString(options.edges.color)) { - this.constants.edges.fontColor = options.edges.color; - } else if (options.edges.color.color !== undefined) { - this.constants.edges.fontColor = options.edges.color.color; - } - } - } - } + this.body.emitter.emit("change", { queue: true }); + }; - if (options.nodes) { - if (options.nodes.color) { - var newColorObj = util.parseColor(options.nodes.color); - this.constants.nodes.color.background = newColorObj.background; - this.constants.nodes.color.border = newColorObj.border; - this.constants.nodes.color.highlight.background = newColorObj.highlight.background; - this.constants.nodes.color.highlight.border = newColorObj.highlight.border; - this.constants.nodes.color.hover.background = newColorObj.hover.background; - this.constants.nodes.color.hover.border = newColorObj.hover.border; - } - } - if (options.groups) { - for (var groupname in options.groups) { - if (options.groups.hasOwnProperty(groupname)) { - var group = options.groups[groupname]; - this.groups.add(groupname, group); - } - } - } + /** + * Get the current groups + * @returns {vis.DataSet | null} groups + */ + ItemSet.prototype.getGroups = function () { + return this.groupsData; + }; - if (options.tooltip) { - for (prop in options.tooltip) { - if (options.tooltip.hasOwnProperty(prop)) { - this.constants.tooltip[prop] = options.tooltip[prop]; - } - } - if (options.tooltip.color) { - this.constants.tooltip.color = util.parseColor(options.tooltip.color); - } - } + /** + * Remove an item by its id + * @param {String | Number} id + */ + ItemSet.prototype.removeItem = function (id) { + var item = this.itemsData.get(id), + dataset = this.itemsData.getDataSet(); - if ("clickToUse" in options) { - if (options.clickToUse) { - if (!this.activator) { - this.activator = new Activator(this.frame); - this.activator.on("change", this._createKeyBinds.bind(this)); - } - } else { - if (this.activator) { - this.activator.destroy(); - delete this.activator; - } + if (item) { + // confirm deletion + this.options.onRemove(item, function (item) { + if (item) { + // remove by id here, it is possible that an item has no id defined + // itself, so better not delete by the item itself + dataset.remove(id); } - } - - if (options.labels) { - throw new Error("Option \"labels\" is deprecated. Use options \"locale\" and \"locales\" instead."); - } - - // (Re)loading the mixins that can be enabled or disabled in the options. - // load the force calculation functions, grouped under the physics system. - // load the navigation system. - //this._loadNavigationControls(); - //// load the data manipulation system - //this._loadManipulationSystem(); - //// configure the smooth curves - //this._configureSmoothCurves(); - - // bind hammer - this.canvas._bindHammer(); - - // bind keys. If disabled, this will not do anything; - //this._createKeyBinds(); - - this._markAllEdgesAsDirty(); - this.canvas.setSize(this.constants.width, this.constants.height); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - - if (this.initializing !== true) { - this.moving = true; - this.start(); - } + }); } }; + /** + * Get the time of an item based on it's data and options.type + * @param {Object} itemData + * @returns {string} Returns the type + * @private + */ + ItemSet.prototype._getType = function (itemData) { + return itemData.type || this.options.type || (itemData.end ? "range" : "box"); + }; /** - * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin + * Get the group id for an item + * @param {Object} itemData + * @returns {string} Returns the groupId * @private */ - Network.prototype._createKeyBinds = function () { - return; - - //var me = this; - //if (this.keycharm !== undefined) { - // this.keycharm.destroy(); - //} - // - //if (this.constants.keyboard.bindToWindow == true) { - // this.keycharm = keycharm({container: window, preventDefault: false}); - //} - //else { - // this.keycharm = keycharm({container: this.frame, preventDefault: false}); - //} - // - //this.keycharm.reset(); - // - //if (this.constants.keyboard.enabled && this.isActive()) { - // this.keycharm.bind("up", this._moveUp.bind(me) , "keydown"); - // this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup"); - // this.keycharm.bind("down", this._moveDown.bind(me) , "keydown"); - // this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup"); - // this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown"); - // this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup"); - // this.keycharm.bind("right",this._moveRight.bind(me), "keydown"); - // this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup"); - // this.keycharm.bind("=", this._zoomIn.bind(me), "keydown"); - // this.keycharm.bind("=", this._stopZoom.bind(me), "keyup"); - // this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown"); - // this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup"); - // this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown"); - // this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup"); - // this.keycharm.bind("-", this._zoomOut.bind(me), "keydown"); - // this.keycharm.bind("-", this._stopZoom.bind(me), "keyup"); - // this.keycharm.bind("[", this._zoomIn.bind(me), "keydown"); - // this.keycharm.bind("[", this._stopZoom.bind(me), "keyup"); - // this.keycharm.bind("]", this._zoomOut.bind(me), "keydown"); - // this.keycharm.bind("]", this._stopZoom.bind(me), "keyup"); - // this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown"); - // this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup"); - // this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); - // this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); - //} - // - //if (this.constants.dataManipulation.enabled == true) { - // this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); - // this.keycharm.bind("delete",this._deleteSelected.bind(me)); - //} + ItemSet.prototype._getGroupId = function (itemData) { + var type = this._getType(itemData); + if (type == "background" && itemData.group == undefined) { + return BACKGROUND; + } else { + return this.groupsData ? itemData.group : UNGROUPED; + } }; /** - * 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; + * Handle updated items + * @param {Number[]} ids + * @protected */ - Network.prototype.destroy = function () { - this.start = function () {}; - this.redraw = function () {}; - this.renderTimer = false; + ItemSet.prototype._onUpdate = function (ids) { + var me = this; - // cleanup physicsConfiguration if it exists - this._cleanupPhysicsConfiguration(); + ids.forEach(function (id) { + var itemData = me.itemsData.get(id, me.itemOptions); + var item = me.items[id]; + var type = me._getType(itemData); - // remove keybindings - this.keycharm.reset(); + var constructor = ItemSet.types[type]; - // clear hammer bindings - this.hammer.dispose(); + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, delete the item and recreate it + me._removeItem(item); + item = null; + } else { + me._updateItem(item, itemData); + } + } - // clear events - this.off(); + if (!item) { + // create item + if (constructor) { + item = new constructor(itemData, me.conversion, me.options); + item.id = id; // TODO: not so nice setting id afterwards + me._addItem(item); + } else if (type == "rangeoverflow") { + // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day + throw new TypeError("Item type \"rangeoverflow\" is deprecated. Use css styling instead: " + ".vis.timeline .item.range .content {overflow: visible;}"); + } else { + throw new TypeError("Unknown item type \"" + type + "\""); + } + } + }); - this._recursiveDOMDelete(this.containerElement); + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit("change", { queue: true }); }; - Network.prototype._recursiveDOMDelete = function (DOMobject) { - while (DOMobject.hasChildNodes() == true) { - this._recursiveDOMDelete(DOMobject.firstChild); - DOMobject.removeChild(DOMobject.firstChild); + /** + * Handle added items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; + + /** + * Handle removed items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onRemove = function (ids) { + var count = 0; + var me = this; + ids.forEach(function (id) { + var item = me.items[id]; + if (item) { + count++; + me._removeItem(item); + } + }); + + if (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit("change", { queue: true }); } }; /** - * 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 + * Update the order of item in all groups * @private */ - Network.prototype._checkShowPopup = function (pointer) { - var obj = { - left: this._XconvertDOMtoCanvas(pointer.x), - top: this._YconvertDOMtoCanvas(pointer.y), - right: this._XconvertDOMtoCanvas(pointer.x), - bottom: this._YconvertDOMtoCanvas(pointer.y) - }; + ItemSet.prototype._order = function () { + // reorder the items in all groups + // TODO: optimization: only reorder groups affected by the changed items + util.forEach(this.groups, function (group) { + group.order(); + }); + }; - var id; - var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; - var nodeUnderCursor = false; - var popupType = "node"; + /** + * Handle updated groups + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onUpdateGroups = function (ids) { + this._onAddGroups(ids); + }; - if (this.popupObj == undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodes = this.body.nodes; - var overlappingNodes = []; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - if (node.isOverlappingWith(obj)) { - if (node.getTitle() !== undefined) { - overlappingNodes.push(id); - } - } - } - } + /** + * Handle changed groups (added or updated) + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onAddGroups = function (ids) { + var me = this; - 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 = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; - // if you hover over a node, the title of the edge is not supposed to be shown. - nodeUnderCursor = true; - } - } + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; - if (this.popupObj === undefined && nodeUnderCursor == false) { - // search the edges for overlap - var edges = this.body.edges; - var overlappingEdges = []; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected === true && edge.getTitle() !== undefined && edge.isOverlappingWith(obj)) { - overlappingEdges.push(id); - } + if (!group) { + // check for reserved ids + if (id == UNGROUPED || id == BACKGROUND) { + throw new Error("Illegal group id. " + id + " is a reserved id."); } - } - if (overlappingEdges.length > 0) { - this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; - popupType = "edge"; - } - } + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); - if (this.popupObj) { - // show popup message window - if (this.popupObj.id != previousPopupObjId) { - if (this.popup === undefined) { - this.popup = new Popup(this.frame, this.constants.tooltip); - } + group = new Group(id, groupData, me); + me.groups[id] = group; - this.popup.popupTargetType = popupType; - this.popup.popupTargetId = this.popupObj.id; + // add items with this groupId to the new group + for (var itemId in me.items) { + if (me.items.hasOwnProperty(itemId)) { + var item = me.items[itemId]; + if (item.data.group == id) { + group.add(item); + } + } + } - // 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(); + group.order(); + group.show(); + } else { + // update group + group.setData(groupData); } - } - }; + }); + this.body.emitter.emit("change", { queue: true }); + }; /** - * 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 + * Handle removed groups + * @param {Number[]} ids * @private */ - Network.prototype._checkHidePopup = function (pointer) { - var pointerObj = { - left: this._XconvertDOMtoCanvas(pointer.x), - top: this._YconvertDOMtoCanvas(pointer.y), - right: this._XconvertDOMtoCanvas(pointer.x), - bottom: this._YconvertDOMtoCanvas(pointer.y) - }; + ItemSet.prototype._onRemoveGroups = function (ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; - var stillOnObj = false; - if (this.popup.popupTargetType == "node") { - stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); - if (stillOnObj === true) { - var overNode = this._getNodeAt(pointer); - stillOnObj = overNode.id == this.popup.popupTargetId; - } - } else { - if (this._getNodeAt(pointer) === null) { - stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + if (group) { + group.hide(); + delete groups[id]; } - } + }); + this.markDirty(); - if (stillOnObj === false) { - this.popupObj = undefined; - this.popup.hide(); - } + this.body.emitter.emit("change", { queue: true }); }; - /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * Reorder the groups if needed + * @return {boolean} changed * @private */ - Network.prototype._setNodes = function (nodes) { - 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); + ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder }); - } - // remove drawn nodes - this.body.nodes = {}; + var changed = !util.equalArray(groupIds, this.groupIds); + if (changed) { + // hide all groups, removes them from the DOM + var groups = this.groups; + groupIds.forEach(function (groupId) { + groups[groupId].hide(); + }); - if (this.body.data.nodes) { - // subscribe to new dataset - var me = this; - util.forEach(this.nodesListeners, function (callback, event) { - me.body.data.nodes.on(event, callback); - }); + // show the groups again, attach them to the DOM in correct order + groupIds.forEach(function (groupId) { + groups[groupId].show(); + }); - // draw all new nodes - var ids = this.body.data.nodes.getIds(); - this._addNodes(ids); + this.groupIds = groupIds; + } + + return changed; + } else { + return false; } - this._updateSelection(); }; /** - * Add nodes - * @param {Number[] | String[]} ids + * Add a new item + * @param {Item} item * @private */ - Network.prototype._addNodes = function (ids) { - var id; - for (var i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - var data = this.body.data.nodes.get(id); - var node = new Node(data, this.images, this.groups, this.constants); - this.body.nodes[id] = node; // note: this may replace an existing node - if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { - var radius = 10 * 0.1 * ids.length + 10; - var angle = 2 * Math.PI * Math.random(); - if (node.xFixed == false) { - node.x = radius * Math.cos(angle); - } - if (node.yFixed == false) { - node.y = radius * Math.sin(angle); - } - } - this.moving = true; - } + ItemSet.prototype._addItem = function (item) { + this.items[item.id] = item; - this._updateNodeIndexList(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this.physics._updateCalculationNodes(); - this._reconnectEdges(); - this._updateValueRange(this.body.nodes); + // add to group + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); }; /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids + * Update an existing item + * @param {Item} item + * @param {Object} itemData * @private */ - Network.prototype._updateNodes = function (ids, changedData) { - var nodes = this.body.nodes; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = changedData[i]; - if (node) { - // update node - node.setProperties(data, this.constants); - } else { - // create node - node = new Node(properties, this.images, this.groups, this.constants); - nodes[id] = node; - } - } - this.moving = true; - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this._updateNodeIndexList(); - this._updateValueRange(nodes); - this._markAllEdgesAsDirty(); - }; + ItemSet.prototype._updateItem = function (item, itemData) { + var oldGroupId = item.data.group; + + // update the items data (will redraw the item when displayed) + item.setData(itemData); + // update group + if (oldGroupId != item.data.group) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); - Network.prototype._markAllEdgesAsDirty = function () { - for (var edgeId in this.body.edges) { - this.body.edges[edgeId].colorDirty = true; + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); } }; /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids + * Delete an item from the ItemSet: remove it from the DOM, from the map + * with items, and from the map with visible items, and from the selection + * @param {Item} item * @private */ - Network.prototype._removeNodes = function (ids) { - var nodes = this.body.nodes; + ItemSet.prototype._removeItem = function (item) { + // remove from DOM + item.hide(); - // remove from selection - for (var i = 0, len = ids.length; i < len; i++) { - if (this.selectionObj.nodes[ids[i]] !== undefined) { - this.body.nodes[ids[i]].unselect(); - this._removeFromSelection(this.body.nodes[ids[i]]); - } - } + // remove from items + delete this.items[item.id]; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - delete nodes[id]; - } + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); + // remove from group + item.parent && item.parent.remove(item); + }; + /** + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} + * @private + */ + ItemSet.prototype._constructByEndArray = function (array) { + var endArray = []; - this._updateNodeIndexList(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof RangeItem) { + endArray.push(array[i]); + } } - this.physics._updateCalculationNodes(); - this._reconnectEdges(); - this._updateSelection(); - this._updateValueRange(nodes); + return endArray; }; /** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private + * Register the clicked item on touch, before dragStart is initiated. + * + * dragStart is initiated from a mousemove event, AFTER the mouse/touch is + * already moving. Therefore, the mouse/touch can sometimes be above an other + * DOM element than the item itself. + * + * @param {Event} event * @private */ - Network.prototype._setEdges = function (edges) { - var oldEdgesData = this.body.data.edges; + ItemSet.prototype._onTouch = function (event) { + // store the touched item, used in _onDragStart + this.touchParams.item = ItemSet.itemFromTarget(event); + this.touchParams.dragLeftItem = event.target.dragLeftItem || false; + this.touchParams.dragRightItem = event.target.dragRightItem || false; + this.touchParams.itemProps = null; + }; - 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"); + /** + * Start dragging the selected events + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragStart = function (event) { + if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { + return; } - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.off(event, callback); - }); - } + var item = this.touchParams.item || null; + var me = this; + var props; - // remove drawn edges - this.body.edges = {}; + if (item && item.selected) { + var dragLeftItem = this.touchParams.dragLeftItem; + var dragRightItem = this.touchParams.dragRightItem; - if (this.body.data.edges) { - // subscribe to new dataset - var me = this; - util.forEach(this.edgesListeners, function (callback, event) { - me.body.data.edges.on(event, callback); - }); + if (dragLeftItem) { + props = { + item: dragLeftItem, + initialX: event.center.x + }; - // draw all new nodes - var ids = this.body.data.edges.getIds(); - this._addEdges(ids); - } + if (me.options.editable.updateTime) { + props.start = item.data.start.valueOf(); + } + if (me.options.editable.updateGroup) { + if ("group" in item.data) props.group = item.data.group; + } - this._reconnectEdges(); - }; + this.touchParams.itemProps = [props]; + } else if (dragRightItem) { + props = { + item: dragRightItem, + initialX: event.center.x + }; - /** - * Add edges - * @param {Number[] | String[]} ids - * @private - */ - Network.prototype._addEdges = function (ids) { - var edges = this.body.edges, - edgesData = this.body.data.edges; + if (me.options.editable.updateTime) { + props.end = item.data.end.valueOf(); + } + if (me.options.editable.updateGroup) { + if ("group" in item.data) props.group = item.data.group; + } - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; + this.touchParams.itemProps = [props]; + } else { + this.touchParams.itemProps = this.getSelection().map(function (id) { + var item = me.items[id]; + var props = { + item: item, + initialX: event.center.x + }; - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); + if (me.options.editable.updateTime) { + if ("start" in item.data) { + props.start = item.data.start.valueOf(); + + if ("end" in item.data) { + // we store a duration here in order not to change the width + // of the item when moving it. + props.duration = item.data.end.valueOf() - props.start; + } + } + } + if (me.options.editable.updateGroup) { + if ("group" in item.data) props.group = item.data.group; + } + + return props; + }); } - var data = edgesData.get(id, { showInternalIds: true }); - edges[id] = new Edge(data, this.body, this.constants); - } - this.moving = true; - this._updateValueRange(edges); - this._createBezierNodes(); - this.physics._updateCalculationNodes(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); + event.stopPropagation(); + event.preventDefault(); } }; /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids + * Drag selected items + * @param {Event} event * @private */ - Network.prototype._updateEdges = function (ids) { - var edges = this.body.edges; - var edgesData = this.body.data.edges; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; + ItemSet.prototype._onDrag = function (event) { + event.preventDefault(); - var data = edgesData.get(id); - var edge = edges[id]; - if (edge) { - // update edge - edge.disconnect(); - edge.setProperties(data); - edge.connect(); - } else { - // create edge - edge = new Edge(data, this.body, this.constants); - this.body.edges[id] = edge; - } - } + if (this.touchParams.itemProps) { + var me = this; + var snap = this.options.snap || null; + var xOffset = this.body.dom.root.offsetLeft + this.body.domProps.left.width; + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); - this._createBezierNodes(); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); - } - this.moving = true; - this._updateValueRange(edges); + // move + this.touchParams.itemProps.forEach(function (props) { + var newProps = {}; + var current = me.body.util.toTime(event.center.x - xOffset); + var initial = me.body.util.toTime(props.initialX - xOffset); + var offset = current - initial; + + if ("start" in props) { + var start = new Date(props.start + offset); + newProps.start = snap ? snap(start, scale, step) : start; + } + + if ("end" in props) { + var end = new Date(props.end + offset); + newProps.end = snap ? snap(end, scale, step) : end; + } else if ("duration" in props) { + newProps.end = new Date(newProps.start.valueOf() + props.duration); + } + + if ("group" in props) { + // drag from one group to another + var group = me.groupFromTarget(event); + newProps.group = group && group.groupId; + } + + // confirm moving the item + var itemData = util.extend({}, props.item.data, newProps); + me.options.onMoving(itemData, function (itemData) { + if (itemData) { + me._updateItemProps(props.item, itemData); + } + }); + }); + + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit("change"); + + event.stopPropagation(); + } }; /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids + * Update an items properties + * @param {Item} item + * @param {Object} props Can contain properties start, end, and group. * @private */ - Network.prototype._removeEdges = function (ids) { - var edges = this.body.edges; - - // remove from selection - for (var i = 0, len = ids.length; i < len; i++) { - if (this.selectionObj.edges[ids[i]] !== undefined) { - edges[ids[i]].unselect(); - this._removeFromSelection(edges[ids[i]]); - } + ItemSet.prototype._updateItemProps = function (item, props) { + // TODO: copy all properties from props to item? (also new ones) + if ("start" in props) { + item.data.start = props.start; } - - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge) { - if (edge.via != null) { - delete this.body.supportNodes[edge.via.id]; - } - edge.disconnect(); - delete edges[id]; - } + if ("end" in props) { + item.data.end = props.end; + } else if ("duration" in props) { + item.data.end = new Date(props.start.valueOf() + props.duration); } - - this.moving = true; - this._updateValueRange(edges); - if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { - this._resetLevels(); - this._setupHierarchicalLayout(); + if ("group" in props && item.data.group != props.group) { + this._moveToGroup(item, props.group); } - this.physics._updateCalculationNodes(); }; /** - * Reconnect all edges + * Move an item to another group + * @param {Item} item + * @param {String | Number} groupId * @private */ - Network.prototype._reconnectEdges = function () { - var id, - nodes = this.body.nodes, - edges = this.body.edges; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } + ItemSet.prototype._moveToGroup = function (item, groupId) { + var group = this.groups[groupId]; + if (group && group.groupId != item.data.group) { + var oldGroup = item.parent; + oldGroup.remove(item); + oldGroup.order(); + group.add(item); + group.order(); - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } + item.data.group = group.groupId; } }; /** - * 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). + * End of dragging selected items + * @param {Event} event * @private */ - Network.prototype._updateValueRange = function (obj) { - var id; + ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + // prepare a change set for the changed items + var changes = [], + me = this, + dataset = this.itemsData.getDataSet(); - // 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; + var itemProps = this.touchParams.itemProps; + this.touchParams.itemProps = null; + itemProps.forEach(function (props) { + var id = props.item.id, + itemData = me.itemsData.get(id, me.itemOptions); + + var changed = false; + if ("start" in props.item.data) { + changed = props.start != props.item.data.start.valueOf(); + itemData.start = util.convert(props.item.data.start, dataset._options.type && dataset._options.type.start || "Date"); + } + if ("end" in props.item.data) { + changed = changed || props.end != props.item.data.end.valueOf(); + itemData.end = util.convert(props.item.data.end, dataset._options.type && dataset._options.type.end || "Date"); + } + if ("group" in props.item.data) { + changed = changed || props.group != props.item.data.group; + itemData.group = props.item.data.group; } - } - } - // 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); + // only apply changes when start or end is actually changed + if (changed) { + me.options.onMove(itemData, function (itemData) { + if (itemData) { + // apply changes + itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) + changes.push(itemData); + } else { + // restore original values + me._updateItemProps(props.item, props); + + me.stackDirty = true; // force re-stacking of all items next redraw + me.body.emitter.emit("change"); + } + }); } + }); + + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); } + + event.stopPropagation(); } }; - /** - * Set the translation of the network - * @param {Number} offsetX Horizontal offset - * @param {Number} offsetY Vertical offset + * Handle selecting/deselecting an item when tapping it + * @param {Event} event * @private */ - Network.prototype._setTranslation = function (offsetX, offsetY) { - if (this.translation === undefined) { - this.translation = { - x: 0, - y: 0 - }; - } + ItemSet.prototype._onSelectItem = function (event) { + if (!this.options.selectable) return; - if (offsetX !== undefined) { - this.translation.x = offsetX; - } - if (offsetY !== undefined) { - this.translation.y = offsetY; + var ctrlKey = event.srcEvent && event.srcEvent.ctrlKey; + var shiftKey = event.srcEvent && event.srcEvent.shiftKey; + if (ctrlKey || shiftKey) { + this._onMultiSelectItem(event); + return; } - this.emit("viewChanged"); - }; + var oldSelection = this.getSelection(); - /** - * Get the translation of the network - * @return {Object} translation An object with parameters x and y, both a number - * @private - */ - Network.prototype._getTranslation = function () { - return { - x: this.translation.x, - y: this.translation.y - }; - }; + var item = ItemSet.itemFromTarget(event); + var selection = item ? [item.id] : []; + this.setSelection(selection); - /** - * Scale the network - * @param {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._setScale = function (scale) { - this.scale = scale; - }; + var newSelection = this.getSelection(); - /** - * Get the current scale of the network - * @return {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._getScale = function () { - return this.scale; + // emit a select event, + // except when old selection is empty and new selection is still empty + if (newSelection.length > 0 || oldSelection.length > 0) { + this.body.emitter.emit("select", { + items: newSelection + }); + } }; - /** - * Move the network according to the keyboard presses. - * + * Handle creation and updates of an item on double tap + * @param event * @private */ - Network.prototype._handleNavigation = function () { - if (this.xIncrement != 0 || this.yIncrement != 0) { - var translation = this._getTranslation(); - this._setTranslation(translation.x + this.xIncrement, translation.y + this.yIncrement); - } - if (this.zoomIncrement != 0) { - var center = { - x: this.frame.canvas.clientWidth / 2, - y: this.frame.canvas.clientHeight / 2 - }; - this._zoom(this.scale * (1 + this.zoomIncrement), center); - } - }; - + ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; - /** - * Freeze the _animationStep - */ - Network.prototype.freezeSimulation = function (freeze) { - if (freeze == true) { - this.freezeSimulationEnabled = true; - this.moving = false; - } else { - this.freezeSimulationEnabled = false; - this.moving = true; - this.start(); - } - }; + var me = this, + snap = this.options.snap || null, + item = ItemSet.itemFromTarget(event); + if (item) { + // update item - /** - * This function cleans the support nodes if they are not needed and adds them when they are. - * - * @param {boolean} [disableStart] - * @private - */ - Network.prototype._configureSmoothCurves = function () { - var disableStart = arguments[0] === undefined ? true : arguments[0]; - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - this._createBezierNodes(); - // cleanup unused support nodes - for (var i = 0; i < this.body.supportNodeIndices.length; i++) { - var nodeId = this.body.supportNodeIndices[i]; - // delete support nodes for edges that have been deleted - if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) { - delete this.body.supportNodes[nodeId]; + // execute async handler to update the item (or cancel it) + var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset + this.options.onUpdate(itemData, function (itemData) { + if (itemData) { + me.itemsData.getDataSet().update(itemData); } - } + }); } else { - // delete the support nodes - this.body.supportNodes = {}; - for (var edgeId in this.body.edges) { - if (this.body.edges.hasOwnProperty(edgeId)) { - this.body.edges[edgeId].via = null; - } - } - } + // add item + var xAbs = util.getAbsoluteLeft(this.dom.frame); + var x = event.center.x - xAbs; + var start = this.body.util.toTime(x); + var scale = this.body.util.getScale(); + var step = this.body.util.getStep(); + var newItem = { + start: snap ? snap(start, scale, step) : start, + content: "new item" + }; - this._updateNodeIndexList(); - this.physics._updateCalculationNodes(); - if (!disableStart) { - this.moving = true; - this.start(); - } - }; + // when default type is a range, add a default end date to the new item + if (this.options.type === "range") { + var end = this.body.util.toTime(x + this.props.width / 5); + newItem.end = snap ? snap(end, scale, step) : end; + } + newItem[this.itemsData._fieldId] = util.randomUUID(); - /** - * 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. - * - * @private - */ - Network.prototype._createBezierNodes = function () { - var specificEdges = arguments[0] === undefined ? this.body.edges : arguments[0]; - if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { - for (var edgeId in specificEdges) { - if (specificEdges.hasOwnProperty(edgeId)) { - var edge = specificEdges[edgeId]; - if (edge.via == null) { - var nodeId = "edgeId:".concat(edge.id); - var node = new Node({ id: nodeId, - mass: 1, - shape: "circle", - image: "", - internalMultiplier: 1 - }, {}, {}, this.constants); - this.body.supportNodes[nodeId] = node; - edge.via = node; - edge.via.parentEdgeId = edge.id; - edge.positionBezierNode(); - } - } + var group = this.groupFromTarget(event); + if (group) { + newItem.group = group.groupId; } - this._updateNodeIndexList(); + + // 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? + } + }); } }; /** - * load the functions that load the mixins into the prototype. - * + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event * @private */ - Network.prototype._initializeMixinLoaders = function () { - for (var mixin in MixinLoader) { - if (MixinLoader.hasOwnProperty(mixin)) { - Network.prototype[mixin] = MixinLoader[mixin]; - } - } - }; + ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; - /** - * Load the XY positions of the nodes into the dataset. - */ - Network.prototype.storePosition = function () { - console.log("storePosition is depricated: use .storePositions() from now on."); - this.storePositions(); - }; + var selection, + item = ItemSet.itemFromTarget(event); - /** - * Load the XY positions of the nodes into the dataset. - */ - Network.prototype.storePositions = function () { - 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); - }; + if (item) { + // multi select items + selection = this.getSelection(); // current selection - /** - * 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) }; + 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 { - 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) }; + // 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); } } - } - return dataArray; - }; + this.setSelection(selection); - /** - * Returns true when the Network is active. - * @returns {boolean} - */ - Network.prototype.isActive = function () { - return !this.activator || this.activator.active; + this.body.emitter.emit("select", { + items: this.getSelection() + }); + } }; - /** - * Sets the scale - * @returns {Number} + * 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 */ - Network.prototype.setScale = function () { - return this._setScale(); - }; + ItemSet._getItemRange = function (itemsData) { + var max = null; + var min = null; + itemsData.forEach(function (data) { + if (min == null || data.start < min) { + min = data.start; + } - /** - * Returns the scale - * @returns {Number} - */ - Network.prototype.getScale = function () { - return this._getScale(); - }; + 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 + }; + }; /** - * Check if a node is a cluster. - * @param nodeId - * @returns {*} + * 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 */ - 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; + ItemSet.itemFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty("timeline-item")) { + return target["timeline-item"]; + } + target = target.parentNode; } - }; - /** - * 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 }); + 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; + //} + // - Network.prototype.getBoundingBox = function (nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].boundingBox; - } - }; + 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; + } - 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; - } + 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 nodeList; - }; + return null; + }; - 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); + /** + * 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 edgesList; - }; - Network.prototype.generateColorObject = function (color) { - return util.parseColor(color); + return null; }; - module.exports = Network; + module.exports = ItemSet; /***/ }, -/* 37 */ +/* 29 */ /***/ function(module, exports, __webpack_require__) { "use strict"; + var moment = __webpack_require__(2); + var DateUtil = __webpack_require__(26); var util = __webpack_require__(1); - var Node = __webpack_require__(40); /** - * @class Edge + * @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. * - * A edge connects two nodes - * @param {Object} properties Object with properties. Must contain - * At least properties from and to. - * Available properties: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color + * 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 Edge(properties, body, networkConstants) { - if (body === undefined) { - throw "No body provided"; - } - var fields = ["edges"]; - var constants = util.selectiveBridgeObject(fields, networkConstants); - this.options = constants.edges; - - this.options.smoothCurves = networkConstants.smoothCurves; - this.body = body; - - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.title = undefined; - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - this.value = undefined; - this.selected = false; - this.hover = false; - this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached - this.dirtyLabel = true; - this.colorDirty = true; - - this.from = null; // a node - this.to = null; // a node - this.via = null; // a temp node + function TimeStep(start, end, minimumStep, hiddenDates) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); - this.fromBackup = null; // used to clean up after reconnect (used for manipulation) - this.toBackup = null; // used to clean up after reconnect (used for manipulation) + this.autoScale = true; + this.scale = "day"; + this.step = 1; - // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster - // by storing the original information we can revert to the original connection when the cluser is opened. - this.fromArray = []; - this.toArray = []; + // initialize the range + this.setRange(start, end, minimumStep); - this.connected = false; + // hidden Dates options + this.switchedDay = false; + this.switchedMonth = false; + this.switchedYear = false; + this.hiddenDates = hiddenDates; + if (hiddenDates === undefined) { + this.hiddenDates = []; + } - this.widthFixed = false; - this.lengthFixed = false; + this.format = TimeStep.FORMAT; // default formatting + } - this.setProperties(properties); + // 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.controlNodesEnabled = false; - this.controlNodes = { from: null, to: null, positions: {} }; - this.connectedNode = null; - } + /** + * Set custom formatting for the minor an major labels of the TimeStep. + * Both `minorLabels` and `majorLabels` are an Object with properties: + * 'millisecond, 'second, 'minute', 'hour', 'weekday, 'day, 'month, 'year'. + * @param {{minorLabels: Object, majorLabels: Object}} format + */ + TimeStep.prototype.setFormat = function (format) { + var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); + this.format = util.deepExtend(defaultFormat, format); + }; /** - * Set or overwrite properties for the edge - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties + * 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 */ - Edge.prototype.setProperties = function (properties) { - this.colorDirty = true; - if (!properties) { - return; + 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.properties = properties; - var fields = ["style", "fontSize", "fontFace", "fontColor", "fontFill", "fontStrokeWidth", "fontStrokeColor", "width", "widthSelectionMultiplier", "hoverWidth", "arrowScaleFactor", "dash", "inheritColor", "labelAlignment", "opacity", "customScalingFunction", "useGradients", "value"]; - util.selectiveDeepExtend(fields, this.options, properties); + this._start = start != undefined ? new Date(start.valueOf()) : new Date(); + this._end = end != undefined ? new Date(end.valueOf()) : new Date(); - if (properties.from !== undefined) { - this.fromId = properties.from; - } - if (properties.to !== undefined) { - this.toId = properties.to; + if (this.autoScale) { + this.setMinimumStep(minimumStep); } + }; - if (properties.id !== undefined) { - this.id = properties.id; - } - if (properties.label !== undefined) { - this.label = properties.label;this.dirtyLabel = true; - } + /** + * Set the range iterator to the start date. + */ + TimeStep.prototype.first = function () { + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); + }; - if (properties.title !== undefined) { - this.title = properties.title; - } - if (properties.value !== undefined) { - this.value = properties.value; - } - if (properties.length !== undefined) { - this.physics.springLength = properties.length; + /** + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date + */ + TimeStep.prototype.roundToMinor = function () { + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + // noinspection FallThroughInSwitchStatementJS + switch (this.scale) { + case "year": + this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step)); + this.current.setMonth(0); + case "month": + this.current.setDate(1); + case "day": + // intentional fall through + case "weekday": + this.current.setHours(0); + case "hour": + this.current.setMinutes(0); + case "minute": + this.current.setSeconds(0); + case "second": + this.current.setMilliseconds(0); + //case 'millisecond': // nothing to do for milliseconds } - if (properties.color !== undefined) { - this.options.inheritColor = false; - if (util.isString(properties.color)) { - this.options.color.color = properties.color; - this.options.color.highlight = properties.color; - } else { - if (properties.color.color !== undefined) { - this.options.color.color = properties.color.color; - } - if (properties.color.highlight !== undefined) { - this.options.color.highlight = properties.color.highlight; - } - if (properties.color.hover !== undefined) { - this.options.color.hover = properties.color.hover; - } + 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; } } - - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); - - this.widthFixed = this.widthFixed || properties.width !== undefined; - this.lengthFixed = this.lengthFixed || properties.length !== undefined; - - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - - // set draw method based on style - switch (this.options.style) { - case "line": - this.draw = this._drawLine;break; - case "arrow": - this.draw = this._drawArrow;break; - case "arrow-center": - this.draw = this._drawArrowCenter;break; - case "dash-line": - this.draw = this._drawDashLine;break; - default: - this.draw = this._drawLine;break; - } }; + /** + * 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(); + }; /** - * Connect an edge to its nodes + * Do the next step */ - Edge.prototype.connect = function () { - this.disconnect(); + TimeStep.prototype.next = function () { + var prev = this.current.valueOf(); - this.from = this.body.nodes[this.fromId] || null; - this.to = this.body.nodes[this.toId] || null; - this.connected = this.from !== null && this.to !== null; + // 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": - if (this.connected === true) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } else { - if (this.from) { - this.from.detachEdge(this); + + 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; } - if (this.to) { - this.to.detachEdge(this); + } 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; } } - }; - /** - * Disconnect an edge from its nodes - */ - Edge.prototype.disconnect = function () { - if (this.from) { - this.from.detachEdge(this); - this.from = null; - } - if (this.to) { - this.to.detachEdge(this); - this.to = null; + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case "millisecond": + if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; + case "second": + if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; + case "minute": + if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; + case "hour": + if (this.current.getHours() < this.step) this.current.setHours(0);break; + case "weekday": + // intentional fall through + case "day": + if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; + case "month": + if (this.current.getMonth() < this.step) this.current.setMonth(0);break; + case "year": + break; // nothing to do for year + default: + break; + } } - this.connected = false; - }; + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); + } - /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. - */ - Edge.prototype.getTitle = function () { - return typeof this.title === "function" ? this.title() : this.title; + DateUtil.stepOverHiddenDates(this, prev); }; /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - Edge.prototype.getValue = function () { - return this.value; - }; - - /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max + * Get the current datetime + * @return {Date} current The current date */ - Edge.prototype.setValueRange = function (min, max, total) { - if (!this.widthFixed && this.value !== undefined) { - var scale = this.options.customScalingFunction(min, max, total, this.value); - var widthDiff = this.options.widthMax - this.options.widthMin; - this.options.width = this.options.widthMin + scale * widthDiff; - this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; - } + TimeStep.prototype.getCurrent = function () { + return this.current; }; /** - * 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 + * 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. */ - Edge.prototype.draw = function (ctx) { - throw "Method draw not initialized in edge"; + 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; + } }; /** - * 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 + * Enable or disable autoscaling + * @param {boolean} enable If true, autoascaling is set true */ - Edge.prototype.isOverlappingWith = function (obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; + TimeStep.prototype.setAutoScale = function (enable) { + this.autoScale = enable; + }; - var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - return dist < distMax; - } else { - return false; + /** + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds + */ + TimeStep.prototype.setMinimumStep = function (minimumStep) { + if (minimumStep == undefined) { + return; } - }; - Edge.prototype._getColor = function (ctx) { - var colorObj = this.options.color; - if (this.options.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; + //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; - if (this.from.selected == false && this.to.selected == false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.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); - return grd; + // find the smallest step that is larger than the provided minimumStep + if (stepYear * 1000 > minimumStep) { + this.scale = "year";this.step = 1000; } - - if (this.colorDirty === true) { - if (this.options.inheritColor == "to") { - colorObj = { - highlight: this.to.options.color.highlight.border, - hover: this.to.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; - } else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { - colorObj = { - highlight: this.from.options.color.highlight.border, - hover: this.from.options.color.hover.border, - color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) - }; - } - this.options.color = colorObj; - this.colorDirty = false; + if (stepYear * 500 > minimumStep) { + this.scale = "year";this.step = 500; } - - - - if (this.selected == true) { - return colorObj.highlight; - } else if (this.hover == true) { - return colorObj.hover; - } else { - return colorObj.color; + if (stepYear * 100 > minimumStep) { + this.scale = "year";this.step = 100; + } + if (stepYear * 50 > minimumStep) { + this.scale = "year";this.step = 50; + } + if (stepYear * 10 > minimumStep) { + this.scale = "year";this.step = 10; + } + if (stepYear * 5 > minimumStep) { + this.scale = "year";this.step = 5; + } + if (stepYear > minimumStep) { + this.scale = "year";this.step = 1; + } + if (stepMonth * 3 > minimumStep) { + this.scale = "month";this.step = 3; + } + if (stepMonth > minimumStep) { + this.scale = "month";this.step = 1; + } + if (stepDay * 5 > minimumStep) { + this.scale = "day";this.step = 5; + } + if (stepDay * 2 > minimumStep) { + this.scale = "day";this.step = 2; + } + if (stepDay > minimumStep) { + this.scale = "day";this.step = 1; + } + if (stepDay / 2 > minimumStep) { + this.scale = "weekday";this.step = 1; + } + if (stepHour * 4 > minimumStep) { + this.scale = "hour";this.step = 4; + } + if (stepHour > minimumStep) { + this.scale = "hour";this.step = 1; + } + if (stepMinute * 15 > minimumStep) { + this.scale = "minute";this.step = 15; + } + if (stepMinute * 10 > minimumStep) { + this.scale = "minute";this.step = 10; + } + if (stepMinute * 5 > minimumStep) { + this.scale = "minute";this.step = 5; + } + if (stepMinute > minimumStep) { + this.scale = "minute";this.step = 1; + } + if (stepSecond * 15 > minimumStep) { + this.scale = "second";this.step = 15; + } + if (stepSecond * 10 > minimumStep) { + this.scale = "second";this.step = 10; + } + if (stepSecond * 5 > minimumStep) { + this.scale = "second";this.step = 5; + } + if (stepSecond > minimumStep) { + this.scale = "second";this.step = 1; + } + if (stepMillisecond * 200 > minimumStep) { + this.scale = "millisecond";this.step = 200; + } + if (stepMillisecond * 100 > minimumStep) { + this.scale = "millisecond";this.step = 100; + } + if (stepMillisecond * 50 > minimumStep) { + this.scale = "millisecond";this.step = 50; + } + if (stepMillisecond * 10 > minimumStep) { + this.scale = "millisecond";this.step = 10; + } + if (stepMillisecond * 5 > minimumStep) { + this.scale = "millisecond";this.step = 5; + } + if (stepMillisecond > minimumStep) { + this.scale = "millisecond";this.step = 1; } }; - /** - * 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 + * 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 */ - Edge.prototype._drawLine = function (ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.lineWidth = this._getLineWidth(); + TimeStep.snap = function (date, scale, step) { + var clone = new Date(date.valueOf()); - if (this.from != this.to) { - // draw line - var via = this._line(ctx); + 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); + } - // draw label - var point; - if (this.label) { - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); - var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); - point = { x: midpointX, y: midpointY }; - } else { - point = this._pointOnLine(0.5); - } - this._label(ctx, this.label, point.x, point.y); + 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; } - } else { - var x, y; - var radius = this.physics.springLength / 4; - var node = this.from; - if (!node.width) { - node.resize(ctx); + 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; } - if (node.width > node.height) { - x = node.x + node.width / 2; - y = node.y - radius; - } else { - x = node.x + radius; - y = node.y - node.height / 2; + 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; } - this._circle(ctx, x, y, radius); - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); + 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 clone; }; /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width - * @private + * Check if the current value is a major value (for example when the step + * is DAY, a major value is each first day of the MONTH) + * @return {boolean} true if current date is major, else false. */ - Edge.prototype._getLineWidth = function () { - if (this.selected == true) { - return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3 * this.networkScaleInv); - } else { - if (this.hover == true) { - return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3 * this.networkScaleInv); - } else { - return Math.max(this.options.width, 0.3 * this.networkScaleInv); + 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; } - } - }; - - Edge.prototype._getViaCoordinates = function () { - if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) { - return this.via; - } else if (this.options.smoothCurves.enabled == false) { - return { x: 0, y: 0 }; - } else { - var xVia = null; - var yVia = null; - var factor = this.options.smoothCurves.roundness; - var type = this.options.smoothCurves.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") { - var dx = this.to.x - this.from.x; - var 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); - - 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") { - var dx = this.to.x - this.from.x; - var 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); - - 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; - } - } - } + } 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; } + } - - return { x: xVia, y: yVia }; + 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; } }; + /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx - * @private + * 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 */ - Edge.prototype._line = function (ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - if (this.options.smoothCurves.enabled == true) { - if (this.options.smoothCurves.dynamic == false) { - var via = this._getViaCoordinates(); - if (via.x == null) { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; - } else { - // this.via.x = via.x; - // this.via.y = via.y; - ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); - ctx.stroke(); - //ctx.circle(via.x,via.y,2) - //ctx.stroke(); - return via; - } - } else { - ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - ctx.stroke(); - return this.via; - } - } else { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return null; + TimeStep.prototype.getLabelMinor = function (date) { + if (date == undefined) { + date = this.current; } - }; - /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @private - */ - Edge.prototype._circle = function (ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + var format = this.format.minorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ""; }; /** - * Draw label with white background and with the middle at (x, y) - * @param {CanvasRenderingContext2D} ctx - * @param {String} text - * @param {Number} x - * @param {Number} y - * @private + * 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 */ - Edge.prototype._label = function (ctx, text, x, y) { - if (text) { - ctx.font = (this.from.selected || this.to.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace; - var yLine; + TimeStep.prototype.getLabelMajor = function (date) { + if (date == undefined) { + date = this.current; + } - if (this.dirtyLabel == true) { - var lines = String(text).split("\n"); - var lineCount = lines.length; - var fontSize = Number(this.options.fontSize); - yLine = y + (1 - lineCount) / 2 * fontSize; + var format = this.format.majorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ""; + }; - var 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; - } - var height = this.options.fontSize * lineCount; - var left = x - width / 2; - var top = y - height / 2; + 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; - // cache - this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; + function even(value) { + return value / step % 2 == 0 ? " even" : " odd"; + } + + 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 ""; + } - var yLine = this.labelDimensions.yLine; + function currentWeek(date) { + return date.isSame(new Date(), "week") ? " current-week" : ""; + } - ctx.save(); + function currentMonth(date) { + return date.isSame(new Date(), "month") ? " current-month" : ""; + } - if (this.options.labelAlignment != "horizontal") { - ctx.translate(x, yLine); - this._rotateForLabelAlignment(ctx); - x = 0; - yLine = 0; - } + function currentYear(date) { + return date.isSame(new Date(), "year") ? " current-year" : ""; + } + switch (this.scale) { + case "millisecond": + return even(date.milliseconds()).trim(); - this._drawLabelRect(ctx); - this._drawLabelText(ctx, x, yLine, lines, lineCount, fontSize); + case "second": + return even(date.seconds()).trim(); - ctx.restore(); - } - }; + case "minute": + return even(date.minutes()).trim(); - /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private - */ - Edge.prototype._rotateForLabelAlignment = function (ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); + case "hour": + var hours = date.hours(); + if (this.step == 4) { + hours = hours + "-h" + (hours + 4); + } + return "h" + hours + today(date) + even(date.hours()); - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; - } + case "weekday": + return date.format("dddd").toLowerCase() + today(date) + currentWeek(date) + even(date.date()); - ctx.rotate(angleInDegrees); - }; + case "day": + var day = date.date(); + var month = date.format("MMMM").toLowerCase(); + return "day" + day + " " + month + currentMonth(date) + even(day - 1); - /** - * Draws the label rectangle - * @param {CanvasRenderingContext2D} ctx - * @param {String} labelAlignment - * @private - */ - Edge.prototype._drawLabelRect = function (ctx) { - if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { - ctx.fillStyle = this.options.fontFill; + case "month": + return date.format("MMMM").toLowerCase() + currentMonth(date) + even(date.month()); - var lineMargin = 2; + case "year": + var year = date.year(); + return "year" + year + currentYear(date) + even(year); - if (this.options.labelAlignment == "line-center") { - ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height); - } else if (this.options.labelAlignment == "line-above") { - ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height); - } else if (this.options.labelAlignment == "line-below") { - ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height); - } else { - ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height); - } + default: + return ""; } }; + module.exports = TimeStep; + +/***/ }, +/* 30 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var stack = __webpack_require__(31); + var RangeItem = __webpack_require__(32); + /** - * Draws the label text - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} yLine - * @param {Array} lines - * @param {Number} lineCount - * @param {Number} fontSize - * @private + * @constructor Group + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ - Edge.prototype._drawLabelText = function (ctx, x, yLine, lines, lineCount, fontSize) { - // draw text - ctx.fillStyle = this.options.fontColor || "black"; - ctx.textAlign = "center"; + function Group(groupId, data, itemSet) { + this.groupId = groupId; + this.subgroups = {}; + this.subgroupIndex = 0; + this.subgroupOrderer = data && data.subgroupOrder; + this.itemSet = itemSet; - // check for label alignment - if (this.options.labelAlignment != "horizontal") { - var lineMargin = 2; - if (this.options.labelAlignment == "line-above") { - ctx.textBaseline = "alphabetic"; - yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } else if (this.options.labelAlignment == "line-below") { - ctx.textBaseline = "hanging"; - yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers - } else { - ctx.textBaseline = "middle"; + this.dom = {}; + this.props = { + label: { + width: 0, + height: 0 } - } else { - ctx.textBaseline = "middle"; - } + }; + this.className = null; - // check for strokeWidth - if (this.options.fontStrokeWidth > 0) { - ctx.lineWidth = this.options.fontStrokeWidth; - ctx.strokeStyle = this.options.fontStrokeColor; - ctx.lineJoin = "round"; - } - for (var i = 0; i < lineCount; i++) { - if (this.options.fontStrokeWidth > 0) { - ctx.strokeText(lines[i], x, yLine); - } - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } - }; + 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); + } /** - * Redraw a edge as a dashed line - * Draw this edge in the given canvas - * @author David Jordan - * @date 2012-08-08 - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * Create DOM elements for the group * @private */ - Edge.prototype._drawDashLine = function (ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.lineWidth = this._getLineWidth(); + Group.prototype._create = function () { + var label = document.createElement("div"); + label.className = "vlabel"; + this.dom.label = label; - var via = null; - // 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.dash.length !== undefined && this.options.dash.gap !== undefined) { - pattern = [this.options.dash.length, this.options.dash.gap]; - } else { - pattern = [5, 5]; - } + var inner = document.createElement("div"); + inner.className = "inner"; + label.appendChild(inner); + this.dom.inner = inner; - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; + var foreground = document.createElement("div"); + foreground.className = "group"; + foreground["timeline-group"] = this; + this.dom.foreground = foreground; - // draw the line - via = this._line(ctx); + this.dom.background = document.createElement("div"); + this.dom.background.className = "group"; - // restore the dash settings. - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; - ctx.restore(); + 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); + }; + + /** + * Set the group data for this group + * @param {Object} data Group data, can contain properties content and className + */ + Group.prototype.setData = function (data) { + // update contents + var content = data && data.content; + if (content instanceof Element) { + this.dom.inner.appendChild(content); + } else if (content !== undefined && content !== null) { + this.dom.inner.innerHTML = content; } else { - // unsupporting smooth lines - // draw dashed line - ctx.beginPath(); - ctx.lineCap = "round"; - if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap, this.options.dash.altLength, this.options.dash.gap]); - } else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap]); - } else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); + this.dom.inner.innerHTML = this.groupId || ""; // groupId can be null } - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); - var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); - point = { x: midpointX, y: midpointY }; - } else { - point = this._pointOnLine(0.5); + // update title + this.dom.label.title = data && data.title || ""; + + if (!this.dom.inner.firstChild) { + util.addClassName(this.dom.inner, "hidden"); + } else { + util.removeClassName(this.dom.inner, "hidden"); + } + + // 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); } - this._label(ctx, this.label, point.x, point.y); + util.addClassName(this.dom.label, className); + util.addClassName(this.dom.foreground, className); + util.addClassName(this.dom.background, className); + util.addClassName(this.dom.axis, className); + this.className = className; } - }; - /** - * Get a point on a line - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private - */ - Edge.prototype._pointOnLine = function (percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y - }; + // update style + if (this.style) { + util.removeCssText(this.dom.label, this.style); + this.style = null; + } + if (data && data.style) { + util.addCssText(this.dom.label, data.style); + this.style = data.style; + } }; /** - * 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 + * Get the width of the group label + * @return {number} width */ - Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { - var angle = (percentage - 3 / 8) * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; + Group.prototype.getLabelWidth = function () { + return this.props.label.width; }; + /** - * Redraw a edge as a line with an arrow halfway the line - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private + * Repaint this group + * @param {{start: number, end: number}} range + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @param {boolean} [restack=false] Force restacking of all items + * @return {boolean} Returns true if the group is resized */ - Edge.prototype._drawArrowCenter = function (ctx) { - var point; - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this._getLineWidth(); - - if (this.from != this.to) { - // draw line - var via = this._line(ctx); - - var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - // draw an arrow halfway the line - if (this.options.smoothCurves.enabled == true && via != null) { - var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); - var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); - point = { x: midpointX, y: midpointY }; - } else { - point = this._pointOnLine(0.5); - } + Group.prototype.redraw = function (range, margin, restack) { + var resized = false; - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - // draw label - if (this.label) { - this._label(ctx, this.label, point.x, point.y); - } - } else { - // draw circle - var x, y; - var radius = 0.25 * Math.max(100, this.physics.springLength); - var node = this.from; - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - y = node.y - radius; - } else { - x = node.x + radius; - y = node.y - node.height * 0.5; - } - this._circle(ctx, x, y, radius); + // 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; - // draw all arrows - var angle = 0.2 * Math.PI; - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - point = this._pointOnCircle(x, y, radius, 0.5); - ctx.arrow(point.x, point.y, angle, length); - ctx.fill(); - ctx.stroke(); + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + restack = true; } - }; - Edge.prototype._pointOnBezier = function (t) { - var via = this._getViaCoordinates(); + // 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); + } - 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; + // recalculate the height of the group + var height = this._calculateHeight(margin); - return { x: x, y: y }; + // 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; }; /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. - * - * @param from - * @param ctx - * @returns {*} + * recalculate the height of the group + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @returns {number} Returns the height * @private */ - Edge.prototype._findBorderPosition = function (from, ctx) { - var maxIterations = 10; - var iteration = 0; - var low = 0; - var high = 1; - var pos, angle, distanceToBorder, distanceToNodes, difference; - var threshold = 0.2; - var node = this.to; - if (from == true) { - node = this.from; - } - - while (low <= high && iteration < maxIterations) { - var middle = (low + high) * 0.5; - - pos = this._pointOnBezier(middle); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToNodes = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToNodes; - 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; + 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) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, item.top + item.height); + if (item.data.subgroup !== undefined) { + me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); + me.subgroups[item.data.subgroup].visible = true; + //if (visibleSubgroups.indexOf(item.data.subgroup) == -1){ + // visibleSubgroups.push(item.data.subgroup); + // me.visibleSubgroups += 1; + //} } + }); + if (min > margin.axis) { + // there is an empty gap between the lowest item and the axis + var offset = min - margin.axis; + max -= offset; + util.forEach(visibleItems, function (item) { + item.top -= offset; + }); } - - iteration++; + height = max + margin.item.vertical / 2; + } else { + height = margin.axis + margin.item.vertical; } - pos.t = middle; + height = Math.max(height, this.props.label.height); - return pos; + return height; }; /** - * Redraw a edge as a line with an arrow - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - * @private + * Show this group: attach to the DOM */ - Edge.prototype._drawArrow = function (ctx) { - // set style - ctx.strokeStyle = this._getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this._getLineWidth(); - - // set vars - var angle, length, arrowPos; + Group.prototype.show = function () { + if (!this.dom.label.parentNode) { + this.itemSet.dom.labelSet.appendChild(this.dom.label); + } - // if not connected to itself - if (this.from != this.to) { - // draw line - this._line(ctx); + if (!this.dom.foreground.parentNode) { + this.itemSet.dom.foreground.appendChild(this.dom.foreground); + } - // draw arrow head - if (this.options.smoothCurves.enabled == true) { - var via = this._getViaCoordinates(); - arrowPos = this._findBorderPosition(false, ctx); - var guidePos = this._pointOnBezier(Math.max(0, arrowPos.t - 0.1)); - angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); - } else { - angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); - var dx = this.to.x - this.from.x; - var dy = this.to.y - this.from.y; - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); + } - arrowPos = {}; - arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; - } + if (!this.dom.axis.parentNode) { + this.itemSet.dom.axis.appendChild(this.dom.axis); + } + }; - // draw arrow at the end of the line - length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrowPos.x, arrowPos.y, angle, length); - ctx.fill(); - ctx.stroke(); + /** + * Hide this group: remove from the DOM + */ + Group.prototype.hide = function () { + var label = this.dom.label; + if (label.parentNode) { + label.parentNode.removeChild(label); + } - // draw label - if (this.label) { - var point; - if (this.options.smoothCurves.enabled == true && via != null) { - point = this._pointOnBezier(0.5); - } else { - point = this._pointOnLine(0.5); - } - this._label(ctx, this.label, point.x, point.y); - } - } else { - // draw circle - var node = this.from; - var x, y, arrow; - var radius = 0.25 * Math.max(100, this.physics.springLength); - if (!node.width) { - node.resize(ctx); - } - if (node.width > node.height) { - x = node.x + node.width * 0.5; - y = node.y - radius; - arrow = { - x: x, - y: node.y, - angle: 0.9 * Math.PI - }; - } else { - x = node.x + radius; - y = node.y - node.height * 0.5; - arrow = { - x: node.x, - y: y, - angle: 0.6 * Math.PI - }; - } - ctx.beginPath(); - // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + var foreground = this.dom.foreground; + if (foreground.parentNode) { + foreground.parentNode.removeChild(foreground); + } - // draw all arrows - var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; - ctx.arrow(arrow.x, arrow.y, arrow.angle, length); - ctx.fill(); - ctx.stroke(); + var background = this.dom.background; + if (background.parentNode) { + background.parentNode.removeChild(background); + } - // draw label - if (this.label) { - point = this._pointOnCircle(x, y, radius, 0.5); - this._label(ctx, this.label, point.x, point.y); - } + var axis = this.dom.axis; + if (axis.parentNode) { + axis.parentNode.removeChild(axis); } }; /** - * 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 + * Add an item to the group + * @param {Item} item */ - Edge.prototype._getDistanceToEdge = function (x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - var returnValue = 0; - if (this.from != this.to) { - if (this.options.smoothCurves.enabled == true) { - var xVia, yVia; - if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { - xVia = this.via.x; - yVia = this.via.y; - } else { - var via = this._getViaCoordinates(); - xVia = via.x; - yVia = via.y; - } - var minDistance = 1000000000; - var distance; - var i, t, x, y, lastX, lastY; - for (i = 0; i < 10; i++) { - t = 0.1 * i; - x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; - y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; - if (i > 0) { - distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); - minDistance = distance < minDistance ? distance : minDistance; - } - lastX = x;lastY = y; - } - returnValue = minDistance; - } else { - returnValue = this._getDistanceToLine(x1, y1, x2, y2, x3, y3); - } - } else { - var x, y, dx, dy; - var radius = 0.25 * this.physics.springLength; - var node = this.from; - if (node.width > node.height) { - x = node.x + 0.5 * node.width; - y = node.y - radius; - } else { - x = node.x + radius; - y = node.y - 0.5 * node.height; + 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++; } - dx = x - x3; - dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); + this.subgroups[item.data.subgroup].items.push(item); } + this.orderSubgroups(); - if (this.labelDimensions.left < x3 && this.labelDimensions.left + this.labelDimensions.width > x3 && this.labelDimensions.top < y3 && this.labelDimensions.top + this.labelDimensions.height > y3) { - return 0; - } else { - return returnValue; + 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); } }; - Edge.prototype._getDistanceToLine = function (x1, y1, x2, y2, x3, y3) { - var px = x2 - x1, - py = y2 - y1, - something = px * px + py * py, - u = ((x3 - x1) * px + (y3 - y1) * py) / something; + 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 (u > 1) { - u = 1; - } else if (u < 0) { - u = 0; + if (sortArray.length > 0) { + for (var i = 0; i < sortArray.length; i++) { + this.subgroups[sortArray[i].subgroup].index = i; + } + } } + }; - var x = x1 + u * px, - y = y1 + u * py, - dx = x - x3, - dy = y - y3; - - //# Note: If the actual distance does not matter, - //# if you only want to compare what this function - //# returns to other results of this function, you - //# can just return the squared distance instead - //# (i.e. remove the sqrt) to gain a little performance - - return Math.sqrt(dx * dx + dy * dy); + Group.prototype.resetSubgroups = function () { + for (var subgroup in this.subgroups) { + if (this.subgroups.hasOwnProperty(subgroup)) { + this.subgroups[subgroup].visible = false; + } + } }; /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale + * Remove an item from the group + * @param {Item} item */ - Edge.prototype.setScale = function (scale) { - this.networkScaleInv = 1 / scale; - }; + 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); - Edge.prototype.select = function () { - this.selected = true; + // TODO: also remove from ordered items? }; - Edge.prototype.unselect = function () { - this.selected = false; + + /** + * Remove an item from the corresponding DataSet + * @param {Item} item + */ + Group.prototype.removeFromDataSet = function (item) { + this.itemSet.removeItem(item.id); }; - Edge.prototype.positionBezierNode = function () { - if (this.via !== null && this.from !== null && this.to !== null) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); - } else if (this.via !== null) { - this.via.x = 0; - this.via.y = 0; + + /** + * Reorder the items + */ + Group.prototype.order = function () { + var array = util.toArray(this.items); + var startArray = []; + var endArray = []; + + for (var i = 0; i < array.length; i++) { + if (array[i].data.end !== undefined) { + endArray.push(array[i]); + } + startArray.push(array[i]); } + this.orderedItems = { + byStart: startArray, + byEnd: endArray + }; + + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); }; + /** - * This function draws the control nodes for the manipulator. - * In order to enable this, only set the this.controlNodesEnabled to true. - * @param ctx + * Update the visible items + * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date + * @param {Item[]} visibleItems The previously visible items. + * @param {{start: number, end: number}} range Visible range + * @return {Item[]} visibleItems The new visible items. + * @private */ - Edge.prototype._drawControlNodes = function (ctx) { - if (this.controlNodesEnabled == true) { - if (this.controlNodes.from === null && this.controlNodes.to === null) { - var nodeIdFrom = "edgeIdFrom:".concat(this.id); - var nodeIdTo = "edgeIdTo:".concat(this.id); - var constants = { - nodes: { group: "", radius: 7, borderWidth: 2, borderWidthSelected: 2 }, - physics: { damping: 0 }, - clustering: { maxNodeSizeIncrements: 0, nodeScaling: { width: 0, height: 0, radius: 0 } } - }; - this.controlNodes.from = new Node({ id: nodeIdFrom, - shape: "dot", - color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } - }, {}, {}, constants); - this.controlNodes.to = new Node({ id: nodeIdTo, - shape: "dot", - color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } - }, {}, {}, constants); + Group.prototype._updateVisibleItems = function (orderedItems, oldVisibleItems, range) { + var visibleItems = []; + var visibleItemsLookup = {}; // we keep this to quickly look up if an item already exists in the list without using indexOf on visibleItems + var interval = (range.end - range.start) / 4; + var lowerBound = range.start - interval; + var upperBound = range.end + interval; + var item, i; + + // this function is used to do the binary search. + var searchFunction = function (value) { + if (value < lowerBound) { + return -1; + } else if (value <= upperBound) { + return 0; + } else { + return 1; } + }; - this.controlNodes.positions = {}; - if (this.controlNodes.from.selected == false) { - this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx); - this.controlNodes.from.x = this.controlNodes.positions.from.x; - this.controlNodes.from.y = this.controlNodes.positions.from.y; + // 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 (this.controlNodes.to.selected == false) { - this.controlNodes.positions.to = this.getControlNodeToPosition(ctx); - this.controlNodes.to.x = this.controlNodes.positions.to.x; - this.controlNodes.to.y = this.controlNodes.positions.to.y; + } + + // we do a binary search for the items that have only start values. + var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, "data", "start"); + + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the start values. + this._traceVisible(initialPosByStart, orderedItems.byStart, visibleItems, visibleItemsLookup, function (item) { + return item.data.start < lowerBound || item.data.start > upperBound; + }); + + // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. + // We therefore have to brute force check all items in the byEnd list + if (this.checkRangedItems == true) { + this.checkRangedItems = false; + for (i = 0; i < orderedItems.byEnd.length; i++) { + this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); } - - this.controlNodes.from.draw(ctx); - this.controlNodes.to.draw(ctx); } else { - this.controlNodes = { from: null, to: null, positions: {} }; + // 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; + }); } - }; - /** - * Enable control nodes. - * @private - */ - Edge.prototype._enableControlNodes = function () { - this.fromBackup = this.from; - this.toBackup = this.to; - this.controlNodesEnabled = true; - }; - /** - * disable control nodes and remove from dynamicEdges from old node - * @private - */ - Edge.prototype._disableControlNodes = function () { - this.fromId = this.from.id; - this.toId = this.to.id; - if (this.fromId != this.fromBackup.id) { - // from was changed, remove edge from old 'from' node dynamic edges - this.fromBackup.detachEdge(this); - } else if (this.toId != this.toBackup.id) { - // to was changed, remove edge from old 'to' node dynamic edges - this.toBackup.detachEdge(this); + // 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(); } - this.fromBackup = null; - this.toBackup = null; - this.controlNodesEnabled = false; + // 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; - /** - * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. - * @param x - * @param y - * @returns {null} - * @private - */ - Edge.prototype._getSelectedControlNode = function (x, y) { - var positions = this.controlNodes.positions; - var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2)); - var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2)); + 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); + } + } + } - if (fromDistance < 15) { - this.connectedNode = this.from; - this.from = this.controlNodes.from; - return this.controlNodes.from; - } else if (toDistance < 15) { - this.connectedNode = this.to; - this.to = this.controlNodes.to; - return this.controlNodes.to; - } else { - return null; + for (i = initialPos + 1; i < items.length; i++) { + item = items[i]; + if (breakCondition(item)) { + break; + } else { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } + } + } } }; /** - * this resets the control nodes to their original position. + * 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 */ - Edge.prototype._restoreControlNodes = function () { - if (this.controlNodes.from.selected == true) { - this.from = this.connectedNode; - this.connectedNode = null; - this.controlNodes.from.unselect(); - } else if (this.controlNodes.to.selected == true) { - this.to = this.connectedNode; - this.connectedNode = null; - this.controlNodes.to.unselect(); + 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(); } }; + /** - * this calculates the position of the control nodes on the edges of the parent nodes. + * 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 ctx - * @returns {x: *, y: *} + * @param {Item} item + * @param {Array} visibleItems + * @param {{start:number, end:number}} range + * @private */ - Edge.prototype.getControlNodeFromPosition = function (ctx) { - // draw arrow head - var controlnodeFromPos; - if (this.options.smoothCurves.enabled == true) { - controlnodeFromPos = this._findBorderPosition(true, ctx); + Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { + if (item.isVisible(range)) { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } } else { - var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); - var dx = this.to.x - this.from.x; - var dy = this.to.y - this.from.y; - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - - var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); - var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; - controlnodeFromPos = {}; - controlnodeFromPos.x = fromBorderPoint * this.from.x + (1 - fromBorderPoint) * this.to.x; - controlnodeFromPos.y = fromBorderPoint * this.from.y + (1 - fromBorderPoint) * this.to.y; + if (item.displayed) item.hide(); } - - return controlnodeFromPos; }; - /** - * this calculates the position of the control nodes on the edges of the parent nodes. - * - * @param ctx - * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} - */ - Edge.prototype.getControlNodeToPosition = function (ctx) { - // draw arrow head - var controlnodeFromPos, controlnodeToPos; - if (this.options.smoothCurves.enabled == true) { - controlnodeToPos = this._findBorderPosition(false, ctx); - } else { - var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); - var dx = this.to.x - this.from.x; - var dy = this.to.y - this.from.y; - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = this.to.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - - controlnodeToPos = {}; - controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; - controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; - } - return controlnodeToPos; - }; - module.exports = Edge; + module.exports = Group; /***/ }, -/* 38 */ +/* 31 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var util = __webpack_require__(1); + // Utility functions for ordering and stacking of items + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors /** - * @class Groups - * This class can store groups and properties specific for groups. + * Order items by their start data + * @param {Item[]} items */ - function Groups() { - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; - this.useDefaultGroups = true; - } - + exports.orderByStart = function (items) { + items.sort(function (a, b) { + return a.data.start - b.data.start; + }); + }; /** - * default constants for group colors + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} items */ - Groups.DEFAULT = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 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 + 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; - { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red + return aTime - bTime; + }); + }; - { 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 + /** + * 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; - { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; + 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; - /** - * Clear all groups - */ - Groups.prototype.clear = function () { - this.groups = {}; - this.groups.length = function () { - var i = 0; - for (var p in this) { - if (this.hasOwnProperty(p)) { - i++; - } + 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 i; - }; + } }; /** - * get group properties of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group properties + * 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. */ - Groups.prototype.get = function (groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - if (this.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; + 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 { - // create new group - var index = this.defaultIndex % Groups.DEFAULT.length; - this.defaultIndex++; - group = {}; - group.color = Groups.DEFAULT[index]; - this.groups[groupname] = group; + items[i].top = margin.axis; } } - - return group; }; /** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object + * 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 */ - Groups.prototype.add = function (groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; + exports.collision = function (a, b, margin) { + return a.left - margin.horizontal + EPSILON < b.left + b.width && a.left + a.width + margin.horizontal - EPSILON > b.left && a.top - margin.vertical + EPSILON < b.top + b.height && a.top + a.height + margin.vertical - EPSILON > b.top; }; - module.exports = Groups; - // 20:bright red - /***/ }, -/* 39 */ +/* 32 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - /** - * @class Images - * This class loads images and keeps them stored. - */ - function Images() { - this.images = {}; - this.imageBroken = {}; - this.callback = undefined; - } - - /** - * Set an onload callback function. This will be called each time an image - * is loaded - * @param {function} callback - */ - Images.prototype.setOnloadCallback = function (callback) { - this.callback = callback; - }; + var Hammer = __webpack_require__(19); + var Item = __webpack_require__(33); /** - * - * @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 + * @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 */ - 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); - } - - 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) { - if (this.src == brokenUrl) { - 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; - } - } else { - console.error("Could not load image:", url); - this.src = brokenUrl; - me.imageBroken[url] = true; - } - } - }; + 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 - img.src = url; + // 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); + } } - return img; - }; - - module.exports = Images; - -/***/ }, -/* 40 */ -/***/ function(module, exports, __webpack_require__) { + Item.call(this, data, conversion, options); + } - "use strict"; + RangeItem.prototype = new Item(null, null, null); - var util = __webpack_require__(1); + RangeItem.prototype.baseClassName = "item range"; /** - * @class Node - * A node. A node can be connected to other nodes via one or multiple edges. - * @param {object} properties An object containing properties for the node. All - * properties are optional, except for the id. - * {number} id Id of the node. Required - * {string} label Text label for the node - * {number} x Horizontal position of the node - * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square", "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 properties - * @param {Object} constants An object with default values for - * example for the color - * + * 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 Node(properties, imagelist, grouplist, networkConstants) { - var constants = util.selectiveBridgeObject(["nodes"], networkConstants); - this.options = constants.nodes; + RangeItem.prototype.isVisible = function (range) { + // determine visibility + return this.data.start < range.end && this.data.end > range.start; + }; - this.selected = false; - this.hover = false; + /** + * Repaint the item + */ + RangeItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - this.edges = []; // all edges connected to this node + // background box + dom.box = document.createElement("div"); + // className is updated in redraw() - // set defaults for the properties - this.id = undefined; - this.allowedToMoveX = false; - this.allowedToMoveY = false; - this.xFixed = false; - this.yFixed = false; - this.horizontalAlignLeft = true; // these are for the navigation controls - this.verticalAlignTop = true; // these are for the navigation controls - this.baseRadiusValue = networkConstants.nodes.radius; - this.radiusFixed = false; - this.level = -1; - this.preassignedLevel = false; - this.hierarchyEnumerated = false; - this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached - this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; + // contents box + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - this.imagelist = imagelist; - this.grouplist = grouplist; + // attach this item as attribute + dom.box["timeline-item"] = this; - // physics properties - this.x = null; - this.y = null; - this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate + this.dirty = true; + } - // used for reverting to previous position on stabilization - this.previousState = { vx: 0, vy: 0, x: 0, y: 0 }; + // 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; - this.fixedData = { x: null, y: null }; + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.box); + this._updateDataAttributes(this.dom.box); + this._updateStyle(this.dom.box); - this.setProperties(properties, constants); + // update class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = this.baseClassName + className; - // variables to tell the node about the network. - this.networkScaleInv = 1; - this.networkScale = 1; - this.canvasTopLeft = { x: -300, y: -300 }; - this.canvasBottomRight = { x: 300, y: 300 }; - this.parentEdgeId = null; - } + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; + // recalculate size + // turn off max-width to be able to calculate the real width + // this causes an extra browser repaint/reflow, but so be it + this.dom.content.style.maxWidth = "none"; + this.props.content.width = this.dom.content.offsetWidth; + this.height = this.dom.box.offsetHeight; + this.dom.content.style.maxWidth = ""; - /** - * Attach a edge to the node - * @param {Edge} edge - */ - Node.prototype.attachEdge = function (edge) { - if (this.edges.indexOf(edge) == -1) { - this.edges.push(edge); + this.dirty = false; } + + this._repaintDeleteButton(dom.box); + this._repaintDragLeft(); + this._repaintDragRight(); }; /** - * Detach a edge from the node - * @param {Edge} edge + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - Node.prototype.detachEdge = function (edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); + RangeItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); } }; - /** - * Set or overwrite properties for the node - * @param {Object} properties an object with properties - * @param {Object} constants and object with default, global properties + * Hide the item from the DOM (when visible) + * @return {Boolean} changed */ - Node.prototype.setProperties = function (properties, constants) { - if (!properties) { - return; - } - this.properties = properties; + RangeItem.prototype.hide = function () { + if (this.displayed) { + var box = this.dom.box; - var fields = ["borderWidth", "borderWidthSelected", "shape", "image", "brokenImage", "radius", "fontColor", "fontSize", "fontFace", "fontFill", "fontStrokeWidth", "fontStrokeColor", "group", "mass", "fontDrawThreshold", "scaleFontWithValue", "fontSizeMaxVisible", "customScalingFunction", "iconFontFace", "icon", "iconColor", "iconSize", "value"]; - util.selectiveDeepExtend(fields, this.options, properties); + if (box.parentNode) { + box.parentNode.removeChild(box); + } - // basic properties - if (properties.id !== undefined) { - this.id = properties.id; - } - if (properties.label !== undefined) { - this.label = properties.label;this.originalLabel = properties.label; - } - if (properties.title !== undefined) { - this.title = properties.title; - } - if (properties.x !== undefined) { - this.x = properties.x;this.predefinedPosition = true; - } - if (properties.y !== undefined) { - this.y = properties.y;this.predefinedPosition = true; - } - if (properties.value !== undefined) { - this.value = properties.value; - } - if (properties.level !== undefined) { - this.level = properties.level;this.preassignedLevel = true; - } + this.top = null; + this.left = null; - // navigation controls properties - if (properties.horizontalAlignLeft !== undefined) { - this.horizontalAlignLeft = properties.horizontalAlignLeft; - } - if (properties.verticalAlignTop !== undefined) { - this.verticalAlignTop = properties.verticalAlignTop; - } - if (properties.triggerFunction !== undefined) { - this.triggerFunction = properties.triggerFunction; + this.displayed = false; } + }; - if (this.id === undefined) { - throw "Node must have an id"; - } + /** + * Reposition the item horizontally + * @Override + */ + 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; - // copy group properties - if (typeof properties.group === "number" || typeof properties.group === "string" && properties.group != "") { - var groupObj = this.grouplist.get(properties.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 properties - if (properties.radius !== undefined) { - this.baseRadiusValue = this.options.radius; + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; } - if (properties.color !== undefined) { - this.options.color = util.parseColor(properties.color); + if (end > 2 * parentWidth) { + end = 2 * parentWidth; } + var boxWidth = Math.max(end - start, 1); - 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"; - } - } + if (this.overflow) { + this.left = start; + this.width = boxWidth + this.props.content.width; + contentWidth = this.props.content.width; - if (properties.allowedToMoveX !== undefined) { - this.xFixed = !properties.allowedToMoveX; - this.allowedToMoveX = properties.allowedToMoveX; - } else if (properties.x !== undefined && this.allowedToMoveX == false) { - this.xFixed = true; + // 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); } + this.dom.box.style.left = this.left + "px"; + this.dom.box.style.width = boxWidth + "px"; - if (properties.allowedToMoveY !== undefined) { - this.yFixed = !properties.allowedToMoveY; - this.allowedToMoveY = properties.allowedToMoveY; - } else if (properties.y !== undefined && this.allowedToMoveY == false) { - this.yFixed = true; - } + switch (this.options.align) { + case "left": + this.dom.content.style.left = "0"; + break; - this.radiusFixed = this.radiusFixed || properties.radius !== undefined; + case "right": + this.dom.content.style.left = Math.max(boxWidth - contentWidth - 2 * this.options.padding, 0) + "px"; + break; - if (this.options.shape === "image" || this.options.shape === "circularImage") { - this.options.radiusMin = constants.nodes.widthMin; - this.options.radiusMax = constants.nodes.widthMax; - } + case "center": + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + "px"; + break; - // choose draw method depending on the shape - switch (this.options.shape) { - case "database": - this.draw = this._drawDatabase;this.resize = this._resizeDatabase;break; - case "box": - this.draw = this._drawBox;this.resize = this._resizeBox;break; - case "circle": - this.draw = this._drawCircle;this.resize = this._resizeCircle;break; - case "ellipse": - this.draw = this._drawEllipse;this.resize = this._resizeEllipse;break; - // TODO: add diamond shape - case "image": - this.draw = this._drawImage;this.resize = this._resizeImage;break; - case "circularImage": - this.draw = this._drawCircularImage;this.resize = this._resizeCircularImage;break; - case "text": - this.draw = this._drawText;this.resize = this._resizeText;break; - case "dot": - this.draw = this._drawDot;this.resize = this._resizeShape;break; - case "square": - this.draw = this._drawSquare;this.resize = this._resizeShape;break; - case "triangle": - this.draw = this._drawTriangle;this.resize = this._resizeShape;break; - case "triangleDown": - this.draw = this._drawTriangleDown;this.resize = this._resizeShape;break; - case "star": - this.draw = this._drawStar;this.resize = this._resizeShape;break; - case "icon": - this.draw = this._drawIcon;this.resize = this._resizeIcon;break; default: - this.draw = this._drawEllipse;this.resize = this._resizeEllipse;break; + // '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"; } - // reset the size of the node, this can be changed - this._reset(); }; /** - * select this node + * Reposition the item vertically + * @Override */ - Node.prototype.select = function () { - this.selected = true; - this._reset(); + RangeItem.prototype.repositionY = function () { + var orientation = this.options.orientation, + box = this.dom.box; + + if (orientation == "top") { + box.style.top = this.top + "px"; + } else { + box.style.top = this.parent.height - this.top - this.height + "px"; + } }; /** - * unselect this node + * Repaint a drag area on the left side of the range when the range is selected + * @protected */ - Node.prototype.unselect = function () { - this.selected = false; - this._reset(); - }; + 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') + // }); - /** - * Reset the calculated size of the node, forces it to recalculate its size - */ - Node.prototype.clearSizeCache = function () { - this._reset(); + 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; + } }; /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private + * Repaint a drag area on the right side of the range when the range is selected + * @protected */ - Node.prototype._reset = function () { - this.width = undefined; - this.height = undefined; - }; + 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; - /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. - */ - Node.prototype.getTitle = function () { - return typeof this.title === "function" ? this.title() : this.title; + //// 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; + } }; - /** - * Calculate the distance to the border of the Node - * @param {CanvasRenderingContext2D} ctx - * @param {Number} angle Angle in radians - * @returns {number} distance Distance to the border in pixels - */ - Node.prototype.distanceToBorder = function (ctx, angle) { - var borderWidth = 1; + module.exports = RangeItem; - if (!this.width) { - this.resize(ctx); - } +/***/ }, +/* 33 */ +/***/ function(module, exports, __webpack_require__) { - switch (this.options.shape) { - case "circle": - case "dot": - return this.options.radius + borderWidth; + "use strict"; - case "ellipse": - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + var Hammer = __webpack_require__(19); + var util = __webpack_require__(1); - // TODO: implement distanceToBorder for database - // TODO: implement distanceToBorder for triangle - // TODO: implement distanceToBorder for triangleDown + /** + * @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 || {}; - case "box": - case "image": - case "text": - default: - if (this.width) { - return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; - // TODO: reckon with border radius too in case of box - } else { - return 0; - } + this.selected = false; + this.displayed = false; + this.dirty = true; - } - // TODO: implement calculation of distance to border for all shapes - }; + this.top = null; + this.left = null; + this.width = null; + this.height = null; + } + Item.prototype.stack = true; /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not + * Select current item */ - Node.prototype.isFixed = function () { - return this.xFixed && this.yFixed; + Item.prototype.select = function () { + this.selected = true; + this.dirty = true; + if (this.displayed) this.redraw(); }; /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false + * Unselect current item */ - Node.prototype.isSelected = function () { - return this.selected; + Item.prototype.unselect = function () { + this.selected = false; + this.dirty = true; + if (this.displayed) this.redraw(); }; /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value + * 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 */ - Node.prototype.getValue = function () { - return this.value; + Item.prototype.setData = function (data) { + this.data = data; + this.dirty = true; + if (this.displayed) this.redraw(); }; /** - * Calculate the distance from the nodes location to the given location (x,y) - * @param {Number} x - * @param {Number} y - * @return {Number} value + * Set a parent for the item + * @param {ItemSet | Group} parent */ - Node.prototype.getDistance = function (x, y) { - var dx = this.x - x, - dy = this.y - y; - return Math.sqrt(dx * dx + dy * dy); + Item.prototype.setParent = function (parent) { + if (this.displayed) { + this.hide(); + this.parent = parent; + if (this.parent) { + this.show(); + } + } else { + this.parent = parent; + } }; - /** - * Adjust the value range of the node. The node will adjust it's radius - * based on its value. - * @param {Number} min - * @param {Number} max + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible */ - Node.prototype.setValueRange = function (min, max, total) { - if (!this.radiusFixed && this.value !== undefined) { - var scale = this.options.customScalingFunction(min, max, total, this.value); - var radiusDiff = this.options.radiusMax - this.options.radiusMin; - if (this.options.scaleFontWithValue == true) { - var fontDiff = this.options.fontSizeMax - this.options.fontSizeMin; - this.options.fontSize = this.options.fontSizeMin + scale * fontDiff; - } - this.options.radius = this.options.radiusMin + scale * radiusDiff; - } - - this.baseRadiusValue = this.options.radius; + Item.prototype.isVisible = function (range) { + // Should be implemented by Item implementations + return false; }; /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed */ - Node.prototype.draw = function (ctx) { - throw "Draw method not initialized for node"; + Item.prototype.show = function () { + return false; }; /** - * 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 + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed */ - Node.prototype.resize = function (ctx) { - throw "Resize method not initialized for node"; + Item.prototype.hide = function () { + return false; }; /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node + * Repaint the item */ - Node.prototype.isOverlappingWith = function (obj) { - return this.left < obj.right && this.left + this.width > obj.left && this.top < obj.bottom && this.top + this.height > obj.top; - }; + Item.prototype.redraw = function () {}; - Node.prototype._resizeImage = function (ctx) { - // TODO: pre calculate the image size + /** + * Reposition the Item horizontally + */ + Item.prototype.repositionX = function () {}; - if (!this.width || !this.height) { - // undefined or 0 - var width, height; - if (this.value) { - this.options.radius = this.baseRadiusValue; - var scale = this.imageObj.height / this.imageObj.width; - if (scale !== undefined) { - width = this.options.radius || this.imageObj.width; - height = this.options.radius * scale || this.imageObj.height; - } else { - width = 0; - height = 0; - } - } else { - width = this.imageObj.width; - height = this.imageObj.height; - } - this.width = width; - this.height = height; - } - }; + /** + * Reposition the Item vertically + */ + Item.prototype.repositionY = function () {}; - Node.prototype._drawImageAtPosition = function (ctx) { - if (this.imageObj.width != 0) { - // draw the image - ctx.globalAlpha = 1; - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); - } - }; + /** + * 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; - Node.prototype._drawImageLabel = function (ctx) { - var yLabel; - var offset = 0; + var deleteButton = document.createElement("div"); + deleteButton.className = "delete"; + deleteButton.title = "Delete this item"; - if (this.height) { - offset = this.height / 2; - var labelDimensions = this.getTextSize(ctx); + // TODO: be able to destroy the delete button + new Hammer(deleteButton).on("tap", function (event) { + me.parent.removeFromDataSet(me); + event.stopPropagation(); + event.preventDefault(); + }); - if (labelDimensions.lineCount >= 1) { - offset += labelDimensions.height / 2; - offset += 3; + 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; } - - yLabel = this.y + offset; - - this._label(ctx, this.label, this.x, yLabel, undefined); }; - Node.prototype._drawImage = function (ctx) { - this._resizeImage(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - this._drawImageAtPosition(ctx); + /** + * 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; + } - 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 (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._drawImageLabel(ctx); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + this.content = content; + } }; - Node.prototype._resizeCircularImage = function (ctx) { - if (!this.imageObj.src || !this.imageObj.width || !this.imageObj.height) { - if (!this.width) { - var diameter = this.options.radius * 2; - this.width = diameter; - this.height = diameter; - this._swapToImageResizeWhenImageLoaded = true; - } + /** + * 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 { - if (this._swapToImageResizeWhenImageLoaded) { - this.width = 0; - this.height = 0; - delete this._swapToImageResizeWhenImageLoaded; - } - this._resizeImage(ctx); + element.removeAttribute("title"); } }; - Node.prototype._drawCircularImage = function (ctx) { - this._resizeCircularImage(ctx); - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - - var centerX = this.left + this.width / 2; - var centerY = this.top + this.height / 2; - var radius = Math.abs(this.height / 2); - - this._drawRawCircle(ctx, centerX, centerY, radius); - - ctx.save(); - ctx.circle(this.x, this.y, radius); - ctx.stroke(); - ctx.clip(); - - this._drawImageAtPosition(ctx); - - ctx.restore(); - - this.boundingBox.top = this.y - this.options.radius; - this.boundingBox.left = this.x - this.options.radius; - this.boundingBox.right = this.x + this.options.radius; - this.boundingBox.bottom = this.y + this.options.radius; + /** + * 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 = []; - this._drawImageLabel(ctx); + if (Array.isArray(this.options.dataAttributes)) { + attributes = this.options.dataAttributes; + } else if (this.options.dataAttributes == "all") { + attributes = Object.keys(this.data); + } else { + return; + } - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); - }; + for (var i = 0; i < attributes.length; i++) { + var name = attributes[i]; + var value = this.data[name]; - Node.prototype._resizeBox = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + if (value != null) { + element.setAttribute("data-" + name, value); + } else { + element.removeAttribute("data-" + name); + } + } } }; - Node.prototype._drawBox = function (ctx) { - this._resizeBox(ctx); - - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + /** + * 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; + } - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + // append new styles + if (this.data.style) { + util.addCssText(element, this.data.style); + this.style = this.data.style; + } + }; - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.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); + module.exports = Item; + // should be implemented by the item + // should be implemented by the item + // should be implemented by the item - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; +/***/ }, +/* 34 */ +/***/ function(module, exports, __webpack_require__) { - ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius); - ctx.fill(); - ctx.stroke(); + "use strict"; - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + var util = __webpack_require__(1); + var Group = __webpack_require__(30); - this._label(ctx, this.label, this.x, this.y); - }; + /** + * @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; + } - Node.prototype._resizeDatabase = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; - } - }; + BackgroundGroup.prototype = Object.create(Group.prototype); - Node.prototype._drawDatabase = function (ctx) { - this._resizeDatabase(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + /** + * 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; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.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); + // calculate actual size + this.width = this.dom.background.offsetWidth; - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx.database(this.x - this.width / 2, this.y - this.height * 0.5, this.width, this.height); - ctx.fill(); - ctx.stroke(); + // apply new height (just always zero for BackgroundGroup + this.dom.background.style.height = "0"; - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + // 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); + } - this._label(ctx, this.label, this.x, this.y); + return resized; }; - - Node.prototype._resizeCircle = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.options.radius = diameter / 2; - - this.width = diameter; - this.height = diameter; + /** + * Show this group: attach to the DOM + */ + BackgroundGroup.prototype.show = function () { + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); } }; - Node.prototype._drawRawCircle = function (ctx, x, y, radius) { - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + module.exports = BackgroundGroup; - ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); +/***/ }, +/* 35 */ +/***/ function(module, exports, __webpack_require__) { - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(this.x, this.y, radius); - ctx.fill(); - ctx.stroke(); - }; + "use strict"; - Node.prototype._drawCircle = function (ctx) { - this._resizeCircle(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + var Item = __webpack_require__(33); + var util = __webpack_require__(1); - this._drawRawCircle(ctx, this.x, this.y, this.options.radius); + /** + * @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 + } + }; - this.boundingBox.top = this.y - this.options.radius; - this.boundingBox.left = this.x - this.options.radius; - this.boundingBox.right = this.x + this.options.radius; - this.boundingBox.bottom = this.y + this.options.radius; + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data); + } + } - this._label(ctx, this.label, this.x, this.y); - }; + Item.call(this, data, conversion, options); + } - Node.prototype._resizeEllipse = function (ctx) { - if (!this.width) { - var textSize = this.getTextSize(ctx); + BoxItem.prototype = new Item(null, null, null); - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } - var defaultSize = this.width; - } + /** + * 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; }; - Node.prototype._drawEllipse = function (ctx) { - this._resizeEllipse(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + /** + * Repaint the item + */ + BoxItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + // create main box + dom.box = document.createElement("DIV"); - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + // contents box (inside the background box). used for making margins + dom.content = document.createElement("DIV"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + // line to axis + dom.line = document.createElement("DIV"); + dom.line.className = "line"; - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + // dot on axis + dom.dot = document.createElement("DIV"); + dom.dot.className = "dot"; - ctx.ellipse(this.left, this.top, this.width, this.height); - ctx.fill(); - ctx.stroke(); + // attach this item as attribute + dom.box["timeline-item"] = this; - 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.dirty = true; + } - this._label(ctx, this.label, this.x, this.y); - }; + // 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; - Node.prototype._drawDot = function (ctx) { - this._drawShape(ctx, "circle"); - }; + // 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); - Node.prototype._drawTriangle = function (ctx) { - this._drawShape(ctx, "triangle"); - }; + // 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; - Node.prototype._drawTriangleDown = function (ctx) { - this._drawShape(ctx, "triangleDown"); - }; + // 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; - Node.prototype._drawSquare = function (ctx) { - this._drawShape(ctx, "square"); - }; + this.dirty = false; + } - Node.prototype._drawStar = function (ctx) { - this._drawShape(ctx, "star"); + this._repaintDeleteButton(dom.box); }; - Node.prototype._resizeShape = function (ctx) { - if (!this.width) { - this.options.radius = this.baseRadiusValue; - var size = 2 * this.options.radius; - this.width = size; - this.height = size; + /** + * 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(); } }; - Node.prototype._drawShape = function (ctx, shape) { - this._resizeShape(ctx); + /** + * Hide the item from the DOM (when visible) + */ + BoxItem.prototype.hide = function () { + if (this.displayed) { + var dom = this.dom; - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + 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); - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - var radiusMultiplier = 2; + this.top = null; + this.left = null; - // choose draw method depending on the shape - switch (shape) { - case "dot": - radiusMultiplier = 2;break; - case "square": - radiusMultiplier = 2;break; - case "triangle": - radiusMultiplier = 3;break; - case "triangleDown": - radiusMultiplier = 3;break; - case "star": - radiusMultiplier = 4;break; + this.displayed = false; } + }; - ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.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); + /** + * 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; - ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](this.x, this.y, this.options.radius); - ctx.fill(); - ctx.stroke(); + // 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; + } - this.boundingBox.top = this.y - this.options.radius; - this.boundingBox.left = this.x - this.options.radius; - this.boundingBox.right = this.x + this.options.radius; - this.boundingBox.bottom = this.y + this.options.radius; + // reposition box + box.style.left = this.left + "px"; - if (this.label) { - this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, "hanging", true); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); - } - }; + // reposition line + line.style.left = start - this.props.line.width / 2 + "px"; - Node.prototype._resizeText = function (ctx) { - if (!this.width) { - var margin = 5; - var textSize = this.getTextSize(ctx); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - } + // reposition dot + dot.style.left = start - this.props.dot.width / 2 + "px"; }; - Node.prototype._drawText = function (ctx) { - this._resizeText(ctx); - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; + /** + * 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; - this._label(ctx, this.label, this.x, this.y); + if (orientation == "top") { + box.style.top = (this.top || 0) + "px"; - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - }; + 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; - Node.prototype._resizeIcon = function (ctx) { - if (!this.width) { - var margin = 5; - var iconSize = { - width: Number(this.options.iconSize), - height: Number(this.options.iconSize) - }; - this.width = iconSize.width + 2 * margin; - this.height = iconSize.height + 2 * margin; + box.style.top = (this.parent.height - this.top - this.height || 0) + "px"; + line.style.top = itemSetHeight - lineHeight + "px"; + line.style.bottom = "0"; } - }; - Node.prototype._drawIcon = function (ctx) { - this._resizeIcon(ctx); + dot.style.top = -this.props.dot.height / 2 + "px"; + }; - this.options.iconSize = this.options.iconSize || 50; + module.exports = BoxItem; - this.left = this.x - this.width / 2; - this.top = this.y - this.height / 2; - this._icon(ctx); +/***/ }, +/* 36 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; - this.boundingBox.top = this.y - this.options.iconSize / 2; - this.boundingBox.left = this.x - this.options.iconSize / 2; - this.boundingBox.right = this.x + this.options.iconSize / 2; - this.boundingBox.bottom = this.y + this.options.iconSize / 2; + var Item = __webpack_require__(33); - if (this.label) { - var iconTextSpacing = 5; - this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, "top", true); + /** + * @constructor PointItem + * @extends Item + * @param {Object} data Object containing parameters start + * content, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} [options] Configuration options + * // TODO: describe available options + */ + function PointItem(data, conversion, options) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data); + } } + + Item.call(this, data, conversion, options); + } + + PointItem.prototype = new Item(null, null, null); + + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + PointItem.prototype.isVisible = function (range) { + // determine visibility + // TODO: account for the real width of the item. Right now we just add 1/4 to the window + var interval = (range.end - range.start) / 4; + return this.data.start > range.start - interval && this.data.start < range.end + interval; }; - Node.prototype._icon = function (ctx) { - var relativeIconSize = Number(this.options.iconSize) * this.networkScale; + /** + * Repaint the item + */ + PointItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - if (this.options.icon && relativeIconSize > this.options.fontDrawThreshold - 1) { - var iconSize = Number(this.options.iconSize); + // background box + dom.point = document.createElement("div"); + // className is updated in redraw() - ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace; + // contents box, right from the dot + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.point.appendChild(dom.content); - // draw icon - ctx.fillStyle = this.options.iconColor || "black"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.options.icon, this.x, this.y); - } - }; + // dot at start + dom.dot = document.createElement("div"); + dom.point.appendChild(dom.dot); - Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) { - var relativeFontSize = Number(this.options.fontSize) * this.networkScale; - if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) { - var fontSize = Number(this.options.fontSize); + // attach this item as attribute + dom.point["timeline-item"] = this; - // 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 (relativeFontSize >= this.options.fontSizeMaxVisible) { - fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; - } + this.dirty = true; + } - // fade in when relative scale is between threshold and threshold - 1 - var fontColor = this.options.fontColor || "#000000"; - var strokecolor = this.options.fontStrokeColor; - if (relativeFontSize <= this.options.fontDrawThreshold) { - var opacity = Math.max(0, Math.min(1, 1 - (this.options.fontDrawThreshold - relativeFontSize))); - fontColor = util.overrideOpacity(fontColor, opacity); - strokecolor = util.overrideOpacity(strokecolor, opacity); + // 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; - ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.point); + this._updateDataAttributes(this.dom.point); + this._updateStyle(this.dom.point); - var lines = text.split("\n"); - var lineCount = lines.length; - var yLine = y + (1 - lineCount) / 2 * fontSize; - if (labelUnderNode == true) { - yLine = y + (1 - lineCount) / (2 * fontSize); - } + // 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; - // font fill from edges now for nodes! - var 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; - } - var height = fontSize * lineCount; - var left = x - width / 2; - var top = y - height / 2; - if (baseline == "hanging") { - top += 0.5 * fontSize; - top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers - yLine += 4; // distance from node - } - this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; + // 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; - // create the fontfill background - if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { - ctx.fillStyle = this.options.fontFill; - ctx.fillRect(left, top, width, height); - } + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + "px"; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - // draw text - ctx.fillStyle = fontColor; - ctx.textAlign = align || "center"; - ctx.textBaseline = baseline || "middle"; - if (this.options.fontStrokeWidth > 0) { - ctx.lineWidth = this.options.fontStrokeWidth; - ctx.strokeStyle = strokecolor; - ctx.lineJoin = "round"; - } - for (var i = 0; i < lineCount; i++) { - if (this.options.fontStrokeWidth) { - ctx.strokeText(lines[i], x, yLine); - } - ctx.fillText(lines[i], x, yLine); - yLine += fontSize; - } + 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); }; + /** + * 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(); + } + }; - Node.prototype.getTextSize = function (ctx) { - if (this.label !== undefined) { - var fontSize = Number(this.options.fontSize); - if (fontSize * this.networkScale > this.options.fontSizeMaxVisible) { - fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; + /** + * Hide the item from the DOM (when visible) + */ + PointItem.prototype.hide = function () { + if (this.displayed) { + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); } - ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; - - var lines = this.label.split("\n"), - height = (fontSize + 4) * lines.length, - width = 0; - for (var i = 0, iMax = lines.length; i < iMax; i++) { - width = Math.max(width, ctx.measureText(lines[i]).width); - } + this.top = null; + this.left = null; - return { width: width, height: height, lineCount: lines.length }; - } else { - return { width: 0, height: 0, lineCount: 0 }; + this.displayed = false; } }; + /** + * Reposition the item horizontally + * @Override + */ + PointItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); + + this.left = start - this.props.dot.width; + + // reposition point + this.dom.point.style.left = this.left + "px"; + }; /** - * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. - * there is a safety margin of 0.3 * width; - * - * @returns {boolean} + * Reposition the item vertically + * @Override */ - Node.prototype.inArea = function () { - if (this.width !== undefined) { - return this.x + this.width * this.networkScaleInv >= this.canvasTopLeft.x && this.x - this.width * this.networkScaleInv < this.canvasBottomRight.x && this.y + this.height * this.networkScaleInv >= this.canvasTopLeft.y && this.y - this.height * this.networkScaleInv < this.canvasBottomRight.y; + PointItem.prototype.repositionY = function () { + var orientation = this.options.orientation, + point = this.dom.point; + + if (orientation == "top") { + point.style.top = this.top + "px"; } else { - return true; + point.style.top = this.parent.height - this.top - this.height + "px"; } }; + module.exports = PointItem; - /** - * This allows the zoom level of the network to influence the rendering - * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas - * - * @param scale - * @param canvasTopLeft - * @param canvasBottomRight - */ - Node.prototype.setScaleAndPos = function (scale, canvasTopLeft, canvasBottomRight) { - this.networkScaleInv = 1 / scale; - this.networkScale = scale; - this.canvasTopLeft = canvasTopLeft; - this.canvasBottomRight = canvasBottomRight; - }; +/***/ }, +/* 37 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + var Hammer = __webpack_require__(19); + var Item = __webpack_require__(33); + var BackgroundGroup = __webpack_require__(34); + var RangeItem = __webpack_require__(32); /** - * This allows the zoom level of the network to influence the rendering - * - * @param scale + * @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 */ - Node.prototype.setScale = function (scale) { - this.networkScaleInv = 1 / scale; - this.networkScale = scale; - }; + // TODO: implement support for the BackgroundItem just having a start, then being displayed as a sort of an annotation + function BackgroundItem(data, conversion, options) { + this.props = { + content: { + width: 0 + } + }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true + + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data.id); + } + if (data.end == undefined) { + throw new Error("Property \"end\" missing in item " + data.id); + } + } + + Item.call(this, data, conversion, options); + this.emptyContent = false; + } + + BackgroundItem.prototype = new Item(null, null, null); + BackgroundItem.prototype.baseClassName = "item background"; + BackgroundItem.prototype.stack = false; /** - * set the velocity at 0. Is called when this node is contained in another during clustering + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible */ - Node.prototype.clearVelocity = function () { - this.vx = 0; - this.vy = 0; + BackgroundItem.prototype.isVisible = function (range) { + // determine visibility + return this.data.start < range.end && this.data.end > range.start; }; - /** - * Basic preservation of (kinectic) energy - * - * @param massBeforeClustering + * Repaint the item */ - Node.prototype.updateVelocity = function (massBeforeClustering) { - var energyBefore = this.vx * this.vx * massBeforeClustering; - //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); - this.vx = Math.sqrt(energyBefore / this.options.mass); - energyBefore = this.vy * this.vy * massBeforeClustering; - //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); - this.vy = Math.sqrt(energyBefore / this.options.mass); - }; + BackgroundItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - module.exports = Node; + // background box + dom.box = document.createElement("div"); + // className is updated in redraw() -/***/ }, -/* 41 */ -/***/ function(module, exports, __webpack_require__) { + // contents box + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - "use strict"; + // Note: we do NOT attach this item as attribute to the DOM, + // such that background items cannot be selected + //dom.box['timeline-item'] = this; - /** - * Popup is a class to create a popup window with some text - * @param {Element} container The container object. - * @param {Number} [x] - * @param {Number} [y] - * @param {String} [text] - * @param {Object} [style] An object containing borderColor, - * backgroundColor, etc. - */ - function Popup(container, x, y, text, style) { - if (container) { - this.container = container; - } else { - this.container = document.body; + this.dirty = true; } - // 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" - } - }; + // append DOM to parent DOM + if (!this.parent) { + throw new Error("Cannot redraw item: no parent attached"); + } + if (!dom.box.parentNode) { + var background = this.parent.dom.background; + if (!background) { + throw new Error("Cannot redraw item: parent has no background container element"); } + background.appendChild(dom.box); } + this.displayed = true; - this.x = 0; - this.y = 0; - this.padding = 5; - this.hidden = false; + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.content); + this._updateDataAttributes(this.dom.content); + this._updateStyle(this.dom.box); - if (x !== undefined && y !== undefined) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); + // update class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = this.baseClassName + className; + + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; + + // recalculate size + this.props.content.width = this.dom.content.offsetWidth; + this.height = 0; // set height zero, so this item will be ignored when stacking items + + this.dirty = false; } + }; - // 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); - } + /** + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. + */ + BackgroundItem.prototype.show = RangeItem.prototype.show; /** - * @param {number} x Horizontal position of the popup window - * @param {number} y Vertical position of the popup window + * Hide the item from the DOM (when visible) + * @return {Boolean} changed */ - Popup.prototype.setPosition = function (x, y) { - this.x = parseInt(x); - this.y = parseInt(y); - }; + BackgroundItem.prototype.hide = RangeItem.prototype.hide; /** - * Set the content for the popup window. This can be HTML code or text. - * @param {string | Element} content + * Reposition the item horizontally + * @Override */ - Popup.prototype.setText = function (content) { - if (content instanceof Element) { - this.frame.innerHTML = ""; - this.frame.appendChild(content); - } else { - this.frame.innerHTML = content; // string containing text or HTML - } - }; + BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; /** - * Show the popup window - * @param {boolean} show Optional. Show or hide the window + * Reposition the item vertically + * @Override */ - Popup.prototype.show = function (show) { - if (show === undefined) { - show = true; - } + 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; - if (show) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + // 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; + } + } + } - var top = this.y - height; - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; - } - if (top < this.padding) { - top = this.padding; + // 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 = ""; } - - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; + // 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 = ""; } - if (left < this.padding) { - left = this.padding; + } + // 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.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; - this.hidden = false; - } else { - this.hide(); } + this.dom.box.style.height = height + "px"; }; - /** - * Hide the popup window - */ - Popup.prototype.hide = function () { - this.hidden = true; - this.frame.style.visibility = "hidden"; - }; - - module.exports = Popup; + module.exports = BackgroundItem; /***/ }, -/* 42 */ +/* 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); + /** - * 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 + * Turn an element into an clickToUse element. + * When not active, the element has a transparent overlay. When the overlay is + * clicked, the mode is changed to active. + * When active, the element is displayed with a blue border around it, and + * the interactive contents of the element can be used. When clicked outside + * the element, the elements mode is changed to inactive. + * @param {Element} container + * @constructor */ - function parseDOT(data) { - dot = data; - return parseGraph(); - } + function Activator(container) { + this.active = false; - // token types enumeration - var TOKENTYPE = { - NULL: 0, - DELIMITER: 1, - IDENTIFIER: 2, - UNKNOWN: 3 - }; + this.dom = { + container: container + }; - // map with all delimiters - var DELIMITERS = { - "{": true, - "}": true, - "[": true, - "]": true, - ";": true, - "=": true, - ",": true, + this.dom.overlay = document.createElement("div"); + this.dom.overlay.className = "overlay"; - "->": true, - "--": true - }; + this.dom.container.appendChild(this.dom.overlay); - 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.hammer = Hammer(this.dom.overlay, { prevent_default: false }); + this.hammer.on("tap", this._onTapOverlay.bind(this)); - /** - * 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); - } + // 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(); + }); + }); - /** - * 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); - } + // 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(); + } + }); - /** - * Preview the next character from the dot file. - * @return {String} cNext - */ - function nextPreview() { - return dot.charAt(index + 1); - } + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } + this.keycharm = keycharm(); - /** - * 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); + // keycharm listener only bounded when active) + this.escListener = this.deactivate.bind(this); } - /** - * Merge all properties of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - function merge(a, b) { - if (!a) { - a = {}; - } + // turn into an event emitter + Emitter(Activator.prototype); - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; - } - } - } - return a; - } + // The currently active activator + Activator.current = null; /** - * 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 + * Destroy the activator. Cleans up all created DOM and event listeners */ - 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; - } - } - } + Activator.prototype.destroy = function () { + this.deactivate(); + + // remove dom + this.dom.overlay.parentNode.removeChild(this.dom.overlay); + + // cleanup hammer instances + this.hammer = null; + this.windowHammer = null; + // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + }; /** - * 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 + * Activate the element + * Overlay is hidden, element is decorated with a blue shadow border */ - 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; + Activator.prototype.activate = function () { + // we allow only one active activator at a time + if (Activator.current) { + Activator.current.deactivate(); } + Activator.current = this; - // 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.active = true; + this.dom.overlay.style.display = "none"; + util.addClassName(this.dom.container, "vis-active"); - 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.emit("change"); + this.emit("activate"); - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; + // 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); + }; - if (!g.nodes) { - g.nodes = []; - } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); - } - } + /** + * Deactivate the element + * Overlay is displayed on top of the element + */ + Activator.prototype.deactivate = function () { + this.active = false; + this.dom.overlay.style.display = ""; + util.removeClassName(this.dom.container, "vis-active"); + this.keycharm.unbind("esc", this.escListener); - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); - } - } + this.emit("change"); + this.emit("deactivate"); + }; /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge + * Handle a tap event: activate the container + * @param event + * @private */ - 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 + Activator.prototype._onTapOverlay = function (event) { + // activate the container + this.activate(); + event.stopPropagation(); + }; + + /** + * Test whether the element has the requested parent element somewhere in + * its chain of parent nodes. + * @param {HTMLElement} element + * @param {HTMLElement} parent + * @returns {boolean} Returns true when the parent is found somewhere in the + * chain of parent nodes. + * @private + */ + function _hasParent(element, parent) { + while (element) { + if (element === parent) { + return true; + } + element = element.parentNode; } + return false; } + module.exports = Activator; + +/***/ }, +/* 39 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; /** - * 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 + * Created by Alex on 11/6/2014. */ - 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 + // 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(); } - edge.attr = merge(edge.attr || {}, attr); // merge attributes + }(this, function () { - return edge; - } + function keycharm(options) { + var preventDefault = options && options.preventDefault || false; - /** - * 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 = ""; + var container = options && options.container || window; + var _exportFunctions = {}; + var _bound = {keydown:{}, keyup:{}}; + var _keys = {}; + var i; - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); - } + // 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};} - do { - var isComment = 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}; - // 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(); + + + var down = function(event) {handleEvent(event,'keydown');}; + var up = function(event) {handleEvent(event,'keyup');}; + + // handle the actualy bound key with the event + var handleEvent = function(event,type) { + if (_bound[type][event.keyCode] !== undefined) { + var bound = _bound[type][event.keyCode]; + for (var i = 0; i < bound.length; i++) { + if (bound[i].shift === undefined) { + bound[i].fn(event); + } + else if (bound[i].shift == true && event.shiftKey == true) { + bound[i].fn(event); + } + else if (bound[i].shift == false && event.shiftKey == false) { + bound[i].fn(event); + } + } + + if (preventDefault == true) { + event.preventDefault(); } - isComment = true; } - } - if (c == "/" && nextPreview() == "/") { - // skip line comment - while (c != "" && c != "\n") { - next(); + }; + + // bind a key to a callback + _exportFunctions.bind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; } - isComment = true; - } - if (c == "/" && nextPreview() == "*") { - // skip block comment - while (c != "") { - if (c == "*" && nextPreview() == "/") { - // end of block comment found. skip these last two characters - next(); - next(); - break; - } else { - next(); + 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}); + }; + + + // 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); } } - isComment = true; - } + }; - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); - } - } while (isComment); + // 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"; + }; - // check for end of dot file - if (c == "") { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; - } + // 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] = []; + } + }; - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; - next(); - next(); - return; - } + // reset all bound variables. + _exportFunctions.reset = function() { + _bound = {keydown:{}, keyup:{}}; + }; - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; - next(); - return; - } + // unbind all listeners and reset all variables. + _exportFunctions.destroy = function() { + _bound = {keydown:{}, keyup:{}}; + container.removeEventListener('keydown', down, true); + container.removeEventListener('keyup', up, true); + }; - // 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(); + // create listeners. + container.addEventListener('keydown',down,true); + container.addEventListener('keyup',up,true); - 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; + // return the public functions. + return _exportFunctions; } - // 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; - } + return keycharm; + })); - // something unknown is found, wrong characters, a syntax error - tokenType = TOKENTYPE.UNKNOWN; - while (c != "") { - token += c; - next(); - } - throw new SyntaxError("Syntax error in part \"" + chop(token, 30) + "\""); - } - /** - * Parse a graph. - * @returns {Object} graph - */ - function parseGraph() { - var graph = {}; - first(); - getToken(); - // optional strict keyword - if (token == "strict") { - graph.strict = true; - getToken(); - } +/***/ }, +/* 40 */ +/***/ function(module, exports, __webpack_require__) { - // graph or digraph keyword - if (token == "graph" || token == "digraph") { - graph.type = token; - getToken(); - } + "use strict"; - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); - } + 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); - // open angle bracket - if (token != "{") { - throw newSyntaxError("Angle bracket { expected"); - } - getToken(); + /** + * A custom time bar + * @param {{range: Range, dom: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCustomTime] + * @constructor CustomTime + * @extends Component + */ - // statements - parseStatements(graph); + function CustomTime(body, options) { + this.body = body; - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); - } - getToken(); + // default options + this.defaultOptions = { + showCustomTime: false, + locales: locales, + locale: "en", + id: 0 + }; + this.options = util.extend({}, this.defaultOptions); - // end of file - if (token !== "") { - throw newSyntaxError("End of file expected"); + if (options && options.time) { + this.customTime = options.time; + } else { + this.customTime = new Date(); } - getToken(); - // remove temporary default properties - delete graph.node; - delete graph.edge; - delete graph.graph; + this.eventParams = {}; // stores state parameters while dragging the bar - return graph; - } + // create the DOM + this._create(); - /** - * Parse a list with statements. - * @param {Object} graph - */ - function parseStatements(graph) { - while (token !== "" && token != "}") { - parseStatement(graph); - if (token == ";") { - getToken(); - } - } + this.setOptions(options); } + CustomTime.prototype = new Component(); + /** - * 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 + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCustomTime] */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); - - return; - } - - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { - return; - } - - // parse node - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier expected"); - } - var id = token; // id can be a string or a number - getToken(); + CustomTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(["showCustomTime", "locale", "locales", "id"], this.options, options); - if (token == "=") { - // id statement - getToken(); - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier expected"); + // Triggered by addCustomTimeBar, redraw to add new bar + if (this.options.id) { + this.redraw(); } - 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 + * Create the DOM for the custom time + * @private */ - function parseSubgraph(graph) { - var subgraph = null; + 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; - // optional subgraph keyword - if (token == "subgraph") { - subgraph = {}; - subgraph.type = "subgraph"; - getToken(); + 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); - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); - } - } + // 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(); + }); + }; - // open angle bracket - if (token == "{") { - getToken(); + /** + * Destroy the CustomTime bar + */ + CustomTime.prototype.destroy = function () { + this.options.showCustomTime = false; + this.redraw(); // will remove the bar from the DOM - if (!subgraph) { - subgraph = {}; - } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; + this.hammer.enable(false); + this.hammer = null; - // statements - parseStatements(subgraph); + this.body = null; + }; - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + 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); } - getToken(); - // remove temporary default properties - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; + var x = this.body.util.toScreen(this.customTime); - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; + var locale = this.options.locales[this.options.locale]; + var title = locale.time + ": " + moment(this.customTime).format("dddd, MMMM Do YYYY, H:mm:ss"); + title = title.charAt(0).toUpperCase() + title.substring(1); + + this.bar.style.left = x + "px"; + this.bar.title = title; + } else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); } - graph.subgraphs.push(subgraph); } - return subgraph; - } + return false; + }; /** - * 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. + * Set custom time. + * @param {Date | number | string} time */ - function parseAttributeStatement(graph) { - // attribute statements - if (token == "node") { - getToken(); - - // node attributes - graph.node = parseAttributeList(); - return "node"; - } else if (token == "edge") { - getToken(); - - // edge attributes - graph.edge = parseAttributeList(); - return "edge"; - } else if (token == "graph") { - getToken(); - - // graph attributes - graph.graph = parseAttributeList(); - return "graph"; - } + CustomTime.prototype.setCustomTime = function (time) { + this.customTime = util.convert(time, "Date"); + this.redraw(); + }; - return null; - } + /** + * Retrieve the current custom time. + * @return {Date} customTime + */ + CustomTime.prototype.getCustomTime = function () { + return new Date(this.customTime.valueOf()); + }; /** - * parse a node statement - * @param {Object} graph - * @param {String | Number} id + * Start moving horizontally + * @param {Event} event + * @private */ - function parseNodeStatement(graph, id) { - // node statement - var node = { - id: id - }; - var attr = parseAttributeList(); - if (attr) { - node.attr = attr; - } - addNode(graph, node); + CustomTime.prototype._onDragStart = function (event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; - // edge statements - parseEdge(graph, id); - } + event.stopPropagation(); + event.preventDefault(); + }; /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node + * Perform moving operating. + * @param {Event} event + * @private */ - function parseEdge(graph, from) { - while (token == "->" || token == "--") { - var to; - var type = token; - getToken(); + CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; - 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(); - } + var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; + var time = this.body.util.toTime(x); - // parse edge attributes - var attr = parseAttributeList(); + this.setCustomTime(time); - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); + // fire a timechange event + this.body.emitter.emit("timechange", { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - from = to; - } - } + event.stopPropagation(); + event.preventDefault(); + }; /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr + * Stop moving operating. + * @param {Event} event + * @private */ - function parseAttributeList() { - var attr = null; + CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; - while (token == "[") { - getToken(); - attr = {}; - while (token !== "" && token != "]") { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute name expected"); - } - var name = token; + // fire a timechanged event + this.body.emitter.emit("timechanged", { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - getToken(); - if (token != "=") { - throw newSyntaxError("Equal sign = expected"); - } - getToken(); + event.stopPropagation(); + event.preventDefault(); + }; - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute value expected"); - } - var value = token; - setValue(attr, name, value); // name can be a path + module.exports = CustomTime; - getToken(); - if (token == ",") { - getToken(); - } - } +/***/ }, +/* 41 */ +/***/ function(module, exports, __webpack_require__) { - if (token != "]") { - throw newSyntaxError("Bracket ] expected"); - } - getToken(); - } + "use strict"; - return attr; - } + // English + exports.en = { + current: "current", + time: "time" + }; + exports.en_EN = exports.en; + exports.en_US = exports.en; - /** - * 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 + ")"); - } + // Dutch + exports.nl = { + custom: "aangepaste", + time: "tijd" + }; + exports.nl_NL = exports.nl; + exports.nl_BE = exports.nl; + +/***/ }, +/* 42 */ +/***/ 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); /** - * Chop off text after a maximum length - * @param {String} text - * @param {Number} maxLength - * @returns {String} + * 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 chop(text, maxLength) { - return text.length <= maxLength ? text : text.substr(0, 27) + "..."; + 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(); + + this.setOptions(options); } + TimeAxis.prototype = new Component(); + /** - * Execute a function fn for each pair of elements in two arrays - * @param {Array | *} array1 - * @param {Array | *} array2 - * @param {function} fn + * Set options for the TimeAxis. + * Parameters will be merged in current options. + * @param {Object} options Available options: + * {string} [orientation] + * {boolean} [showMinorLabels] + * {boolean} [showMajorLabels] */ - function forEach2(array1, array2, fn) { - if (Array.isArray(array1)) { - array1.forEach(function (elem1) { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(elem1, elem2); - }); + 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 { - fn(elem1, array2); + moment.lang(options.locale); } - }); - } else { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } else { - fn(array1, array2); } } - } + }; /** - * Convert a string containing a graph in DOT language into a map containing - * with nodes and edges in the format of graph. - * @param {String} data Text containing a graph in DOT-notation - * @return {Object} graphData + * Create the HTML DOM for the TimeAxis */ - function DOTToGraph(data) { - // parse the DOT file - var dotData = parseDOT(data); - var graphData = { - nodes: [], - edges: [], - options: {} - }; + TimeAxis.prototype._create = function () { + this.dom.foreground = document.createElement("div"); + this.dom.background = document.createElement("div"); - // 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); - }); + 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); } - // 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; - }; + this.body = null; + }; - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } else { - from = { - id: dotEdge.from - }; - } + /** + * 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; - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } else { - to = { - id: dotEdge.to - }; - } + // 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; - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } + // calculate character width and height + this._calculateCharSize(); - 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); - }); + // 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; - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } - }); + // 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); } - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; + return this._isResized() || parentChanged; + }; + + /** + * Repaint major and minor text labels and vertical grid lines + * @private + */ + 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; - return graphData; - } + // 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 = []; - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; + var cur; + var x = 0; + var isMajor; + var xPrev = 0; + var width = 0; + var prevLine; + var xFirstMajorLabel = undefined; + var max = 0; + var className; -/***/ }, -/* 43 */ -/***/ function(module, exports, __webpack_require__) { + step.first(); + while (step.hasNext() && max < 1000) { + max++; - "use strict"; + cur = step.getCurrent(); + isMajor = step.isMajor(); + className = step.getClassName(); - function parseGephi(gephiJSON, options) { - var edges = []; - var nodes = []; - this.options = { - edges: { - inheritColor: true - }, - nodes: { - allowedToMove: false, - parseColor: false + xPrev = x; + x = this.body.util.toScreen(cur); + width = x - xPrev; + if (prevLine) { + prevLine.style.width = width + "px"; } - }; - if (options !== undefined) { - this.options.nodes.allowedToMove = options.allowedToMove | false; - this.options.nodes.parseColor = options.parseColor | false; - this.options.edges.inheritColor = options.inheritColor | true; - } - - 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); - } + if (this.options.showMinorLabels) { + this._repaintMinorText(x, step.getLabelMinor(), orientation, className); + } - 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; + 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 { - node.color = gNode.color !== undefined ? { background: gNode.color, border: gNode.color } : undefined; + prevLine = this._repaintMinorLine(x, orientation, className); } - node.radius = gNode.size; - node.allowedToMoveX = this.options.nodes.allowedToMove; - node.allowedToMoveY = this.options.nodes.allowedToMove; - nodes.push(node); + + step.next(); } - return { nodes: nodes, edges: edges }; - } + // 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 - exports.parseGephi = parseGephi; + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText, orientation, className); + } + } -/***/ }, -/* 44 */ -/***/ function(module, exports, __webpack_require__) { + // 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); + } + } + }); + }; - "use strict"; + /** + * 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(); - // 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__(64); + 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); -/***/ }, -/* 45 */ -/***/ function(module, exports, __webpack_require__) { + label.childNodes[0].nodeValue = text; - "use strict"; + 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 + }; - // 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__(66); - module.exports = propagating(Hammer); - } else { - module.exports = function () { - throw Error("hammer.js is only available in a browser, not in node.js."); - }; - } + /** + * 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 + */ + TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); -/***/ }, -/* 46 */ -/***/ function(module, exports, __webpack_require__) { + 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); - "use strict"; + label.childNodes[0].nodeValue = text; + label.className = "text major " + className; + //label.title = title; // TODO: this is a heavy operation - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(45); - 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__(53); - var DateUtil = __webpack_require__(15); - var CustomTime = __webpack_require__(27); + label.style.top = orientation == "top" ? "0" : this.props.minorLabelHeight + "px"; + label.style.left = x + "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 + * 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 */ - function Core() {} + 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); - // turn Core into an event emitter - Emitter(Core.prototype); + 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"; + + line.className = "grid vertical minor " + className; + + return line; + }; /** - * 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. + * 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 */ - Core.prototype._create = function (container) { - 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"); - - 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"; - - 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); + 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); - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); + var props = this.props; + if (orientation == "top") { + line.style.top = "0"; + } else { + line.style.top = this.body.domProps.top.height + "px"; + } + line.style.left = x - props.majorLineWidth / 2 + "px"; + line.style.height = props.majorLineHeight + "px"; - this.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); + line.className = "grid vertical major " + className; - this.on("rangechange", this.redraw.bind(this)); + return line; + }; - 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(); - } - }); + /** + * Determine the size of text on the axis (both major and minor axis). + * The size is calculated only once and then cached in this.props. + * @private + */ + TimeAxis.prototype._calculateCharSize = function () { + // Note: We calculate char size with every redraw. Size may change, for + // example when any of the timelines parents had display:none for example. - // 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 = {}; + // 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"; - 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; - }); + 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; - // emulate a touch event (emitted before the start of a pan, pinch, tap, or press) - this.hammer.on("hammer.input", (function (event) { - if (event.isFirst) { - if (me.isActive()) { - me.emit("touch", event); - } - } - }).bind(this)); + // 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 onMouseWheel(event) { - if (me.isActive()) { - me.emit("mousewheel", event); - } + this.dom.measureCharMajor.appendChild(document.createTextNode("0")); + this.dom.foreground.appendChild(this.dom.measureCharMajor); } - this.dom.root.addEventListener("mousewheel", onMouseWheel); - this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel); + this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; + this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; + }; - // size properties of each of the panels - this.props = { - root: {}, - background: {}, - centerContainer: {}, - leftContainer: {}, - rightContainer: {}, - center: {}, - left: {}, - right: {}, - top: {}, - bottom: {}, - border: {}, - scrollTop: 0, - scrollTopMin: 0 - }; + module.exports = TimeAxis; - this.redrawCount = 0; +/***/ }, +/* 43 */ +/***/ function(module, exports, __webpack_require__) { - // attach the root panel to the provided container - if (!container) throw new Error("No container provided"); - container.appendChild(this.dom.root); - }; + "use strict"; + + var util = __webpack_require__(1); + var Component = __webpack_require__(25); + var moment = __webpack_require__(2); + var locales = __webpack_require__(41); /** - * 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 + * A current time bar + * @param {{range: Range, dom: Object, domProps: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCurrentTime] + * @constructor CurrentTime + * @extends Component */ - 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); + function CurrentTime(body, options) { + this.body = body; - if ("hiddenDates" in this.options) { - DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); - } + // default options + this.defaultOptions = { + showCurrentTime: true, - 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; - } - } - } + locales: locales, + locale: "en" + }; + this.options = util.extend({}, this.defaultOptions); + this.offset = 0; - // enable/disable autoResize - this._initAutoResize(); - } + this._create(); - // propagate options to all components - this.components.forEach(function (component) { - return component.setOptions(options); - }); + this.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."); - } + CurrentTime.prototype = new Component(); - // redraw everything - this._redraw(); + /** + * 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%"; + + this.bar = bar; + }; + + /** + * Destroy the CurrentTime bar + */ + CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing + + this.body = null; }; /** - * Returns true when the Timeline is active. - * @returns {boolean} + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] */ - Core.prototype.isActive = function () { - return !this.activator || this.activator.active; + CurrentTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(["showCurrentTime", "locale", "locales"], this.options, options); + } }; /** - * Destroy the Core, clean up all DOM elements and event listeners. + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - Core.prototype.destroy = function () { - // unbind datasets - this.clear(); - - // remove all event listeners - this.off(); + 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); - // stop checking for changed size - this._stopAutoResize(); + this.start(); + } - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); - } - this.dom = null; + var now = new Date(new Date().valueOf() + this.offset); + var x = this.body.util.toScreen(now); - // remove Activator - if (this.activator) { - this.activator.destroy(); - delete this.activator; - } + 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); - // cleanup hammer touch events - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - delete this.listeners[event]; + 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.listeners = null; - this.hammer = null; - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - return component.destroy(); - }); - - this.body = null; + return false; }; - /** - * Set a custom time bar - * @param {Date} time - * @param {int} id + * Start auto refreshing the current time bar */ - Core.prototype.setCustomTime = function (time, id) { - if (!this.customTime) { - throw new Error("Cannot get custom time: Custom time bar is not enabled"); - } + CurrentTime.prototype.start = function () { + var me = this; - var barId = id || 0; + function update() { + me.stop(); - this.components.forEach(function (element, index, components) { - if (element instanceof CustomTime && element.options.id === barId) { - element.setCustomTime(time); - } - }); + // determine interval to refresh + var scale = me.body.range.conversion(me.body.domProps.center.width).scale; + var interval = 1 / scale / 10; + if (interval < 30) interval = 30; + if (interval > 1000) interval = 1000; + + me.redraw(); + + // start a renderTimer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); + } + + update(); }; /** - * Retrieve the current custom time. - * @return {Date} customTime - * @param {int} id + * Stop auto refreshing the current time bar */ - Core.prototype.getCustomTime = function (id) { - if (!this.customTime) { - throw new Error("Cannot get custom time: Custom time bar is not enabled"); + CurrentTime.prototype.stop = function () { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; } - - var barId = id || 0, - customTime = this.customTime.getCustomTime(); - - this.components.forEach(function (element, index, components) { - if (element instanceof CustomTime && element.options.id === barId) { - customTime = element.getCustomTime(); - } - }); - - return customTime; }; /** - * 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 + * 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. */ - Core.prototype.addCustomTime = function (time, id) { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); - } + CurrentTime.prototype.setCurrentTime = function (time) { + var t = util.convert(time, "Date").valueOf(); + var now = new Date().valueOf(); + this.offset = t - now; + this.redraw(); + }; - if (time === undefined) { - throw new Error("Time parameter for the custom bar must be provided"); - } + /** + * Get the current time. + * @return {Date} Returns the current time. + */ + CurrentTime.prototype.getCurrentTime = function () { + return new Date(new Date().valueOf() + this.offset); + }; - var ts = util.convert(time, "Date").valueOf(), - numIds, - customTime, - customBarId; + module.exports = CurrentTime; - // 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]; - } +/***/ }, +/* 44 */ +/***/ function(module, exports, __webpack_require__) { - // 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); - }); + "use strict"; - 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"); - } - }); + 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); - customBarId = id; + /** + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Graph2d.setOptions for the available options. + * @constructor + * @extends Core + */ + function Graph2d(container, items, groups, options) { + // if the third element is options, the forth is groups (optionally); + if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) { + var forthArgument = options; + options = groups; + groups = forthArgument; } - this.customBarIds.push(customBarId); + var me = this; + this.defaultOptions = { + start: null, + end: null, - customTime = new CustomTime(this.body, { - showCustomTime: true, - time: ts, - id: customBarId - }); + autoResize: true, - this.components.push(customTime); - this.redraw(); + orientation: "bottom", + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - return customBarId; - }; + // Create the DOM, props, and emitter + this._create(container); - /** - * 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 - */ - Core.prototype.removeCustomTime = function (id) { - var me = this; + // all components listed here will be repainted automatically + this.components = []; - 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.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) } - }); - }; + }; + + // 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); - /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items - */ - Core.prototype.getVisibleItems = function () { - return this.itemSet && this.itemSet.getVisibleItems() || []; - }; + // 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); - /** - * 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.itemsData = null; // DataSet + this.groupsData = null; // DataSet - // clear groups - if (!what || what.groups) { - this.setGroups(null); + // apply options + if (options) { + this.setOptions(options); } - // clear options of timeline and of each of the components - if (!what || what.options) { - this.components.forEach(function (component) { - return component.setOptions(component.defaultOptions); - }); + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } - this.setOptions(this.defaultOptions); // this will also do a redraw + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); } - }; + } + + // Extend the functionality from Core + Graph2d.prototype = new Core(); /** - * 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. + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - Core.prototype.fit = function (options) { - var range = this._getDataRange(); + Graph2d.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - // skip range set if there is no start and end date - if (range.start === null && range.end === null) { - return; + // 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 animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(range.start, range.end, animate); - }; + // set items + this.itemsData = newDataSet; + this.linegraph && this.linegraph.setItems(newDataSet); - /** - * Calculate the data range of the items and applies a 5% window around it. - * @returns {{start: Date | null, end: Date | null}} - * @protected - */ - Core.prototype._getDataRange = function () { - // apply the data range as range - var dataRange = this.getItemRange(); + 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; - // 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 + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); } - start = new Date(start.valueOf() - interval * 0.05); - end = new Date(end.valueOf() + interval * 0.05); } - - 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. + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - 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); + 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 { - animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(start, end, animate); + // turn an array into a dataset + newDataSet = new DataSet(groups); } - }; - - /** - * 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. - */ - Core.prototype.moveTo = function (time, options) { - var interval = this.range.end - this.range.start; - var t = util.convert(time, "Date").valueOf(); - - var start = t - interval / 2; - var end = t + interval / 2; - var animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(start, end, animate); + this.groupsData = newDataSet; + this.linegraph.setGroups(newDataSet); }; /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range + * 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 */ - Core.prototype.getWindow = function () { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) - }; + 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; + } }; /** - * Force a redraw. Can be overridden by implementations of Core + * This checks if the visible option of the supplied group (by ID) is true or false. + * @param groupId + * @returns {*} */ - Core.prototype.redraw = function () { - this._redraw(); + 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; + } }; + /** - * Redraw for internal use. Redraws all components. See also the public - * method redraw. - * @protected + * 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 */ - Core.prototype._redraw = function () { - var resized = false; - var options = this.options; - var props = this.props; - var dom = this.dom; - - if (!dom) return; // when destroyed - - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - - // update class names - if (options.orientation == "top") { - util.addClassName(dom.root, "top"); - util.removeClassName(dom.root, "bottom"); - } else { - util.removeClassName(dom.root, "top"); - util.addClassName(dom.root, "bottom"); + Graph2d.prototype.getItemRange = function () { + var min = null; + var max = null; + + // calculate min from start filed + for (var groupId in this.linegraph.groups) { + if (this.linegraph.groups.hasOwnProperty(groupId)) { + if (this.linegraph.groups[groupId].visible == true) { + for (var i = 0; i < this.linegraph.groups[groupId].itemsData.length; i++) { + var item = this.linegraph.groups[groupId].itemsData[i]; + var value = util.convert(item.x, "Date").valueOf(); + min = min == null ? value : min > value ? value : min; + max = max == null ? value : max < value ? value : max; + } + } + } } - // 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, ""); + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; + }; - // calculate border widths - props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2; - props.border.right = props.border.left; - props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2; - props.border.bottom = props.border.top; - var borderRootHeight = dom.root.offsetHeight - dom.root.clientHeight; - var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth; - // workaround for a bug in IE: the clientWidth of an element with - // a height:0px and overflow:hidden is not calculated and always has value 0 - if (dom.centerContainer.clientHeight === 0) { - props.border.left = props.border.top; - props.border.right = props.border.left; - } - if (dom.root.clientHeight === 0) { - borderRootWidth = borderRootHeight; - } - // calculate the heights. If any of the side panels is empty, we set the height to - // minus the border width, such that the border will be invisible - props.center.height = dom.center.offsetHeight; - props.left.height = dom.left.offsetHeight; - props.right.height = dom.right.offsetHeight; - props.top.height = dom.top.clientHeight || -props.border.top; - props.bottom.height = dom.bottom.clientHeight || -props.border.bottom; + module.exports = Graph2d; - // TODO: compensate borders when any of the panels is empty. +/***/ }, +/* 45 */ +/***/ function(module, exports, __webpack_require__) { - // apply auto height - // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM) - var contentHeight = Math.max(props.left.height, props.center.height, props.right.height); - var autoHeight = props.top.height + contentHeight + props.bottom.height + borderRootHeight + props.border.top + props.border.bottom; - dom.root.style.height = util.option.asSize(options.height, autoHeight + "px"); + "use strict"; - // calculate heights of the content panels - props.root.height = dom.root.offsetHeight; - props.background.height = props.root.height - borderRootHeight; - var containerHeight = props.root.height - props.top.height - props.bottom.height - borderRootHeight; - props.centerContainer.height = containerHeight; - props.leftContainer.height = containerHeight; - props.rightContainer.height = props.leftContainer.height; + var util = __webpack_require__(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); - // 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; + var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items - // 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"; + /** + * 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; - 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.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: {} + } + }; - // 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"; + // 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; - // 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(); + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - // 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"; + // 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); + } + }; - // 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; + // 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); + } + }; - // redraw all components - this.components.forEach(function (component) { - resized = component.redraw() || resized; + 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); }); - 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; - } - this.emit("finishedRedraw"); - }; + // 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"); + } - // TODO: deprecated since version 1.1.0, remove some day - Core.prototype.repaint = function () { - throw new Error("Function repaint is deprecated. Use redraw instead."); - }; + LineGraph.prototype = new 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. + * Create the HTML DOM for the ItemSet */ - Core.prototype.setCurrentTime = function (time) { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); - } + LineGraph.prototype._create = function () { + var frame = document.createElement("div"); + frame.className = "LineGraph"; + this.dom.frame = frame; - this.currentTime.setCurrentTime(time); + // create svg element for graph drawing. + this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.svg.style.position = "relative"; + this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; + this.svg.style.display = "block"; + frame.appendChild(this.svg); + + // data axis + this.options.dataAxis.orientation = "left"; + this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); + + this.options.dataAxis.orientation = "right"; + this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); + delete this.options.dataAxis.orientation; + + // legends + this.legendLeft = new Legend(this.body, this.options.legend, "left", this.options.groups); + this.legendRight = new Legend(this.body, this.options.legend, "right", this.options.groups); + + this.show(); }; /** - * Get the current time. - * Only applicable when option `showCurrentTime` is true. - * @return {Date} Returns the current time. + * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. + * @param {object} options */ - Core.prototype.getCurrentTime = function () { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); + 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"); + + 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; + } + } + } + } + + if (this.yAxisLeft) { + if (options.dataAxis !== undefined) { + this.yAxisLeft.setOptions(this.options.dataAxis); + this.yAxisRight.setOptions(this.options.dataAxis); + } + } + + if (this.legendLeft) { + if (options.legend !== undefined) { + this.legendLeft.setOptions(this.options.legend); + this.legendRight.setOptions(this.options.legend); + } + } + + if (this.groups.hasOwnProperty(UNGROUPED)) { + this.groups[UNGROUPED].setOptions(options); + } } - return this.currentTime.getCurrentTime(); + // this is used to redraw the graph if the visibility of the groups is changed. + if (this.dom.frame) { + this.redraw(true); + } }; /** - * 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 the component from the DOM */ - // TODO: move this function to Range - Core.prototype._toTime = function (x) { - return DateUtil.toTime(this, x, this.props.center.width); + LineGraph.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } }; + /** - * 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 + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed */ - // 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); + LineGraph.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } }; + /** - * 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 + * Set items + * @param {vis.DataSet | null} items */ - // TODO: move this function to Range - Core.prototype._toScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.center.width); - }; + 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 (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - /** - * 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 - */ - // 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; - }; + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - /** - * Initialize watching when option autoResize is true - * @private - */ - Core.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); - } else { - this._stopAutoResize(); + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); } + this._updateUngrouped(); + //this._updateGraph(); + this.redraw(true); }; + /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. - * @private + * Set groups + * @param {vis.DataSet} groups */ - Core.prototype._startAutoResize = function () { + LineGraph.prototype.setGroups = function (groups) { var me = this; + var ids; - this._stopAutoResize(); - - this._onResize = function () { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); - return; - } + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - 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; + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - me.emit("change"); - } - } - }; + // 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"); + } - // add event listener to window resize - util.addEventListener(window, "resize", this._onResize); + 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.watchTimer = setInterval(this._onResize, 1000); + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } + this._onUpdate(); }; + /** - * Stop watching for a resize of the frame. + * Update the data + * @param [ids] * @private */ - Core.prototype._stopAutoResize = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; + 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]); } - // remove event listener on window.resize - util.removeEventListener(window, "resize", this._onResize); - this._onResize = null; + //this._updateGraph(); + this.redraw(true); }; - - /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop - * @private - */ - Core.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; + LineGraph.prototype._onAddGroups = function (groupIds) { + this._onUpdateGroups(groupIds); }; + /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop + * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph + * @param {Array} groupIds * @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; + 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.props.scrollTopMin = scrollTopMin; } - - // 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; + this._updateUngrouped(); + //this._updateGraph(); + this.redraw(true); }; + /** - * Get the current scrollTop - * @returns {number} scrollTop + * update a group object with the group dataset entree + * + * @param group + * @param groupId * @private */ - Core.prototype._getScrollTop = function () { - return this.props.scrollTop; + 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(); }; - module.exports = Core; - -/***/ }, -/* 47 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var Hammer = __webpack_require__(45); /** - * Register a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * this updates all groups, it is used when there is an update the the itemset. + * + * @private */ - exports.onTouch = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFirst) { - callback(event); + LineGraph.prototype._updateAllGroupData = function () { + if (this.itemsData != null) { + var groupsContent = {}; + var groupId; + for (groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + groupsContent[groupId] = []; + } } - }; - - hammer.on("hammer.input", callback.inputHandler); - }; - - /** - * Register a release event, taking place after a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) - */ - exports.onRelease = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFinal) { - callback(event); + 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); + } } - }; - - return hammer.on("hammer.input", callback.inputHandler); + for (groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + this.groups[groupId].setItems(groupsContent[groupId]); + } + } + } }; - /** - * 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); - }; /** - * Unregister a release event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * 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 */ - exports.offRelease = exports.offTouch; - -/***/ }, -/* 48 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; + 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; + } + } + } - // English - exports.en = { - current: "current", - time: "time" - }; - exports.en_EN = exports.en; - exports.en_US = exports.en; + 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); + } - // Dutch - exports.nl = { - custom: "aangepaste", - time: "tijd" + this.legendLeft.redraw(); + this.legendRight.redraw(); }; - exports.nl_NL = exports.nl; - exports.nl_BE = exports.nl; - -/***/ }, -/* 49 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; /** - * Created by Alex on 11/11/2014. + * Redraw the component, mandatory function + * @return {boolean} Returns true if the component is resized */ - var DOMutil = __webpack_require__(2); - var Points = __webpack_require__(51); + LineGraph.prototype.redraw = function (forceGraphUpdate) { + var resized = false; - function Line(groupId, options) { - this.groupId = groupId; - this.options = options; - } + // calculate actual size and position + this.props.width = this.dom.frame.offsetWidth; + this.props.height = this.body.domProps.centerContainer.height; - 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; + // update the graph if there is no lastWidth or with, used for the initial draw + if (this.lastWidth === undefined && this.props.width) { + forceGraphUpdate = true; } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; - }; + // check if this component is resized + resized = this._isResized() || resized; - /** - * 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); - } + // 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; - // construct path from dataset - if (group.options.catmullRom.enabled == true) { - d = Line._catmullRom(dataset, group); - } else { - d = Line._linear(dataset); - } - // 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); + // 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); - // draw points - if (group.options.drawPoints.enabled == true) { - Points.draw(dataset, group, framework); - } + // if the height of the graph is set as proportional, change the height of the svg + if ((this.options.height + "").indexOf("%") != -1 || this.updateSVGheightOnResize == true) { + this.updateSVGheight = true; } } - }; - - - - /** - * This 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; - - // Catmull-Rom to Cubic Bezier conversion matrix - // 0 1 0 0 - // -1/6 1 1/6 0 - // 0 1/6 1 -1/6 - // 0 0 1 0 - - // bp0 = { x: p1.x, y: p1.y }; - bp1 = { x: (-p0.x + 6 * p1.x + p2.x) * normalization, y: (-p0.y + 6 * p1.y + p2.y) * normalization }; - bp2 = { x: (p1.x + 6 * p2.x - p3.x) * normalization, y: (p1.y + 6 * p2.y - p3.y) * normalization }; - // bp0 = { x: p2.x, y: p2.y }; + // 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"; + } - d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; + // 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"; + } + } } - return d; + this.legendLeft.redraw(); + this.legendRight.redraw(); + return resized; }; + /** - * 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. + * Update and redraw the graph. * - * 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; - - d1 = Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2)); - d2 = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); - d3 = Math.sqrt(Math.pow(p2.x - p3.x, 2) + Math.pow(p2.y - p3.y, 2)); - - // Catmull-Rom to Cubic Bezier conversion matrix - - // A = 2d1^2a + 3d1^a * d2^a + d3^2a - // B = 2d3^2a + 3d3^a * d2^a + d2^2a + 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; - // [ 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 ] + // 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); - 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); + // apply sampling, if disabled, it will pass through this function. + this._applySampling(groupIds, groupsData); - 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; + // we transform the X coordinates to detect collisions + for (i = 0; i < groupIds.length; i++) { + preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); } - bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, - y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + // 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; - bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, - y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; + // 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 (bp1.x == 0 && bp1.y == 0) { - bp1 = p1; - } - if (bp2.x == 0 && bp2.y == 0) { - bp2 = p2; + // 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); } - d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; } - - return d; } + + // cleanup unused svg elements + DOMutil.cleanupElements(this.svgElements); + return false; }; + /** - * this generates the SVG path for a linear drawing between datapoints. - * @param data - * @returns {string} + * 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 */ - 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; + 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); + } + } + } + } } } - return d; }; - module.exports = Line; - -/***/ }, -/* 50 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; /** - * Created by Alex on 11/11/2014. + * + * @param groupIds + * @param groupsData + * @private */ - var DOMutil = __webpack_require__(2); - var Points = __webpack_require__(51); + 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; - function Bargraph(groupId, options) { - this.groupId = groupId; - this.options = options; - } + // 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))); - 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 - }); + var sampledData = []; + for (var j = 0; j < amountOfPoints; j += increment) { + sampledData.push(dataContainer[j]); + } + groupsData[groupIds[i]] = sampledData; + } + } } - return barCombinedData; } }; - /** - * draw a bar graph * - * @param groupIds - * @param processedGroupData + * + * @param {array} groupIds + * @param {object} groupsData + * @param {object} groupRanges | this is being filled here + * @private */ - 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; + 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]); } } } - } - if (barPoints == 0) { - return; + // 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"); } + }; - // 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; - } - }); - - // get intersections - Bargraph._getDataIntersections(intersections, combinedData); - - // plot barchart - for (i = 0; i < combinedData.length; i++) { - group = framework.groups[combinedData[i].groupId]; - var minWidth = 0.1 * group.options.barChart.width; - key = combinedData[i].x; - var heightOffset = 0; - if (intersections[key] === undefined) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - key); - } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); - } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - } else { - var nextKey = i + (intersections[key].amount - intersections[key].resolved); - var prevKey = i - (intersections[key].resolved + 1); - if (nextKey < combinedData.length) { - coreDistance = Math.abs(combinedData[nextKey].x - key); - } - if (prevKey > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + /** + * 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; } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - intersections[key].resolved += 1; + } - if (group.options.barChart.handleOverlap == "stack") { - heightOffset = intersections[key].accumulated; - intersections[key].accumulated += group.zeroPosition - combinedData[i].y; - } else if (group.options.barChart.handleOverlap == "sideBySide") { - drawData.width = drawData.width / intersections[key].amount; - drawData.offset += intersections[key].resolved * drawData.width - 0.5 * drawData.width * (intersections[key].amount + 1); - if (group.options.barChart.align == "left") { - drawData.offset -= 0.5 * drawData.width; - } else if (group.options.barChart.align == "right") { - drawData.offset += 0.5 * drawData.width; + // 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; + } } } } - 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 (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; + + 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; + } + + // 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); + } + + return resized; }; /** - * Fill the intersections object with counters of how many datapoints share the same x coordinates - * @param intersections - * @param combinedData + * 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 */ - 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)); + LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { + var changed = false; + if (axisUsed == false) { + if (axis.dom.frame.parentNode && axis.hidden == false) { + axis.hide(); + changed = true; } - 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; + } else { + if (!axis.dom.frame.parentNode && axis.hidden == true) { + axis.show(); + changed = true; } } + return changed; }; /** - * Get the width and offset for bargraphs based on the coredistance between datapoints + * 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 coreDistance + * @param datapoints + * @returns {Array} + * @private + */ + LineGraph.prototype._convertXcoordinates = function (datapoints) { + var extractedData = []; + var xValue, yValue; + var toScreen = this.body.util.toScreen; + + for (var i = 0; i < datapoints.length; i++) { + xValue = toScreen(datapoints[i].x) + this.props.width; + yValue = datapoints[i].y; + extractedData.push({ x: xValue, y: yValue }); + } + + return extractedData; + }; + + + /** + * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the + * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for + * the yAxis. + * + * @param datapoints * @param group - * @param minWidth - * @returns {{width: Number, offset: Number}} + * @returns {Array} * @private */ - Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { - var width, offset; - if (coreDistance < group.options.barChart.width && coreDistance > 0) { - width = coreDistance < minWidth ? minWidth : coreDistance; + 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; + } - 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; - } + 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 }); } - return { width: width, offset: offset }; - }; + group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); + + return extractedData; + }; + + + module.exports = LineGraph; + +/***/ }, +/* 46 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(6); + var Component = __webpack_require__(25); + var DataStep = __webpack_require__(47); + + /** + * 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: {} + }; + + this.dom = {}; - 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 = {}; + this.range = { start: 0, end: 0 }; - Bargraph._getDataIntersections(intersections, barCombinedData); - groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData); - groupRanges[groupLabel].yAxisOrientation = orientation; - groupIds.push(groupLabel); - } - }; + this.options = util.extend({}, this.defaultOptions); + this.conversionFactor = 1; - 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; - } - } + this.setOptions(options); + this.width = Number(("" + this.options.width).replace("px", "")); + this.minWidth = this.width; + this.height = this.linegraphSVG.offsetHeight; + this.hidden = false; - return { min: yMin, max: yMax }; - }; + this.stepPixels = 25; + this.stepPixelsForced = 25; + this.zeroCrossing = -1; - module.exports = Bargraph; + this.lineOffset = 0; + this.master = true; + this.svgElements = {}; + this.iconsRemoved = false; -/***/ }, -/* 51 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + this.groups = {}; + this.amountOfGroups = 0; - /** - * Created by Alex on 11/11/2014. - */ - var DOMutil = __webpack_require__(2); + // create the HTML DOM + this._create(); - function Points(groupId, options) { - this.groupId = groupId; - this.options = options; + var me = this; + this.body.emitter.on("verticalDrag", function () { + me.dom.lineContainer.style.top = me.body.domProps.scrollTop + "px"; + }); } + DataAxis.prototype = new Component(); - 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; + + DataAxis.prototype.addGroup = function (label, graphOptions) { + if (!this.groups.hasOwnProperty(label)) { + this.groups[label] = graphOptions; } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + this.amountOfGroups += 1; }; - Points.prototype.draw = function (dataset, group, framework, offset) { - Points.draw(dataset, group, framework, offset); + DataAxis.prototype.updateGroup = function (label, graphOptions) { + this.groups[label] = graphOptions; }; - /** - * draw the data points - * - * @param {Array} dataset - * @param {Object} JSONcontainer - * @param {Object} svg | SVG DOM element - * @param {GraphGroup} group - * @param {Number} [offset] - */ - Points.draw = function (dataset, group, framework, offset) { - 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); + DataAxis.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; } }; - module.exports = Points; + 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); -/***/ }, -/* 52 */ -/***/ function(module, exports, __webpack_require__) { + this.minWidth = Number(("" + this.options.width).replace("px", "")); - "use strict"; + if (redraw == true && this.dom.frame) { + this.hide(); + this.show(); + } + } + }; - var SelectionMixin = __webpack_require__(74); - var ManipulationMixin = __webpack_require__(75); - var NavigationMixin = __webpack_require__(76); - var HierarchicalLayoutMixin = __webpack_require__(77); /** - * Load a mixin into the network object - * - * @param {Object} sourceVariable | this object has to contain functions. - * @private + * Create the HTML DOM for the DataAxis */ - exports._loadMixin = function (sourceVariable) { - for (var mixinFunction in sourceVariable) { - if (sourceVariable.hasOwnProperty(mixinFunction)) { - this[mixinFunction] = sourceVariable[mixinFunction]; + 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 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; + } + + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); + y += iconHeight + iconOffset; + } } } + + DOMutil.cleanupElements(this.svgElements); + this.iconsRemoved = false; }; + DataAxis.prototype._cleanupIcons = function () { + if (this.iconsRemoved == false) { + DOMutil.prepareElements(this.svgElements); + DOMutil.cleanupElements(this.svgElements); + this.iconsRemoved = true; + } + }; /** - * removes a mixin from the network object. - * - * @param {Object} sourceVariable | this object has to contain functions. - * @private + * Create the HTML DOM for the DataAxis */ - exports._clearMixin = function (sourceVariable) { - for (var mixinFunction in sourceVariable) { - if (sourceVariable.hasOwnProperty(mixinFunction)) { - this[mixinFunction] = undefined; + DataAxis.prototype.show = function () { + this.hidden = false; + if (!this.dom.frame.parentNode) { + if (this.options.orientation == "left") { + this.body.dom.left.appendChild(this.dom.frame); + } else { + this.body.dom.right.appendChild(this.dom.frame); } } - }; + if (!this.dom.lineContainer.parentNode) { + this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); + } + }; /** - * Mixin the physics system and initialize the parameters required. - * - * @private + * Create the HTML DOM for the DataAxis */ - exports._loadPhysicsSystem = function () { - this._loadMixin(PhysicsMixin); - this._loadSelectedForceSolver(); - if (this.constants.configurePhysics == true) { - this._loadPhysicsConfiguration(); - } else { - this._cleanupPhysicsConfiguration(); + DataAxis.prototype.hide = function () { + this.hidden = true; + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); } - }; - - + if (this.dom.lineContainer.parentNode) { + this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); + } + }; /** - * Mixin the selection system and initialize the parameters required - * - * @private + * Set a range (start and end) + * @param end + * @param start + * @param end */ - exports._loadSelectionSystem = function () { - this.selectionObj = { nodes: {}, edges: {} }; - - this._loadMixin(SelectionMixin); + 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; }; - /** - * Mixin the navigationUI (User Interface) system and initialize the parameters required - * - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - exports._loadManipulationSystem = function () { - // reset global variables -- these are used by the selection of nodes and edges. - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false; + DataAxis.prototype.redraw = function () { + var resized = false; + var activeGroups = 0; - if (this.constants.dataManipulation.enabled == true) { - // load the manipulator HTML elements. All styling done in css. - if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement("div"); - this.manipulationDiv.className = "network-manipulationDiv"; - if (this.editMode == true) { - this.manipulationDiv.style.display = "block"; - } else { - this.manipulationDiv.style.display = "none"; - } - this.frame.appendChild(this.manipulationDiv); - } + // Make sure the line container adheres to the vertical scrolling. + this.dom.lineContainer.style.top = this.body.domProps.scrollTop + "px"; - 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"; + 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.frame.appendChild(this.editModeDiv); } + } + if (this.amountOfGroups == 0 || activeGroups == 0) { + this.hide(); + } else { + this.show(); + this.height = Number(this.linegraphSVG.style.height.replace("px", "")); - if (this.closeDiv === undefined) { - this.closeDiv = document.createElement("div"); - this.closeDiv.className = "network-manipulation-closeDiv"; - this.closeDiv.style.display = this.manipulationDiv.style.display; - this.frame.appendChild(this.closeDiv); - } + // 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; - // load the manipulation functions - this._loadMixin(ManipulationMixin); + var props = this.props; + var frame = this.dom.frame; - // create the manipulator toolbar - this._createManipulatorBar(); - } else { - if (this.manipulationDiv !== undefined) { - // removes all the bindings and overloads - this._createManipulatorBar(); + // update classname + frame.className = "dataaxis"; - // remove the manipulation divs - this.frame.removeChild(this.manipulationDiv); - this.frame.removeChild(this.editModeDiv); - this.frame.removeChild(this.closeDiv); + // calculate character width and height + this._calculateCharSize(); - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; - // remove the mixin functions - this._clearMixin(ManipulationMixin); + var orientation = this.options.orientation; + var showMinorLabels = this.options.showMinorLabels; + var showMajorLabels = this.options.showMajorLabels; + + // determine the width and height of the elements for the axis + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; + + props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset; + props.minorLineHeight = 1; + props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset; + props.majorLineHeight = 1; + + // take frame offline while updating (is almost twice as fast) + if (orientation == "left") { + frame.style.top = "0"; + frame.style.left = "0"; + frame.style.bottom = ""; + frame.style.width = this.width + "px"; + frame.style.height = this.height + "px"; + this.props.width = this.body.domProps.left.width; + this.props.height = this.body.domProps.left.height; + } else { + // right + frame.style.top = ""; + frame.style.bottom = "0"; + frame.style.left = "0"; + frame.style.width = this.width + "px"; + frame.style.height = this.height + "px"; + this.props.width = this.body.domProps.right.width; + this.props.height = this.body.domProps.right.height; } - } - }; + resized = this._redrawLabels(); + resized = this._isResized() || resized; - /** - * Mixin the navigation (User Interface) system and initialize the parameters required - * - * @private - */ - exports._loadNavigationControls = function () { - this._loadMixin(NavigationMixin); - // the clean function removes the button divs, this is done to remove the bindings. - this._cleanNavigation(); - if (this.constants.navigation.enabled == true) { - this._loadNavigationElements(); + if (this.options.icons == true) { + this._redrawGroupIcons(); + } else { + this._cleanupIcons(); + } + + this._redrawTitle(orientation); } + return resized; }; - /** - * Mixin the hierarchical layout system. - * + * Repaint major and minor text labels and vertical grid lines * @private */ - exports._loadHierarchySystem = function () { - this._loadMixin(HierarchicalLayoutMixin); - }; + DataAxis.prototype._redrawLabels = function () { + var resized = false; + DOMutil.prepareElements(this.DOMelements.lines); + DOMutil.prepareElements(this.DOMelements.labels); -/***/ }, -/* 53 */ -/***/ function(module, exports, __webpack_require__) { + var orientation = this.options.orientation; - "use strict"; + // calculate range and step (step such that we have space for 7 characters per label) + var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced; - var keycharm = __webpack_require__(63); - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(45); - var util = __webpack_require__(1); + 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 + ); - /** - * Turn an element into an clickToUse element. - * When not active, the element has a transparent overlay. When the overlay is - * clicked, the mode is changed to active. - * When active, the element is displayed with a blue border around it, and - * the interactive contents of the element can be used. When clicked outside - * the element, the elements mode is changed to inactive. - * @param {Element} container - * @constructor - */ - function Activator(container) { - this.active = false; + this.step = step; + // get the distance in pixels for a step + // dead space is space that is "left over" after a step + var stepPixels = (this.dom.frame.offsetHeight - step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange)) / ((step.marginRange - step.deadSpace) / step.step); - this.dom = { - container: container - }; + this.stepPixels = stepPixels; - this.dom.overlay = document.createElement("div"); - this.dom.overlay.className = "overlay"; + var amountOfSteps = this.height / stepPixels; + var stepDifference = 0; - this.dom.container.appendChild(this.dom.overlay); + // 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; - this.hammer = Hammer(this.dom.overlay, { prevent_default: false }); - this.hammer.on("tap", this._onTapOverlay.bind(this)); + 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 (zeroStepDifference < 0) { + for (var i = 0; i < -zeroStepDifference; i++) { + step.previous(); + } + } + } + } else { + amountOfSteps += 0.25; + } - // block all touch events (except tap) - var me = this; - var events = ["touch", "pinch", "doubletap", "hold", "dragstart", "drag", "dragend", "mousewheel", "DOMMouseScroll" // DOMMouseScroll is needed for Firefox - ]; - events.forEach(function (event) { - me.hammer.on(event, function (event) { - event.stopPropagation(); - }); - }); - // attach a tap event to the window, in order to deactivate when clicking outside the timeline - this.windowHammer = Hammer(window, { prevent_default: false }); - this.windowHammer.on("tap", function (event) { - // deactivate when clicked outside the container - if (!_hasParent(event.target, container)) { - me.deactivate(); - } - }); + this.valueAtZero = step.marginEnd; + var marginStartPos = 0; - if (this.keycharm !== undefined) { - this.keycharm.destroy(); + // 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.keycharm = keycharm(); - // keycharm listener only bounded when active) - this.escListener = this.deactivate.bind(this); - } + 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++; + } - // turn into an event emitter - Emitter(Activator.prototype); + if (this.master == false) { + this.conversionFactor = y / (this.valueAtZero - step.current); + } else { + this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; + } - // The currently active activator - Activator.current = null; + // 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; - /** - * Destroy the activator. Cleans up all created DOM and event listeners - */ - Activator.prototype.destroy = function () { - this.deactivate(); + // 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; + } - // remove dom - this.dom.overlay.parentNode.removeChild(this.dom.overlay); + return resized; + }; - // cleanup hammer instances - this.hammer = null; - this.windowHammer = null; - // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + DataAxis.prototype.convertValue = function (value) { + var invertedValue = this.valueAtZero - value; + var convertedValue = invertedValue * this.conversionFactor; + return convertedValue; }; /** - * Activate the element - * Overlay is hidden, element is decorated with a blue shadow border + * Create a label for the axis at position x + * @private + * @param y + * @param text + * @param orientation + * @param className + * @param characterHeight */ - Activator.prototype.activate = function () { - // we allow only one active activator at a time - if (Activator.current) { - Activator.current.deactivate(); + 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"; } - Activator.current = this; - this.active = true; - this.dom.overlay.style.display = "none"; - util.addClassName(this.dom.container, "vis-active"); + label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + "px"; - this.emit("change"); - this.emit("activate"); + text += ""; - // 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); + var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); + if (this.maxLabelSize < text.length * largestWidth) { + this.maxLabelSize = text.length * largestWidth; + } }; /** - * Deactivate the element - * Overlay is displayed on top of the element + * Create a minor line for the axis at position y + * @param y + * @param orientation + * @param className + * @param offset + * @param width */ - 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); + 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 = ""; - this.emit("change"); - this.emit("deactivate"); - }; + if (orientation == "left") { + line.style.left = this.width - offset + "px"; + } else { + line.style.right = this.width - offset + "px"; + } - /** - * Handle a tap event: activate the container - * @param event - * @private - */ - Activator.prototype._onTapOverlay = function (event) { - // activate the container - this.activate(); - event.stopPropagation(); + line.style.width = width + "px"; + line.style.top = y + "px"; + } }; /** - * 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. + * Create a title for the axis * @private + * @param orientation */ - function _hasParent(element, parent) { - while (element) { - if (element === parent) { - return true; - } - element = element.parentNode; - } - return false; - } + DataAxis.prototype._redrawTitle = function (orientation) { + DOMutil.prepareElements(this.DOMelements.title); - module.exports = Activator; + // 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; -/***/ }, -/* 54 */ -/***/ function(module, exports, __webpack_require__) { + // Add style - if provided + if (this.options.title[orientation].style !== undefined) { + util.addCssText(title, this.options.title[orientation].style); + } - "use strict"; + if (orientation == "left") { + title.style.left = this.props.titleCharHeight + "px"; + } else { + title.style.right = this.props.titleCharHeight + "px"; + } - // 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; + title.style.width = this.height + "px"; + } - // 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." + // we need to clean up in case we did not use all elements. + DOMutil.cleanupElements(this.DOMelements.title); }; - exports.nl_NL = exports.nl; - exports.nl_BE = exports.nl; -/***/ }, -/* 55 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + /** - * Canvas shapes used by Network + * 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 */ - if (typeof CanvasRenderingContext2D !== "undefined") { - /** - * Draw a circle shape - */ - CanvasRenderingContext2D.prototype.circle = function (x, y, r) { - this.beginPath(); - this.arc(x, y, r, 0, 2 * Math.PI, false); - }; - - /** - * Draw a square shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r size, width and height of the square - */ - CanvasRenderingContext2D.prototype.square = function (x, y, r) { - this.beginPath(); - this.rect(x - r, y - r, r * 2, r * 2); - }; - - /** - * Draw a triangle shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.triangle = function (x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - - this.moveTo(x, y - (h - ir)); - this.lineTo(x + s2, y + ir); - this.lineTo(x - s2, y + ir); - this.lineTo(x, y - (h - ir)); - this.closePath(); - }; - - /** - * Draw a triangle shape in downward orientation - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius - */ - CanvasRenderingContext2D.prototype.triangleDown = function (x, y, r) { - // http://en.wikipedia.org/wiki/Equilateral_triangle - this.beginPath(); - - var s = r * 2; - var s2 = s / 2; - var ir = Math.sqrt(3) / 6 * s; // radius of inner circle - var h = Math.sqrt(s * s - s2 * s2); // height - - this.moveTo(x, y + (h - ir)); - this.lineTo(x + s2, y - ir); - this.lineTo(x - s2, y - ir); - this.lineTo(x, y + (h - ir)); - this.closePath(); - }; - - /** - * Draw a star shape, a star with 5 points - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.star = function (x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); - - for (var n = 0; n < 10; n++) { - var radius = n % 2 === 0 ? r * 1.3 : r * 0.5; - this.lineTo(x + radius * Math.sin(n * 2 * Math.PI / 10), y - radius * Math.cos(n * 2 * Math.PI / 10)); - } - - this.closePath(); - }; - - /** - * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas - */ - CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { - var r2d = Math.PI / 180; - if (w - 2 * r < 0) { - r = w / 2; - } //ensure that the radius isn't too large for x - if (h - 2 * r < 0) { - r = h / 2; - } //ensure that the radius isn't too large for y - this.beginPath(); - this.moveTo(x + r, y); - this.lineTo(x + w - r, y); - this.arc(x + w - r, y + r, r, r2d * 270, r2d * 360, false); - this.lineTo(x + w, y + h - r); - this.arc(x + w - r, y + h - r, r, 0, r2d * 90, false); - this.lineTo(x + r, y + h); - this.arc(x + r, y + h - r, r, r2d * 90, r2d * 180, false); - this.lineTo(x, y + r); - this.arc(x + r, y + r, r, r2d * 180, r2d * 270, false); - }; - - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.ellipse = function (x, y, w, h) { - var kappa = 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 - - 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); - }; - + 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; - /** - * 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.dom.frame.removeChild(measureCharMinor); + } - 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 + 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); - this.beginPath(); - this.moveTo(xe, ym); + this.props.majorCharHeight = measureCharMajor.clientHeight; + this.props.majorCharWidth = measureCharMajor.clientWidth; - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + this.dom.frame.removeChild(measureCharMajor); + } - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + 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.lineTo(xe, ymb); + this.props.titleCharHeight = measureCharTitle.clientHeight; + this.props.titleCharWidth = measureCharTitle.clientWidth; - this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); - this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); + this.dom.frame.removeChild(measureCharTitle); + } + }; - this.lineTo(x, ym); - }; + module.exports = DataAxis; +/***/ }, +/* 47 */ +/***/ function(module, exports, __webpack_require__) { - /** - * 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); + "use strict"; - // 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); + /** + * @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; - // 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); + this.autoScale = true; + this.stepIndex = 0; + this.step = 1; + this.scale = 1; - // right - var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); - var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); + this.marginStart; + this.marginEnd; + this.deadSpace = 0; - this.beginPath(); - this.moveTo(x, y); - this.lineTo(xl, yl); - this.lineTo(xi, yi); - this.lineTo(xr, yr); - this.closePath(); - }; + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; - /** - * 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.alignZeros = alignZeros; - // TODO: add diamond shape + this.setRange(start, end, minimumStep, containerHeight, customRange); } -/***/ }, -/* 56 */ -/***/ 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); }; + /** + * 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; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + if (this._start == this._end) { + this._start -= 0.75; + this._end += 1; + } + + if (this.autoScale == true) { + this.setMinimumStep(minimumStep, containerHeight); + } + + this.setFirst(customRange); + }; /** - * Created by Alex on 2/23/2015. + * 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 BarnesHutSolver = __webpack_require__(67).BarnesHutSolver; - var Repulsion = __webpack_require__(68).Repulsion; - var HierarchicalRepulsion = __webpack_require__(69).HierarchicalRepulsion; - var SpringSolver = __webpack_require__(70).SpringSolver; - var HierarchicalSpringSolver = __webpack_require__(71).HierarchicalSpringSolver; - var CentralGravitySolver = __webpack_require__(72).CentralGravitySolver; + var minorStepIdx = -1; + var magnitudefactor = Math.pow(10, orderOfMagnitude); + var start = 0; + if (orderOfMagnitude < 0) { + start = orderOfMagnitude; + } - var util = __webpack_require__(1); + var solutionFound = false; + for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) { + magnitudefactor = Math.pow(10, i); + for (var j = 0; j < this.minorSteps.length; j++) { + var stepSize = magnitudefactor * this.minorSteps[j]; + if (stepSize >= minimumStepValue) { + solutionFound = true; + minorStepIdx = j; + break; + } + } + if (solutionFound == true) { + break; + } + } + this.stepIndex = minorStepIdx; + this.scale = magnitudefactor; + this.step = magnitudefactor * this.minorSteps[minorStepIdx]; + }; - var PhysicsEngine = (function () { - function PhysicsEngine(body, options) { - var _this = this; - _classCallCheck(this, PhysicsEngine); - this.body = body; - this.physicsBody = { calculationNodes: {}, calculationNodeIndices: [], forces: {}, velocities: {} }; - this.scale = 1; - this.viewFunction = undefined; + /** + * 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 = {}; + } - this.body.emitter.on("_setScale", function (scale) { - return _this.scale = scale; - }); + var niceStart = customRange.min === undefined ? this._start - this.scale * 2 * this.minorSteps[this.stepIndex] : customRange.min; + var niceEnd = customRange.max === undefined ? this._end + this.scale * this.minorSteps[this.stepIndex] : customRange.max; - this.simulationInterval = 1000 / 60; - this.requiresTimeout = true; - this.previousStates = {}; - this.renderTimer == undefined; + this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; + this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; - this.stabilized = false; - this.stabilizationIterations = 0; + // 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; + } - // default options - this.options = { - 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: 150, - damping: 0.09 - }, - model: "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 - } - }; + this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; + this.marginRange = this.marginEnd - this.marginStart; - this.setOptions(options); + + this.current = this.marginEnd; + }; + + 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; } + }; - _prototypeProperties(PhysicsEngine, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - if (typeof options.stabilization == "boolean") { - options.stabilization = { - enabled: options.stabilization - }; - } - util.deepExtend(this.options, options); - } - this.init(); - }, - writable: true, - configurable: true - }, - init: { - value: function init() { - var options; - if (this.options.model == "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.model == "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); - } - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - this.modelOptions = options; - }, - writable: true, - configurable: true - }, - startSimulation: { - value: function startSimulation() { - this.stabilized = false; - if (this.options.stabilization.enabled === true) { - this.stabilize(); - } else { - this.runSimulation(); - } - }, - writable: true, - configurable: true - }, - runSimulation: { - value: function runSimulation() { - if (this.viewFunction === undefined) { - this.viewFunction = this.simulationStep.bind(this); - this.body.emitter.on("_beforeRender", this.viewFunction); - this.body.emitter.emit("_startRendering"); - } - }, - 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; + /** + * 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; + }; - // run double speed if it is a little graph - if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { - this.physicsTick(); + /** + * Do the next step + */ + DataStep.prototype.next = function () { + var prev = this.current; + this.current -= this.step; - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - this.runDoubleSpeed = true; - } + // safety mechanism: if current time is still unchanged, move to the end + if (this.current == prev) { + this.current = this._end; + } + }; - 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.body.emitter.emit("_stopRendering"); - } - }, - writable: true, - configurable: true - }, - physicsTick: { + /** + * Do the next step + */ + DataStep.prototype.previous = function () { + this.current += this.step; + this.marginEnd += this.step; + this.marginRange = this.marginEnd - this.marginStart; + }; - /** - * A single simulation step (or "tick") in the physics simulation - * - * @private - */ - value: function physicsTick() { - if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); - // 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; - } - } - this.stabilizationIterations++; + /** + * 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; } - }, - writable: true, - configurable: true - }, - _updateCalculationNodes: { + } + } + } - /** - * 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 _updateCalculationNodes() { - this.physicsBody.calculationNodes = {}; - this.physicsBody.forces = {}; - this.physicsBody.calculationNodeIndices = []; + return toPrecision; + }; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - this.physicsBody.calculationNodes[nodeId] = this.body.nodes[nodeId]; - } + /** + * 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; + }; - // if support nodes are used, we have them here - var supportNodes = this.body.supportNodes; - for (var i = 0; i < this.body.supportNodeIndices.length; i++) { - var supportNodeId = this.body.supportNodeIndices[i]; - if (this.body.edges[supportNodes[supportNodeId].parentEdgeId] !== undefined) { - this.physicsBody.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; - } else { - console.error("Support node detected that does not have an edge!"); - } - } + module.exports = DataStep; - this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes); - for (var i = 0; i < this.physicsBody.calculationNodeIndices.length; i++) { - var nodeId = this.physicsBody.calculationNodeIndices[i]; - this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; +/***/ }, +/* 48 */ +/***/ function(module, exports, __webpack_require__) { - // 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 }; - } - } + "use strict"; - // clean deleted nodes from the velocity vector - for (var nodeId in this.physicsBody.velocities) { - if (this.physicsBody.calculationNodes[nodeId] === undefined) { - delete this.physicsBody.velocities[nodeId]; - } - } - }, - writable: true, - configurable: true - }, - revert: { - value: function revert() { - var nodeIds = Object.keys(this.previousStates); - var nodes = this.physicsBody.calculationNodes; - var velocities = this.physicsBody.velocities; + 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); - 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.calculationNodeIndices; - var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; - var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.scale, 0.05); + /** + * /** + * @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; + } - 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; - } + + /** + * 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 = []; + } + }; - if (nodesPresent == true) { - if (vminCorrected > 0.5 * this.options.maxVelocity) { - return false; - } else { - return stabilized; - } - } - return true; - }, - writable: true, - configurable: true - }, - _performStep: { - value: function _performStep(nodeId, maxVelocity) { - var node = this.physicsBody.calculationNodes[nodeId]; - var timestep = this.options.timestep; - var forces = this.physicsBody.forces; - var velocities = this.physicsBody.velocities; + /** + * 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; + }; - // 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.xFixed) { - 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; - } + /** + * 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); - if (!node.yFixed) { - 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; - } + util.mergeOptions(this.options, options, "catmullRom"); + util.mergeOptions(this.options, options, "drawPoints"); + util.mergeOptions(this.options, options, "shaded"); - 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: { + 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; + } + } + } + } + } + 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); + } + }; + /** + * 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; + 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); + } + 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"); + } - /** - * 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 != null && nodes[id].y != null) { - nodes[id].fixedData.x = nodes[id].xFixed; - nodes[id].fixedData.y = nodes[id].yFixed; - nodes[id].xFixed = true; - nodes[id].yFixed = true; - } - } - } - }, - writable: true, - configurable: true - }, - _restoreFrozenNodes: { + 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); - /** - * 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 (nodes[id].fixedData.x != null) { - nodes[id].xFixed = nodes[id].fixedData.x; - nodes[id].yFixed = nodes[id].fixedData.y; - } - } - } - }, - writable: true, - configurable: true - }, - stabilize: { + var offset = Math.round((iconWidth - 2 * barWidth) / 3); - /** - * Find a stable position for all nodes - * @private - */ - value: function stabilize() { - if (this.options.stabilization.onlyDynamicEdges == true) { - this._freezeNodes(); - } - this.stabilizationSteps = 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); + } + }; - 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++; - } - 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 }); - } + /** + * 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 }; + }; - if (this.options.stabilization.onlyDynamicEdges == true) { - this._restoreFrozenNodes(); - } + GraphGroup.prototype.getYRange = function (groupData) { + return this.type.getYRange(groupData); + }; - this.body.emitter.emit("stabilizationIterationsDone"); - this.body.emitter.emit("_requestRedraw"); - }, - writable: true, - configurable: true - } - }); + GraphGroup.prototype.draw = function (dataset, group, framework) { + this.type.draw(dataset, group, framework); + }; - return PhysicsEngine; - })(); - exports.PhysicsEngine = PhysicsEngine; - Object.defineProperty(exports, "__esModule", { - value: true - }); + module.exports = GraphGroup; /***/ }, -/* 57 */ +/* 49 */ /***/ 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 24-Feb-15. + * Created by Alex on 11/11/2014. */ + var DOMutil = __webpack_require__(6); + var Points = __webpack_require__(50); - var util = __webpack_require__(1); - - var ClusterEngine = (function () { - function ClusterEngine(body) { - _classCallCheck(this, ClusterEngine); + function Line(groupId, options) { + this.groupId = groupId; + this.options = options; + } - this.body = body; - this.clusteredNodes = {}; + 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 }; + }; - _prototypeProperties(ClusterEngine, null, { - clusterByConnectionCount: { + /** + * 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); + } - /** - * - * @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(); - } + // construct path from dataset + if (group.options.catmullRom.enabled == true) { + d = Line._catmullRom(dataset, group); + } else { + d = Line._linear(dataset); + } - 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); - } + // 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; } - - for (var i = 0; i < nodesToCluster.length; i++) { - var node = this.body.nodes[nodesToCluster[i]]; - this.clusterByConnection(node, options, {}, {}, true); + fillPath.setAttributeNS(null, "class", group.className + " fill"); + if (group.options.shaded.style !== undefined) { + fillPath.setAttributeNS(null, "style", group.options.shaded.style); } - this.body.emitter.emit("_dataChanged"); - }, - writable: true, - configurable: true - }, - clusterByNodeData: { + 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); + } + } + } + }; - /** - * loop over all nodes, check if they adhere to the condition and cluster if needed. - * @param options - * @param doNotUpdateCalculationNodes - */ - value: function clusterByNodeData() { - var options = arguments[0] === undefined ? {} : arguments[0]; - var doNotUpdateCalculationNodes = arguments[1] === undefined ? false : arguments[1]; - if (options.joinCondition === undefined) { - throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); - } - // check if the options object is fine, append if needed - options = this._checkOptions(options); - var childNodesObj = {}; - var childEdgesObj = {}; + /** + * 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; - // 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._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); - }, - writable: true, - configurable: true - }, - clusterOutliers: { + // Catmull-Rom to Cubic Bezier conversion matrix + // 0 1 0 0 + // -1/6 1 1/6 0 + // 0 1/6 1 -1/6 + // 0 0 1 0 + // bp0 = { x: p1.x, y: p1.y }; + bp1 = { x: (-p0.x + 6 * p1.x + p2.x) * normalization, y: (-p0.y + 6 * p1.y + p2.y) * normalization }; + bp2 = { x: (p1.x + 6 * p2.x - p3.x) * normalization, y: (p1.y + 6 * p2.y - p3.y) * normalization }; + // bp0 = { x: p2.x, y: p2.y }; - /** - * Cluster all nodes in the network that have only 1 edge - * @param options - * @param doNotUpdateCalculationNodes - */ - value: function clusterOutliers(options, doNotUpdateCalculationNodes) { - options = this._checkOptions(options); - var clusters = []; + d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; + } - // 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 }); - } - } - } + return d; + }; - for (var i = 0; i < clusters.length; i++) { - this._cluster(clusters[i].nodes, clusters[i].edges, options, true); - } + /** + * 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; - if (doNotUpdateCalculationNodes !== true) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - clusterByConnection: { + 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)); - /** - * - * @param nodeId - * @param options - * @param doNotUpdateCalculationNodes - */ - value: function clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) { - // 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!"); - } + // Catmull-Rom to Cubic Bezier conversion matrix + + // A = 2d1^2a + 3d1^a * d2^a + d3^2a + // B = 2d3^2a + 3d3^a * d2^a + d2^2a + + // [ 0 1 0 0 ] + // [ -d2^2a /N A/N d1^2a /N 0 ] + // [ 0 d3^2a /M B/M -d2^2a /M ] + // [ 0 0 1 0 ] + + d3powA = Math.pow(d3, alpha); + d3pow2A = Math.pow(d3, 2 * alpha); + d2powA = Math.pow(d2, alpha); + d2pow2A = Math.pow(d2, 2 * alpha); + d1powA = Math.pow(d1, alpha); + d1pow2A = Math.pow(d1, 2 * alpha); + + A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A; + B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A; + N = 3 * d1powA * (d1powA + d2powA); + if (N > 0) { + N = 1 / N; + } + M = 3 * d3powA * (d3powA + d2powA); + if (M > 0) { + M = 1 / M; + } - var node = this.body.nodes[nodeId]; - options = this._checkOptions(options, node); - if (options.clusterNodeProperties.x === undefined) { - options.clusterNodeProperties.x = node.x;options.clusterNodeProperties.allowedToMoveX = !node.xFixed; - } - if (options.clusterNodeProperties.y === undefined) { - options.clusterNodeProperties.y = node.y;options.clusterNodeProperties.allowedToMoveY = !node.yFixed; - } + bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, + y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; - var childNodesObj = {}; - var childEdgesObj = {}; - var parentNodeId = node.id; - var parentClonedOptions = this._cloneOptions(parentNodeId); - childNodesObj[parentNodeId] = node; + bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, + y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; - // 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); + 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 + " "; + } - 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; - } - } + return d; + } + }; - this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); - }, - writable: true, - configurable: true - }, - _cloneOptions: { + /** + * 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; + } + } + return d; + }; + module.exports = Line; - /** - * This returns a clone of the options or properties 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: { +/***/ }, +/* 50 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; - /** - * 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; + /** + * Created by Alex on 11/11/2014. + */ + var DOMutil = __webpack_require__(6); - var childKeys = Object.keys(childNodesObj); - for (var i = 0; i < childKeys.length; i++) { - childNodeId = childKeys[i]; - childNode = childNodesObj[childNodeId]; + function Points(groupId, options) { + this.groupId = groupId; + this.options = options; + } - // 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; - } + 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 }; + }; - 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: { + Points.prototype.draw = function (dataset, group, framework, offset) { + Points.draw(dataset, group, framework, offset); + }; + /** + * draw the data points + * + * @param {Array} dataset + * @param {Object} JSONcontainer + * @param {Object} svg | SVG DOM element + * @param {GraphGroup} group + * @param {Number} [offset] + */ + Points.draw = function (dataset, group, framework, offset) { + 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); + } + }; - /** - * 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: { + module.exports = Points; - /** - * - * @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} doNotUpdateCalculationNodes | when true, do not wrap up - * @private - */ - value: function _cluster(childNodesObj, childEdgesObj, options) { - var doNotUpdateCalculationNodes = arguments[3] === undefined ? false : arguments[3]; - // kill condition: no children so cant cluster - if (Object.keys(childNodesObj).length == 0) { - return; - } +/***/ }, +/* 51 */ +/***/ function(module, exports, __webpack_require__) { - // check if we have an unique id; - if (options.clusterNodeProperties.id === undefined) { - options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); - } - var clusterId = options.clusterNodeProperties.id; + "use strict"; - // create the new edges that will connect to the cluster - var newEdges = []; - this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); + /** + * Created by Alex on 11/11/2014. + */ + var DOMutil = __webpack_require__(6); + var Points = __webpack_require__(50); - // 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); - } + function Bargraph(groupId, options) { + this.groupId = groupId; + this.options = options; + } - // get clusterproperties based on childNodes - var childEdgesOptions = []; - for (var edgeId in childEdgesObj) { - var clonedOptions = this._cloneOptions(edgeId, "edge"); - childEdgesOptions.push(clonedOptions); - } + 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; + } + }; - 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"; - } - // 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; + /** + * 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; } + } + } + } + if (barPoints == 0) { + return; + } - // force the ID to remain the same - clusterNodeProperties.id = clusterId; + // 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; + } + }); + // get intersections + Bargraph._getDataIntersections(intersections, combinedData); - // create the clusterNode - var clusterNode = this.body.functions.createNode(clusterNodeProperties); - clusterNode.isCluster = true; - clusterNode.containedNodes = childNodesObj; - clusterNode.containedEdges = childEdgesObj; + // plot barchart + for (i = 0; i < combinedData.length; i++) { + group = framework.groups[combinedData[i].groupId]; + var minWidth = 0.1 * group.options.barChart.width; + key = combinedData[i].x; + var heightOffset = 0; + if (intersections[key] === undefined) { + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - key); + } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); + } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + } else { + var nextKey = i + (intersections[key].amount - intersections[key].resolved); + var prevKey = i - (intersections[key].resolved + 1); + if (nextKey < combinedData.length) { + coreDistance = Math.abs(combinedData[nextKey].x - key); + } + if (prevKey > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + intersections[key].resolved += 1; - // delete contained edges from global - for (var edgeId in childEdgesObj) { - if (childEdgesObj.hasOwnProperty(edgeId)) { - if (this.body.edges[edgeId] !== undefined) { - if (this.body.edges[edgeId].via !== null) { - var viaId = this.body.edges[edgeId].via.id; - if (viaId) { - this.body.edges[edgeId].via = null; - delete this.body.supportNodes[viaId]; - } - } - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; - } - } + 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); + } + } + }; - // remove contained nodes from global - for (var nodeId in childNodesObj) { - if (childNodesObj.hasOwnProperty(nodeId)) { - this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; - delete this.body.nodes[nodeId]; - } - } + /** + * 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; + } + } + }; - // finally put the cluster node into global - this.body.nodes[clusterNodeProperties.id] = clusterNode; + /** + * 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; + 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; + } + } - // 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 { width: width, offset: offset }; + }; + 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 = {}; - // create bezier nodes for smooth curves if needed - this.body.emitter.emit("_newEdgesCreated"); + 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; + } + } + 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; + } + } - // set ID to undefined so no duplicates arise - clusterNodeProperties.id = undefined; + return { min: yMin, max: yMax }; + }; + module.exports = Bargraph; - // wrap up - if (doNotUpdateCalculationNodes !== true) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - isCluster: { +/***/ }, +/* 52 */ +/***/ 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; - } 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 Component = __webpack_require__(25); - /** - * 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 + /** + * 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 }, - openCluster: { - - - /** - * Open a cluster by calling this function. - * @param {String} clusterNodeId | the ID of the cluster node - * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true - */ - value: function openCluster(clusterNodeId, doNotUpdateCalculationNodes) { - // 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; - }; - - var node = this.body.nodes[clusterNodeId]; - var containedNodes = node.containedNodes; - var containedEdges = node.containedEdges; + right: { + visible: true, + position: "top-left" // top/bottom - left,center,right + } + }; + this.side = side; + this.options = util.extend({}, this.defaultOptions); + this.linegraphOptions = linegraphOptions; - // release nodes - for (var nodeId in containedNodes) { - if (containedNodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId] = containedNodes[nodeId]; - // inherit position - this.body.nodes[nodeId].x = node.x; - this.body.nodes[nodeId].y = node.y; + this.svgElements = {}; + this.dom = {}; + this.groups = {}; + this.amountOfGroups = 0; + this._create(); - // inherit speed - this.body.nodes[nodeId].vx = node.vx; - this.body.nodes[nodeId].vy = node.vy; + this.setOptions(options); + } - delete this.clusteredNodes[nodeId]; - } - } + Legend.prototype = new Component(); - // release edges - for (var edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - this.body.edges[edgeId] = containedEdges[edgeId]; - this.body.edges[edgeId].connect(); - var edge = this.body.edges[edgeId]; - if (edge.connected === false) { - if (this.clusteredNodes[edge.fromId] !== undefined) { - this._connectEdge(edge, edge.fromId, true); - } - if (this.clusteredNodes[edge.toId] !== undefined) { - this._connectEdge(edge, edge.toId, false); - } - } - } - } + Legend.prototype.clear = function () { + this.groups = {}; + this.amountOfGroups = 0; + }; - this.body.emitter.emit("_newEdgesCreated", containedEdges); + 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; + }; - var edgeIds = []; - for (var i = 0; i < node.edges.length; i++) { - edgeIds.push(node.edges[i].id); - } + Legend.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; + } + }; - // remove edges in clusterNode - for (var i = 0; i < edgeIds.length; i++) { - var edge = this.body.edges[edgeIds[i]]; - // if the edge should have been connected to a contained node - if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) { - // the node in the from array was contained in the cluster - if (this.body.nodes[edge.fromArray[0].id] !== undefined) { - this._connectEdge(edge, edge.fromArray[0].id, true); - } - } else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) { - // the node in the to array was contained in the cluster - if (this.body.nodes[edge.toArray[0].id] !== undefined) { - this._connectEdge(edge, edge.toArray[0].id, false); - } - } else { - var edgeId = edgeIds[i]; - var viaId = this.body.edges[edgeId].via.id; - if (viaId) { - this.body.edges[edgeId].via = null; - delete this.body.supportNodes[viaId]; - } - // this removes the edge from node.edges, which is why edgeIds is formed - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; - } - } + 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"; - // remove clusterNode - delete this.body.nodes[clusterNodeId]; + this.dom.textArea = document.createElement("div"); + this.dom.textArea.className = "legendText"; + this.dom.textArea.style.position = "relative"; + this.dom.textArea.style.top = "0px"; - if (doNotUpdateCalculationNodes !== true) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - _connectEdge: { + 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); + } + }; - /** - * 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: { + /** + * 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); + } + }; - /** - * 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; + Legend.prototype.setOptions = function (options) { + var fields = ["enabled", "orientation", "icons", "left", "right"]; + util.selectiveDeepExtend(fields, this.options, options); + }; - 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: { + 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 = ""; + } - /** - * 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: { + 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 = ""; + } - /** - * 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; + 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(); + } - 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; + 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 + "
"; } - average = average / hubCounter; - averageSquared = averageSquared / hubCounter; + } + } + this.dom.textArea.innerHTML = content; + this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + "px"; + } + }; - var variance = averageSquared - Math.pow(average, 2); - var standardDeviation = Math.sqrt(variance); + 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 hubThreshold = Math.floor(average + 2 * standardDeviation); + this.svg.style.width = iconWidth + 5 + iconOffset + "px"; - // always have at least one to cluster - if (hubThreshold > largestHub) { - hubThreshold = largestHub; + 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; } - - return hubThreshold; - }, - writable: true, - configurable: true + } } - }); - return ClusterEngine; - })(); + DOMutil.cleanupElements(this.svgElements); + } + }; - exports.ClusterEngine = ClusterEngine; - Object.defineProperty(exports, "__esModule", { - value: true - }); + module.exports = Legend; /***/ }, -/* 58 */ +/* 53 */ /***/ 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 26-Feb-15. - */ - - if (typeof window !== "undefined") { - window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; - } - - var CanvasRenderer = (function () { - function CanvasRenderer(body) { - var _this = this; - _classCallCheck(this, CanvasRenderer); - - this.body = body; + var Emitter = __webpack_require__(11); + var Hammer = __webpack_require__(19); + var keycharm = __webpack_require__(39); + var util = __webpack_require__(1); + var hammerUtil = __webpack_require__(24); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); + var dotparser = __webpack_require__(59); + var gephiParser = __webpack_require__(60); + var Groups = __webpack_require__(56); + var Images = __webpack_require__(57); + var Node = __webpack_require__(55); + var Edge = __webpack_require__(54); + var Popup = __webpack_require__(58); + var MixinLoader = __webpack_require__(61); + var Activator = __webpack_require__(38); + var locales = __webpack_require__(66); - this.redrawRequested = false; - this.renderTimer = false; - this.requiresTimeout = true; - this.continueRendering = true; - this.renderRequests = 0; + // Load custom shapes into CanvasRenderingContext2D + __webpack_require__(67); - this.translation = { x: 0, y: 0 }; - this.scale = 1; - this.canvasTopLeft = { x: 0, y: 0 }; - this.canvasBottomRight = { x: 0, y: 0 }; + var PhysicsEngine = __webpack_require__(68).PhysicsEngine; + var ClusterEngine = __webpack_require__(75).ClusterEngine; + var CanvasRenderer = __webpack_require__(76).CanvasRenderer; + var Canvas = __webpack_require__(77).Canvas; + var View = __webpack_require__(78).View; + var TouchEventHandler = __webpack_require__(80).TouchEventHandler; + var SelectionHandler = __webpack_require__(79).SelectionHandler; - this.body.emitter.on("_setScale", function (scale) { - return _this.scale = scale; - }); - this.body.emitter.on("_setTranslation", function (translation) { - _this.translation.x = translation.x;_this.translation.y = translation.y; - }); - this.body.emitter.on("_redraw", this._redraw.bind(this)); - this.body.emitter.on("_redrawHidden", this._redraw.bind(this, true)); - this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); - this.body.emitter.on("_startRendering", function () { - _this.renderRequests += 1;_this.continueRendering = true;_this.startRendering(); - }); - this.body.emitter.on("_stopRendering", function () { - _this.renderRequests -= 1;_this.continueRendering = _this.renderRequests > 0; - }); - this._determineBrowserMethod(); + /** + * @constructor Network + * Create a network visualization, displaying nodes and edges. + * + * @param {Element} container The DOM element in which the Network will + * be created. Normally a div element. + * @param {Object} data An object containing parameters + * {Array} nodes + * {Array} edges + * @param {Object} options Options + */ + function Network(container, data, options) { + if (!(this instanceof Network)) { + throw new SyntaxError("Constructor must be called with the new operator"); } - _prototypeProperties(CanvasRenderer, null, { - startRendering: { - value: function startRendering() { - if (this.continueRendering === 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(); - } + this._initializeMixinLoaders(); - this._redraw(); - if (this.requiresTimeout == false) { - // this schedules a new simulation step - this.startRendering(); + // render and calculation settings + this.initializing = true; + + this.triggerFunctions = { add: null, edit: null, editEdge: null, connect: null, del: null }; + + var 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); + } + }; + + // set constant values + this.defaultOptions = { + nodes: { + customScalingFunction: customScalingFunction, + mass: 1, + radiusMin: 10, + radiusMax: 30, + radius: 10, + shape: "ellipse", + image: undefined, + widthMin: 16, // px + widthMax: 64, // px + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + fontFill: undefined, + fontStrokeWidth: 0, // px + fontStrokeColor: "#ffffff", + fontDrawThreshold: 3, + scaleFontWithValue: false, + fontSizeMin: 14, + fontSizeMax: 30, + fontSizeMaxVisible: 30, + value: 1, + level: -1, + color: { + border: "#2B7CE9", + background: "#97C2FC", + highlight: { + border: "#2B7CE9", + background: "#D2E5FF" + }, + hover: { + border: "#2B7CE9", + background: "#D2E5FF" } }, - writable: true, - configurable: true + group: undefined, + borderWidth: 1, + borderWidthSelected: undefined }, - setCanvas: { - value: function setCanvas(canvas) { - this.canvas = canvas; + edges: { + customScalingFunction: customScalingFunction, + widthMin: 1, // + widthMax: 15, // + width: 1, + widthSelectionMultiplier: 2, + hoverWidth: 1.5, + value: 1, + style: "line", + color: { + color: "#848484", + highlight: "#848484", + hover: "#848484" }, - 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(); + opacity: 1, + fontColor: "#343434", + fontSize: 14, // px + fontFace: "arial", + fontFill: "white", + fontStrokeWidth: 0, // px + fontStrokeColor: "white", + labelAlignment: "horizontal", + arrowScaleFactor: 1, + dash: { + length: 10, + gap: 5, + altLength: undefined }, - writable: true, - configurable: true + inheritColor: "from", // to, from, false, true (== from) + useGradients: false // release in 4.0 }, - _requestRedraw: { - - /** - * 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(hidden) { - if (this.redrawRequested !== true) { - this.redrawRequested = true; - if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, hidden), 0); - } else { - window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); - } + navigation: { + enabled: false + }, + dataManipulation: { + enabled: false, + initiallyVisible: false + }, + hierarchicalLayout: { + enabled: false, + levelSeparation: 150, + nodeSpacing: 100, + direction: "UD", // UD, DU, LR, RL + layout: "hubsize" // hubsize, directed + }, + interaction: { + dragNodes: true, + dragView: true, + zoomView: true, + hoverEnabled: false, + tooltip: { + delay: 300, + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" } }, - writable: true, - configurable: true + keyboard: { + enabled: false, + speed: { x: 10, y: 10, zoom: 0.02 }, + bindToWindow: true + } }, - _redraw: { - value: function _redraw() { - var hidden = arguments[0] === undefined ? false : arguments[0]; - this.body.emitter.emit("_beforeRender"); - - this.redrawRequested = false; - var ctx = this.canvas.frame.canvas.getContext("2d"); + selection: { + enabled: true, + selectConnectedEdges: true + }, + smoothCurves: { + enabled: true, + dynamic: true, + type: "continuous", + roundness: 0.5 + }, + locale: "en", + locales: locales, + useDefaultGroups: true + }; + this.constants = util.extend({}, this.defaultOptions); - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + // containers for nodes and edges + this.body = { + nodes: {}, + nodeIndices: [], + supportNodes: {}, + supportNodeIndices: [], + edges: {}, + data: { + nodes: null, // A DataSet or DataView + edges: null // A DataSet or DataView + }, + functions: { + createNode: this._createNode.bind(this), + createEdge: this._createEdge.bind(this) + }, + 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 } + } + }; - // clear the canvas - var w = this.canvas.frame.canvas.clientWidth; - var h = this.canvas.frame.canvas.clientHeight; - ctx.clearRect(0, 0, w, h); + // modules + this.canvas = new Canvas(this.body); + this.selectionHandler = new SelectionHandler(this.body, this.canvas); + this.touchHandler = new TouchEventHandler(this.body, this.canvas, this.selectionHandler); + this.view = new View(this.body, this.canvas); + this.renderer = new CanvasRenderer(this.body, this.canvas); + this.clustering = new ClusterEngine(this.body); + this.physics = new PhysicsEngine(this.body); - // set scaling and translation - ctx.save(); - ctx.translate(this.translation.x, this.translation.y); - ctx.scale(this.scale, this.scale); + // create the DOM elements + this.canvas.create(); - 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 }); + this.hoverObj = { nodes: {}, edges: {} }; + this.controlNodesActive = false; + this.navigationHammers = []; + this.manipulationHammers = []; - if (hidden === false) { - // todo: solve this - //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { - this._drawEdges(ctx); - //} - } + // Node variables + var me = this; + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function (status) { + me._requestRedraw(); + }); - // todo: solve this - //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { - this._drawNodes(ctx, this.body.nodes, hidden); - //} + // keyboard navigation variables + this.xIncrement = 0; + this.yIncrement = 0; + this.zoomIncrement = 0; - if (hidden === false) { - if (this.controlNodesActive == true) { - this._drawControlNodes(ctx); - } - } + // loading all the mixins: + // load the force calculation functions, grouped under the physics system. + //this._loadPhysicsSystem(); + // create a frame and canvas + // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) + // load the selection system. (mandatory, required by Network) + this._loadSelectionSystem(); + // load the selection system. (mandatory, required by Network) + //this._loadHierarchySystem(); - //this._drawNodes(ctx,this.body.supportNodes,true); - // this.physics.nodesSolver._debug(ctx,"#F00F0F"); + // apply options + this.setOptions(options); - // restore original scaling and translation - ctx.restore(); + // other vars + this.cachedFunctions = {}; + this.startedStabilization = false; + this.stabilized = false; + this.stabilizationIterations = null; + this.draggingNodes = false; - if (hidden === true) { - ctx.clearRect(0, 0, w, h); - } - }, - writable: true, - configurable: true - }, - _drawNodes: { + // position and scale variables and objects + this.pointerPosition = { x: 0, y: 0 }; // coordinates of the bottom right of the canvas. they will be set during _redraw + this.scale = 1; // defining the global scale variable in the constructor - /** - * 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, nodes) { - var alwaysShow = arguments[2] === undefined ? false : arguments[2]; - // first draw the unselected nodes - var selected = []; + // create event listeners used to subscribe on the DataSets of the nodes and edges + this.nodesListeners = { + add: function (event, params) { + me._addNodes(params.items); + me.start(); + }, + update: function (event, params) { + me._updateNodes(params.items, params.data); + me.start(); + }, + remove: function (event, params) { + me._removeNodes(params.items); + me.start(); + } + }; + this.edgesListeners = { + add: function (event, params) { + me._addEdges(params.items); + me.start(); + }, + update: function (event, params) { + me._updateEdges(params.items); + me.start(); + }, + remove: function (event, params) { + me._removeEdges(params.items); + me.start(); + } + }; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].setScaleAndPos(this.scale, this.canvasTopLeft, this.canvasBottomRight); - if (nodes[id].isSelected()) { - selected.push(id); - } else { - if (alwaysShow === true) { - nodes[id].draw(ctx); - } else if (nodes[id].inArea() === true) { - nodes[id].draw(ctx); - } - } - } - } - // draw the selected nodes on top - for (var s = 0, sMax = selected.length; s < sMax; s++) { - if (nodes[selected[s]].inArea() || alwaysShow) { - nodes[selected[s]].draw(ctx); - } - } - }, - writable: true, - configurable: true - }, - _drawEdges: { + // properties for the animation + this.moving = true; + this.renderTimer = undefined; // Scheduling function. Is definded in this.start(); + // load data (the disable start variable will be the same as the enabled clustering) + this.setData(data, this.constants.hierarchicalLayout.enabled); - /** - * 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; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.setScale(this.scale); - if (edge.connected === true) { - edges[id].draw(ctx); - } - } - } - }, - writable: true, - configurable: true - }, - _drawControlNodes: { + // hierarchical layout + if (this.constants.hierarchicalLayout.enabled == true) { + this._setupHierarchicalLayout(); + } else { + // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here. + if (this.constants.stabilize == false) { + this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); + } + } - /** - * 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; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - edges[id]._drawControlNodes(ctx); - } - } - }, - writable: true, - configurable: true - }, - _determineBrowserMethod: { + if (this.constants.stabilize == false) { + this.initializing = false; + } - /** - * 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 + var me = this; + // this event will trigger a rebuilding of the cache of colors, nodes etc. + this.on("_dataChanged", function () { + me._updateNodeIndexList(); + me.physics._updateCalculationNodes(); + me._markAllEdgesAsDirty(); + if (me.initializing !== true) { + me.moving = true; + me.start(); } }); - return CanvasRenderer; - })(); + this.on("_newEdgesCreated", this._createBezierNodes.bind(this)); + //this.on("stabilizationIterationsDone", function () {me.initializing = false; me.start();}.bind(this)); + } - exports.CanvasRenderer = CanvasRenderer; - Object.defineProperty(exports, "__esModule", { - value: true - }); + // Extend Network with an Emitter mixin + Emitter(Network.prototype); -/***/ }, -/* 59 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + Network.prototype._createNode = function (properties) { + return new Node(properties, this.images, this.groups, this.constants); + }; + + Network.prototype._createEdge = function (properties) { + return new Edge(properties, this.body, this.constants); + }; + - 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 26-Feb-15. + * Update the this.body.nodeIndices with the most recent node index list + * @private */ + Network.prototype._updateNodeIndexList = function () { + this.body.supportNodeIndices = Object.keys(this.body.supportNodes); + this.body.nodeIndices = Object.keys(this.body.nodes); + }; - var Hammer = __webpack_require__(45); + /** + * 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, disableStart) { + if (disableStart === undefined) { + disableStart = false; + } - var Canvas = (function () { - /** - * 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 - */ - function Canvas(body, options) { - var _this = this; - _classCallCheck(this, Canvas); + // unselect all to ensure no selections from old data are carried over. + this.selectionHandler.unselectAll(); - this.body = body; - this.setOptions(options); + // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added. + this.initializing = true; - this.translation = { x: 0, y: 0 }; - this.scale = 1; - this.body.emitter.on("_setScale", function (scale) { - _this.scale = scale; - }); - this.body.emitter.on("_setTranslation", function (translation) { - _this.translation.x = translation.x;_this.translation.y = translation.y; - }); - this.body.emitter.once("resize", function (obj) { - _this.translation.x = obj.width * 0.5;_this.translation.y = obj.height * 0.5;_this.body.emitter.emit("_setTranslation", _this.translation); - }); + 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."); + } - this.pixelRatio = 1; + // clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button. + if (this.constants.dataManipulation.enabled == true) { + this._createManipulatorBar(); + } - // remove all elements from the container element. - while (this.body.container.hasChildNodes()) { - this.body.container.removeChild(this.body.container.firstChild); + // 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._setNodes(data && data.nodes); + this._setEdges(data && data.edges); + } - 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.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); + if (disableStart == false) { + if (this.constants.hierarchicalLayout.enabled == true) { + this._resetLevels(); + this._setupHierarchicalLayout(); } else { - var ctx = this.frame.canvas.getContext("2d"); - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - - //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. - this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + // find a stable position or start animating to a stable position + this.body.emitter.emit("stabilize"); } - - // add the frame to the container element - this.body.container.appendChild(this.frame); - - this.body.emitter.emit("_setScale", 1);; - this.body.emitter.emit("_setTranslation", { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight });; - - this._bindHammer(); + } else { + this.initializing = false; } + }; - _prototypeProperties(Canvas, null, { - _bindHammer: { - + /** + * Set options + * @param {Object} options + */ + Network.prototype.setOptions = function (options) { + if (options) { + var prop; + var fields = ["nodes", "edges", "smoothCurves", "hierarchicalLayout", "navigation", "keyboard", "dataManipulation", "onAdd", "onEdit", "onEditEdge", "onConnect", "onDelete", "clickToUse"]; + // extend all but the values in fields + util.selectiveNotDeepExtend(fields, this.constants, options); + util.selectiveNotDeepExtend(["color"], this.constants.nodes, options.nodes); + util.selectiveNotDeepExtend(["color", "length"], this.constants.edges, options.edges); - /** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. - * @private - */ - value: function _bindHammer() { - var me = this; - if (this.hammer !== undefined) { - this.hammer.dispose(); - } - this.drag = {}; - this.pinch = {}; - this.hammer = Hammer(this.frame.canvas, { - prevent_default: true - }); - this.hammer.on("tap", me.body.eventListeners.onTap); - this.hammer.on("doubletap", me.body.eventListeners.onDoubleTap); - this.hammer.on("hold", me.body.eventListeners.onHold); - this.hammer.on("touch", me.body.eventListeners.onTouch); - this.hammer.on("dragstart", me.body.eventListeners.onDragStart); - this.hammer.on("drag", me.body.eventListeners.onDrag); - this.hammer.on("dragend", me.body.eventListeners.onDragEnd); + this.groups.useDefaultGroups = this.constants.useDefaultGroups; - if (this.options.zoomable == true) { - this.hammer.on("mousewheel", me.body.eventListeners.onMouseWheel.bind(me)); - this.hammer.on("DOMMouseScroll", me.body.eventListeners.onMouseWheel.bind(me)); // for FF - this.hammer.on("pinch", me.body.eventListeners.onPinch.bind(me)); - } + this.physics.setOptions(options.physics); + this.canvas.setOptions(options.canvas); + this.renderer.setOptions(options.rendering); + this.touchHandler.setOptions(options.interaction); + this.selectionHandler.setOptions(options.selection); - this.hammer.on("mousemove", me.body.eventListeners.onMouseMove.bind(me)); - this.hammerFrame = Hammer(this.frame, { - prevent_default: true - }); - this.hammerFrame.on("release", me.body.eventListeners.onRelease.bind(me)); - }, - writable: true, - configurable: true - }, - setOptions: { - value: function setOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; - this.options = options; - }, - writable: true, - configurable: true - }, - setSize: { + if (options.onAdd) { + this.triggerFunctions.add = options.onAdd; + } + if (options.onEdit) { + this.triggerFunctions.edit = options.onEdit; + } + if (options.onEditEdge) { + this.triggerFunctions.editEdge = options.onEditEdge; + } + if (options.onConnect) { + this.triggerFunctions.connect = options.onConnect; + } + if (options.onDelete) { + this.triggerFunctions.del = options.onDelete; + } - /** - * 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(width, height) { - 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; + util.mergeOptions(this.constants, options, "smoothCurves"); + util.mergeOptions(this.constants, options, "hierarchicalLayout"); + util.mergeOptions(this.constants, options, "clustering"); + util.mergeOptions(this.constants, options, "navigation"); + util.mergeOptions(this.constants, options, "keyboard"); + util.mergeOptions(this.constants, options, "dataManipulation"); - 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; + if (options.dataManipulation) { + this.editMode = this.constants.dataManipulation.initiallyVisible; + } - this.options.width = width; - this.options.height = height; - emitEvent = true; + // TODO: work out these options and document them + if (options.edges) { + if (options.edges.color !== undefined) { + if (util.isString(options.edges.color)) { + this.constants.edges.color = {}; + this.constants.edges.color.color = options.edges.color; + this.constants.edges.color.highlight = options.edges.color; + this.constants.edges.color.hover = options.edges.color; } else { - // 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 (options.edges.color.color !== undefined) { + this.constants.edges.color.color = options.edges.color.color; } - if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - emitEvent = true; + if (options.edges.color.highlight !== undefined) { + this.constants.edges.color.highlight = options.edges.color.highlight; + } + if (options.edges.color.hover !== undefined) { + this.constants.edges.color.hover = options.edges.color.hover; } } + this.constants.edges.inheritColor = false; + } - 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 }); + if (!options.edges.fontColor) { + if (options.edges.color !== undefined) { + if (util.isString(options.edges.color)) { + this.constants.edges.fontColor = options.edges.color; + } else if (options.edges.color.color !== undefined) { + this.constants.edges.fontColor = options.edges.color.color; + } } - }, - writable: true, - configurable: true - }, - _XconvertDOMtoCanvas: { + } + } + if (options.nodes) { + if (options.nodes.color) { + var newColorObj = util.parseColor(options.nodes.color); + this.constants.nodes.color.background = newColorObj.background; + this.constants.nodes.color.border = newColorObj.border; + this.constants.nodes.color.highlight.background = newColorObj.highlight.background; + this.constants.nodes.color.highlight.border = newColorObj.highlight.border; + this.constants.nodes.color.hover.background = newColorObj.hover.background; + this.constants.nodes.color.hover.border = newColorObj.hover.border; + } + } + if (options.groups) { + for (var groupname in options.groups) { + if (options.groups.hasOwnProperty(groupname)) { + var group = options.groups[groupname]; + this.groups.add(groupname, group); + } + } + } - /** - * 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.translation.x) / this.scale; - }, - writable: true, - configurable: true - }, - _XconvertCanvasToDOM: { + if (options.tooltip) { + for (prop in options.tooltip) { + if (options.tooltip.hasOwnProperty(prop)) { + this.constants.tooltip[prop] = options.tooltip[prop]; + } + } + if (options.tooltip.color) { + this.constants.tooltip.color = util.parseColor(options.tooltip.color); + } + } - /** - * 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.scale + this.translation.x; - }, - writable: true, - configurable: true - }, - _YconvertDOMtoCanvas: { + if ("clickToUse" in options) { + if (options.clickToUse) { + if (!this.activator) { + this.activator = new Activator(this.frame); + this.activator.on("change", this._createKeyBinds.bind(this)); + } + } else { + if (this.activator) { + this.activator.destroy(); + delete this.activator; + } + } + } - /** - * 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.translation.y) / this.scale; - }, - writable: true, - configurable: true - }, - _YconvertCanvasToDOM: { + if (options.labels) { + throw new Error("Option \"labels\" is deprecated. Use options \"locale\" and \"locales\" instead."); + } - /** - * 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.scale + this.translation.y; - }, - writable: true, - configurable: true - }, - canvasToDOM: { + // (Re)loading the mixins that can be enabled or disabled in the options. + // load the force calculation functions, grouped under the physics system. + // load the navigation system. + //this._loadNavigationControls(); + //// load the data manipulation system + //this._loadManipulationSystem(); + //// configure the smooth curves + //this._configureSmoothCurves(); + // bind hammer + this.canvas._bindHammer(); - /** - * - * @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: { + // bind keys. If disabled, this will not do anything; + //this._createKeyBinds(); - /** - * - * @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 + this._markAllEdgesAsDirty(); + this.canvas.setSize(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); } - }); - - return Canvas; - })(); - - exports.Canvas = Canvas; - Object.defineProperty(exports, "__esModule", { - value: true - }); - -/***/ }, -/* 60 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + if (this.initializing !== true) { + this.moving = true; + this.start(); + } + } + }; - 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 26-Feb-15. + * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin + * @private */ + Network.prototype._createKeyBinds = function () { + return; - var util = __webpack_require__(1); - - var View = (function () { - function View(body, options) { - var _this = this; - _classCallCheck(this, View); - - this.body = body; - this.setOptions(options); - - 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 = null; - this.lockedOnNodeOffset = null; - this.touchTime = 0; - - this.translation = { x: 0, y: 0 }; - this.scale = 1; - - this.viewFunction = undefined; - - this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); - this.body.emitter.on("_setScale", function (scale) { - return _this.scale = scale; - }); - this.body.emitter.on("_setTranslation", function (translation) { - _this.translation.x = translation.x;_this.translation.y = translation.y; - }); - this.body.emitter.on("animationFinished", function () { - _this.body.emitter.emit("_stopRendering"); - }); - this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); - } - - _prototypeProperties(View, null, { - setOptions: { - value: function setOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; - this.options = options; - }, - writable: true, - configurable: true - }, - setCanvas: { - value: function setCanvas(canvas) { - this.canvas = canvas; - }, - writable: true, - configurable: true - }, - _getRange: { + //var me = this; + //if (this.keycharm !== undefined) { + // this.keycharm.destroy(); + //} + // + //if (this.constants.keyboard.bindToWindow == true) { + // this.keycharm = keycharm({container: window, preventDefault: false}); + //} + //else { + // this.keycharm = keycharm({container: this.frame, preventDefault: false}); + //} + // + //this.keycharm.reset(); + // + //if (this.constants.keyboard.enabled && this.isActive()) { + // this.keycharm.bind("up", this._moveUp.bind(me) , "keydown"); + // this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup"); + // this.keycharm.bind("down", this._moveDown.bind(me) , "keydown"); + // this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup"); + // this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown"); + // this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup"); + // this.keycharm.bind("right",this._moveRight.bind(me), "keydown"); + // this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup"); + // this.keycharm.bind("=", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("=", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("-", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("-", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("[", this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("[", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("]", this._zoomOut.bind(me), "keydown"); + // this.keycharm.bind("]", this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown"); + // this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup"); + // this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); + // this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); + //} + // + //if (this.constants.dataManipulation.enabled == true) { + // this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); + // this.keycharm.bind("delete",this._deleteSelected.bind(me)); + //} + }; - // 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.boundingBox.left) { - minX = node.boundingBox.left; - } - if (maxX < node.boundingBox.right) { - maxX = node.boundingBox.right; - } - if (minY > node.boundingBox.bottom) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.boundingBox.top) { - maxY = node.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.boundingBox.left) { - minX = node.boundingBox.left; - } - if (maxX < node.boundingBox.right) { - maxX = node.boundingBox.right; - } - if (minY > node.boundingBox.bottom) { - minY = node.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.boundingBox.top) { - maxY = node.boundingBox.bottom; - } // top is negative, bottom is positive - } - } - } + /** + * 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.start = function () {}; + this.redraw = function () {}; + this.renderTimer = false; - 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: { + // cleanup physicsConfiguration if it exists + this._cleanupPhysicsConfiguration(); + // remove keybindings + this.keycharm.reset(); - /** - * @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: { + // clear hammer bindings + this.hammer.destroy(); + // clear events + this.off(); - /** - * 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; + this._recursiveDOMDelete(this.containerElement); + }; - 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; - } + Network.prototype._recursiveDOMDelete = function (DOMobject) { + while (DOMobject.hasChildNodes() == true) { + this._recursiveDOMDelete(DOMobject.firstChild); + DOMobject.removeChild(DOMobject.firstChild); + } + }; - range = this._getRange(options.nodes); + /** + * 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 + */ + Network.prototype._checkShowPopup = function (pointer) { + var obj = { + left: this._XconvertDOMtoCanvas(pointer.x), + top: this._YconvertDOMtoCanvas(pointer.y), + right: this._XconvertDOMtoCanvas(pointer.x), + bottom: this._YconvertDOMtoCanvas(pointer.y) + }; - var numberOfNodes = this.body.nodeIndices.length; - if (this.options.smoothCurves == true) { - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - } else { - zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + var id; + var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = "node"; + + if (this.popupObj == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.body.nodes; + var overlappingNodes = []; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.isOverlappingWith(obj)) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(id); } + } + } + } - // 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("_redrawHidden"); - 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 (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 = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; + } + } - var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; - var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; - zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + if (this.popupObj === undefined && nodeUnderCursor == false) { + // search the edges for overlap + var edges = this.body.edges; + var overlappingEdges = []; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected === true && edge.getTitle() !== undefined && edge.isOverlappingWith(obj)) { + overlappingEdges.push(id); } + } + } - if (zoomLevel > 1) { - zoomLevel = 1; - } + if (overlappingEdges.length > 0) { + this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = "edge"; + } + } + if (this.popupObj) { + // show popup message window + if (this.popupObj.id != previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new Popup(this.frame, this.constants.tooltip); + } - var center = this._findCenter(range); - var animationOptions = { position: center, scale: zoomLevel, animation: options }; - this.moveTo(animationOptions); - }, - writable: true, - configurable: true - }, - focusOnNode: { + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; - // animation + // 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(); + } + } + }; - /** - * 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: { + /** + * 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 + */ + Network.prototype._checkHidePopup = function (pointer) { + var pointerObj = { + left: this._XconvertDOMtoCanvas(pointer.x), + top: this._YconvertDOMtoCanvas(pointer.y), + right: this._XconvertDOMtoCanvas(pointer.x), + bottom: this._YconvertDOMtoCanvas(pointer.y) + }; - /** - * - * @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.scale; - } - if (options.position === undefined) { - options.position = this.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 + var stillOnObj = false; + if (this.popup.popupTargetType == "node") { + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + if (stillOnObj === true) { + var overNode = this.getNodeAt(pointer); + stillOnObj = overNode.id == this.popup.popupTargetId; + } + } else { + if (this.getNodeAt(pointer) === null) { + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + } + } - 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; - } + if (stillOnObj === false) { + this.popupObj = undefined; + this.popup.hide(); + } + }; - // 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.scale; - this.sourceTranslation = this.translation; - this.targetScale = options.scale; + /** + * Set a data set with nodes for the network + * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * @private + */ + Network.prototype._setNodes = function (nodes) { + var oldNodesData = this.body.data.nodes; - // 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.emitter.emit("_setScale", 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 (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 the time is set to 0, don't do an animation - if (options.animation.duration == 0) { - if (this.lockedOnNodeId != null) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("_beforeRender", this.viewFunction); - } else { - this.body.emitter.emit("_setScale", this.targetScale);; - this.body.emitter.emit("_setTranslation", 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; + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.off(event, callback); + }); + } + // remove drawn nodes + this.body.nodes = {}; - this.viewFunction = this._transitionRedraw.bind(this); - this.body.emitter.on("_beforeRender", this.viewFunction); - this.body.emitter.emit("_startRendering"); - } - }, - writable: true, - configurable: true - }, - _lockedRedraw: { + 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); + }); - /** - * 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.translation; - var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y - }; + // draw all new nodes + var ids = this.body.data.nodes.getIds(); + this._addNodes(ids); + } + this._updateSelection(); + }; - this.body.emitter.emit("_setTranslation", targetTranslation); - }, - writable: true, - configurable: true - }, - releaseNode: { - value: function releaseNode() { - if (this.lockedOnNodeId !== undefined) { - this.body.emitter.off("_beforeRender", this.viewFunction); - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; - } - }, - writable: true, - configurable: true - }, - _transitionRedraw: { + /** + * Add nodes + * @param {Number[] | String[]} ids + * @private + */ + Network.prototype._addNodes = function (ids) { + var id; + for (var i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + var data = this.body.data.nodes.get(id); + var node = new Node(data, this.images, this.groups, this.constants); + this.body.nodes[id] = node; // note: this may replace an existing node + if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { + var radius = 10 * 0.1 * ids.length + 10; + var angle = 2 * Math.PI * Math.random(); + if (node.xFixed == false) { + node.x = radius * Math.cos(angle); + } + if (node.yFixed == false) { + node.y = radius * Math.sin(angle); + } + } + this.moving = true; + } - /** - * - * @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._updateNodeIndexList(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this.physics._updateCalculationNodes(); + this._reconnectEdges(); + this._updateValueRange(this.body.nodes); + }; - var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); + /** + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + Network.prototype._updateNodes = function (ids, changedData) { + var nodes = this.body.nodes; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = changedData[i]; + if (node) { + // update node + node.setProperties(data, this.constants); + } else { + // create node + node = new Node(properties, this.images, this.groups, this.constants); + nodes[id] = node; + } + } + this.moving = true; + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this._updateNodeIndexList(); + this._updateValueRange(nodes); + this._markAllEdgesAsDirty(); + }; - this.body.emitter.emit("_setScale", this.sourceScale + (this.targetScale - this.sourceScale) * progress); - this.body.emitter.emit("_setTranslation", { - 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("_beforeRender", this.viewFunction); - this.easingTime = 0; - if (this.lockedOnNodeId != null) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("_beforeRender", this.viewFunction); - } - this.body.emitter.emit("animationFinished"); - } - }, - writable: true, - configurable: true - } - }); + Network.prototype._markAllEdgesAsDirty = function () { + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].colorDirty = true; + } + }; - return View; - })(); + /** + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids + * @private + */ + Network.prototype._removeNodes = function (ids) { + var nodes = this.body.nodes; - exports.View = View; - Object.defineProperty(exports, "__esModule", { - value: true - }); + // remove from selection + for (var i = 0, len = ids.length; i < len; i++) { + if (this.selectionObj.nodes[ids[i]] !== undefined) { + this.body.nodes[ids[i]].unselect(); + this._removeFromSelection(this.body.nodes[ids[i]]); + } + } -/***/ }, -/* 61 */ -/***/ function(module, exports, __webpack_require__) { + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + delete nodes[id]; + } - "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"); } }; + this._updateNodeIndexList(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this.physics._updateCalculationNodes(); + this._reconnectEdges(); + this._updateSelection(); + this._updateValueRange(nodes); + }; /** - * Created by Alex on 2/27/2015. - * + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private + * @private */ + Network.prototype._setEdges = function (edges) { + var oldEdgesData = this.body.data.edges; - var SelectionHandler = __webpack_require__(73).SelectionHandler; - var util = __webpack_require__(1); - - var TouchEventHandler = (function () { - function TouchEventHandler(body) { - var _this = this; - _classCallCheck(this, TouchEventHandler); - - this.body = body; + 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.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); + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.off(event, callback); + }); + } - this.touchTime = 0; - this.drag = {}; - this.pinch = {}; - this.pointerPosition = { x: 0, y: 0 }; + // remove drawn edges + this.body.edges = {}; - this.scale = 1; - this.body.emitter.on("_setScale", function (scale) { - return _this.scale = scale; + if (this.body.data.edges) { + // subscribe to new dataset + var me = this; + util.forEach(this.edgesListeners, function (callback, event) { + me.body.data.edges.on(event, callback); }); - this.selectionHandler = new SelectionHandler(body); + // draw all new nodes + var ids = this.body.data.edges.getIds(); + this._addEdges(ids); } - _prototypeProperties(TouchEventHandler, null, { - setCanvas: { - value: function setCanvas(canvas) { - this.canvas = canvas; - this.selectionHandler.setCanvas(canvas); - }, - writable: true, - configurable: true - }, - getPointer: { + this._reconnectEdges(); + }; - /** - * Get the pointer location from a touch location - * @param {{pageX: Number, pageY: Number}} touch - * @return {{x: Number, y: Number}} pointer - * @private - */ - value: function getPointer(touch) { - return { - x: touch.pageX - util.getAbsoluteLeft(this.canvas.frame.canvas), - y: touch.pageY - util.getAbsoluteTop(this.canvas.frame.canvas) - }; - }, - writable: true, - configurable: true - }, - onTouch: { + /** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ + Network.prototype._addEdges = function (ids) { + var edges = this.body.edges, + edgesData = this.body.data.edges; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; - /** - * 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.gesture.center); - this.drag.pinched = false; - this.pinch.scale = this.scale; + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } - // 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: { + var data = edgesData.get(id, { showInternalIds: true }); + edges[id] = new Edge(data, this.body, this.constants); + } + this.moving = true; + this._updateValueRange(edges); + this._createBezierNodes(); + this.physics._updateCalculationNodes(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + }; - /** - * handle tap/click event: select/unselect a node - * @private - */ - value: function onTap(event) { - console.log("tap", event); - var pointer = this.getPointer(event.gesture.center); - this.pointerPosition = pointer; - this.selectionHandler.selectOnPoint(pointer); - }, - writable: true, - configurable: true - }, - onDragStart: { + /** + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + Network.prototype._updateEdges = function (ids) { + var edges = this.body.edges; + var edgesData = this.body.data.edges; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; - /** - * handle drag start event - * @private - */ + var data = edgesData.get(id); + var edge = edges[id]; + if (edge) { + // update edge + edge.disconnect(); + edge.setProperties(data); + edge.connect(); + } else { + // create edge + edge = new Edge(data, this.body, this.constants); + this.body.edges[id] = edge; + } + } - /** - * 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) {}, - writable: true, - configurable: true - }, - onDrag: { + this._createBezierNodes(); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this.moving = true; + this._updateValueRange(edges); + }; + + /** + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids + * @private + */ + Network.prototype._removeEdges = function (ids) { + var edges = this.body.edges; + // remove from selection + for (var i = 0, len = ids.length; i < len; i++) { + if (this.selectionObj.edges[ids[i]] !== undefined) { + edges[ids[i]].unselect(); + this._removeFromSelection(edges[ids[i]]); + } + } - /** - * handle drag event - * @private - */ - value: function onDrag(event) {}, - writable: true, - configurable: true - }, - onDragEnd: { + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge) { + if (edge.via != null) { + delete this.body.supportNodes[edge.via.id]; + } + edge.disconnect(); + delete edges[id]; + } + } + this.moving = true; + this._updateValueRange(edges); + if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { + this._resetLevels(); + this._setupHierarchicalLayout(); + } + this.physics._updateCalculationNodes(); + }; - /** - * handle drag start event - * @private - */ - value: function onDragEnd(event) {}, - writable: true, - configurable: true - }, - onDoubleTap: { + /** + * Reconnect all edges + * @private + */ + Network.prototype._reconnectEdges = function () { + var id, + nodes = this.body.nodes, + edges = this.body.edges; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } + } - /** - * handle doubletap event - * @private - */ - value: function onDoubleTap(event) {}, - writable: true, - configurable: true - }, - onHold: { + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } + } + }; + /** + * Update the values of all object in the given array according to the current + * value range of the objects in the array. + * @param {Object} obj An object containing a set of Edges or Nodes + * The objects must have a method getValue() and + * setValueRange(min, max). + * @private + */ + 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; + } + } + } - /** - * handle long tap event: multi select nodes - * @private - */ - value: function onHold(event) {}, - 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) {}, - writable: true, - configurable: true - }, - onPinch: { + /** + * Set the translation of the network + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + * @private + */ + Network.prototype._setTranslation = function (offsetX, offsetY) { + if (this.translation === undefined) { + this.translation = { + x: 0, + y: 0 + }; + } + if (offsetX !== undefined) { + this.translation.x = offsetX; + } + if (offsetY !== undefined) { + this.translation.y = offsetY; + } - /** - * Handle pinch event - * @param event - * @private - */ - value: function onPinch(event) {}, - writable: true, - configurable: true - }, - _zoom: { + this.emit("viewChanged"); + }; + /** + * Get the translation of the network + * @return {Object} translation An object with parameters x and y, both a number + * @private + */ + Network.prototype._getTranslation = function () { + return { + x: this.translation.x, + y: this.translation.y + }; + }; - /** - * 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) {}, - writable: true, - configurable: true - }, - onMouseWheel: { + /** + * Scale the network + * @param {Number} scale Scaling factor 1.0 is unscaled + * @private + */ + Network.prototype._setScale = function (scale) { + this.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.scale; + }; - /** - * Event handler for mouse wheel event, used to zoom the timeline - * See http://adomas.org/javascript-mouse-wheel/ - * https://github.com/EightMedia/hammer.js/issues/256 - * @param {MouseEvent} event - * @private - */ - value: function onMouseWheel(event) {}, - writable: true, - configurable: true - }, - onMouseMove: { + /** + * Move the network according to the keyboard presses. + * + * @private + */ + Network.prototype._handleNavigation = function () { + if (this.xIncrement != 0 || this.yIncrement != 0) { + var translation = this._getTranslation(); + this._setTranslation(translation.x + this.xIncrement, translation.y + this.yIncrement); + } + if (this.zoomIncrement != 0) { + var center = { + x: this.frame.canvas.clientWidth / 2, + y: this.frame.canvas.clientHeight / 2 + }; + this.zoom(this.scale * (1 + this.zoomIncrement), center); + } + }; - /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private - */ - value: function onMouseMove(event) {}, - writable: true, - configurable: true + + /** + * Freeze the _animationStep + */ + Network.prototype.freezeSimulation = function (freeze) { + if (freeze == true) { + this.freezeSimulationEnabled = true; + this.moving = false; + } else { + this.freezeSimulationEnabled = false; + this.moving = true; + this.start(); + } + }; + + + /** + * This function cleans the support nodes if they are not needed and adds them when they are. + * + * @param {boolean} [disableStart] + * @private + */ + Network.prototype._configureSmoothCurves = function () { + var disableStart = arguments[0] === undefined ? true : arguments[0]; + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + this._createBezierNodes(); + // cleanup unused support nodes + for (var i = 0; i < this.body.supportNodeIndices.length; i++) { + var nodeId = this.body.supportNodeIndices[i]; + // delete support nodes for edges that have been deleted + if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) { + delete this.body.supportNodes[nodeId]; + } } - }); + } else { + // delete the support nodes + this.body.supportNodes = {}; + for (var edgeId in this.body.edges) { + if (this.body.edges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId].via = null; + } + } + } - return TouchEventHandler; - })(); - exports.TouchEventHandler = TouchEventHandler; - Object.defineProperty(exports, "__esModule", { - value: true - }); + this._updateNodeIndexList(); + this.physics._updateCalculationNodes(); + if (!disableStart) { + this.moving = true; + this.start(); + } + }; - // in case the touch event was triggered on an external div, do the initial touch now. - //if (this.drag.pointer === undefined) { - // this.onTouch(event); - //} - // - //var node = this._getNodeAt(this.drag.pointer); - //// note: drag.pointer is set in onTouch to get the initial touch location - // - //this.drag.dragging = true; - //this.drag.selection = []; - //this.drag.translation = this._getTranslation(); - //this.drag.nodeId = null; - //this.draggingNodes = false; - // - //if (node != null && this.constants.dragNodes == true) { - // this.draggingNodes = true; - // this.drag.nodeId = node.id; - // // select the clicked node if not yet selected - // if (!node.isSelected()) { - // this._selectObject(node, false); - // } - // - // this.emit("dragStart", {nodeIds: this.getSelection().nodes}); - // - // // create an array with the selected nodes and their original location and status - // for (var objectId in this.selectionObj.nodes) { - // if (this.selectionObj.nodes.hasOwnProperty(objectId)) { - // var object = this.selectionObj.nodes[objectId]; - // var s = { - // id: object.id, - // node: object, - // - // // store original x, y, xFixed and yFixed, make the node temporarily Fixed - // x: object.x, - // y: object.y, - // xFixed: object.xFixed, - // yFixed: object.yFixed - // }; - // - // object.xFixed = true; - // object.yFixed = true; - // - // this.drag.selection.push(s); - // } - // } - //} - //if (this.drag.pinched) { - // return; - //} - // - //// remove the focus on node if it is focussed on by the focusOnNode - //this.releaseNode(); - // - //var pointer = this.getPointer(event.gesture.center); - //var me = this; - //var drag = this.drag; - //var selection = drag.selection; - //if (selection && selection.length && this.constants.dragNodes == true) { - // // calculate delta's and new location - // var deltaX = pointer.x - drag.pointer.x; - // var deltaY = pointer.y - drag.pointer.y; - // - // // update position of all selected nodes - // selection.forEach(function (s) { - // var node = s.node; - // - // if (!s.xFixed) { - // node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); - // } - // - // if (!s.yFixed) { - // node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); - // } - // }); - // - // - // // start _animationStep if not yet running - // if (!this.moving) { - // this.moving = true; - // this.start(); - // } - //} - //else { - // // move the network - // if (this.constants.dragNetwork == 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; - // - // this._setTranslation( - // this.drag.translation.x + diffX, - // this.drag.translation.y + diffY - // ); - // this._redraw(); - // } - //} - //this.drag.dragging = false; - //var selection = this.drag.selection; - //if (selection && selection.length) { - // selection.forEach(function (s) { - // // restore original xFixed and yFixed - // s.node.xFixed = s.xFixed; - // s.node.yFixed = s.yFixed; - // }); - // this.moving = true; - // this.start(); - //} - //else { - // this._redraw(); - //} - //if (this.draggingNodes == false) { - // this.emit("dragEnd", {nodeIds: []}); - //} - //else { - // this.emit("dragEnd", {nodeIds: this.getSelection().nodes}); - //} - //var pointer = this.getPointer(event.gesture.center); - //this._handleDoubleTap(pointer); - //var pointer = this.getPointer(event.gesture.center); - //this.pointerPosition = pointer; - //this._handleOnHold(pointer); - //var pointer = this.getPointer(event.gesture.center); - //this._handleOnRelease(pointer); - //var pointer = this.getPointer(event.gesture.center); - // - //this.drag.pinched = true; - //if (!('scale' in this.pinch)) { - // this.pinch.scale = 1; - //} - // - //// TODO: enabled moving while pinching? - //var scale = this.pinch.scale * event.gesture.scale; - //this._zoom(scale, pointer) - //if (this.constants.zoomable == true) { - // var scaleOld = this._getScale(); - // if (scale < 0.00001) { - // scale = 0.00001; - // } - // if (scale > 10) { - // scale = 10; - // } - // - // var preScaleDragPointer = null; - // if (this.drag !== undefined) { - // if (this.drag.dragging == true) { - // preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); - // } - // } - // // + this.canvas.frame.canvas.clientHeight / 2 - // var translation = this._getTranslation(); - // - // var scaleFrac = scale / scaleOld; - // var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - // var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - // - // this._setScale(scale); - // this._setTranslation(tx, ty); - // - // if (preScaleDragPointer != null) { - // var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); - // this.drag.pointer.x = postScaleDragPointer.x; - // this.drag.pointer.y = postScaleDragPointer.y; - // } - // - // this._redraw(); - // - // if (scaleOld < scale) { - // this.emit("zoom", {direction: "+"}); - // } - // else { - // this.emit("zoom", {direction: "-"}); - // } - // - // return scale; - //} - //// retrieve delta - //var delta = 0; - //if (event.wheelDelta) { /* IE/Opera. */ - // delta = event.wheelDelta / 120; - //} else if (event.detail) { /* Mozilla case. */ - // // In Mozilla, sign of delta is different than in IE. - // // Also, delta is multiple of 3. - // delta = -event.detail / 3; - //} - // - //// If delta is nonzero, handle it. - //// Basically, delta is now positive if wheel was scrolled up, - //// and negative, if wheel was scrolled down. - //if (delta) { - // - // // calculate the new scale - // var scale = this._getScale(); - // var zoom = delta / 10; - // if (delta < 0) { - // zoom = zoom / (1 - zoom); - // } - // scale *= (1 + zoom); - // - // // calculate the pointer location - // var gesture = hammerUtil.fakeGesture(this, event); - // var pointer = this.getPointer(gesture.center); - // - // // apply the new scale - // this._zoom(scale, pointer); - //} - // - //// Prevent default actions caused by mouse wheel. - //event.preventDefault(); - //var gesture = hammerUtil.fakeGesture(this, event); - //var pointer = this.getPointer(gesture.center); - //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(); - // } - //} - // - //// if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over - //if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { - // this.canvas.frame.focus(); - //} - // - //// start a timeout that will check if the mouse is positioned above an element - //if (popupVisible === false) { - // var me = this; - // var checkShow = function() { - // me._checkShowPopup(pointer); - // }; - // - // if (this.popupTimer) { - // clearInterval(this.popupTimer); // stop any running calculationTimer - // } - // if (!this.drag.dragging) { - // this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); - // } - //} - // - ///** - // * Adding hover highlights - // */ - //if (this.constants.hover == true) { - // // removing all hover highlights - // for (var edgeId in this.hoverObj.edges) { - // if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - // this.hoverObj.edges[edgeId].hover = false; - // delete this.hoverObj.edges[edgeId]; - // } - // } - // - // // adding hover highlights - // var obj = this._getNodeAt(pointer); - // if (obj == null) { - // obj = this._getEdgeAt(pointer); - // } - // if (obj != null) { - // this._hoverObject(obj); - // } - // - // // removing all node hover highlights except for the selected one. - // for (var nodeId in this.hoverObj.nodes) { - // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { - // this._blurObject(this.hoverObj.nodes[nodeId]); - // delete this.hoverObj.nodes[nodeId]; - // } - // } - // } - // this.redraw(); - //} -/***/ }, -/* 62 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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. + * + * @private + */ + Network.prototype._createBezierNodes = function () { + var specificEdges = arguments[0] === undefined ? this.body.edges : arguments[0]; + if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { + for (var edgeId in specificEdges) { + if (specificEdges.hasOwnProperty(edgeId)) { + var edge = specificEdges[edgeId]; + if (edge.via == null) { + var nodeId = "edgeId:".concat(edge.id); + var node = new Node({ id: nodeId, + mass: 1, + shape: "circle", + image: "", + internalMultiplier: 1 + }, {}, {}, this.constants); + this.body.supportNodes[nodeId] = node; + edge.via = node; + edge.via.parentEdgeId = edge.id; + edge.positionBezierNode(); + } + } + } + this._updateNodeIndexList(); + } + }; + + /** + * load the functions that load the mixins into the prototype. + * + * @private + */ + Network.prototype._initializeMixinLoaders = function () { + for (var mixin in MixinLoader) { + if (MixinLoader.hasOwnProperty(mixin)) { + Network.prototype[mixin] = MixinLoader[mixin]; + } + } + }; - /** - * Expose `Emitter`. + * Load the XY positions of the nodes into the dataset. */ + Network.prototype.storePosition = function () { + console.log("storePosition is depricated: use .storePositions() from now on."); + this.storePositions(); + }; - module.exports = Emitter; + /** + * Load the XY positions of the nodes into the dataset. + */ + Network.prototype.storePositions = function () { + 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); + }; /** - * Initialize a new `Emitter`. - * - * @api public + * 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; + }; - function Emitter(obj) { - if (obj) return mixin(obj); + + /** + * Returns true when the Network is active. + * @returns {boolean} + */ + Network.prototype.isActive = function () { + return !this.activator || this.activator.active; }; + /** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private + * Sets the scale + * @returns {Number} */ + Network.prototype.setScale = function () { + return this._setScale(); + }; - 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 + * Returns the scale + * @returns {Number} */ + Network.prototype.getScale = function () { + return this._getScale(); + }; - Emitter.prototype.on = - Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; + + /** + * 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; + } }; /** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * 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 }); + }; - Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; - function on() { - self.off(event, on); - fn.apply(this, arguments); + Network.prototype.getBoundingBox = function (nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].boundingBox; } + }; - on.fn = fn; - this.on(event, on); - return this; + 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; + } + } + } + } + return nodeList; + }; + + + 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; + }; + + Network.prototype.generateColorObject = function (color) { + return util.parseColor(color); }; + module.exports = Network; + +/***/ }, +/* 54 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var Node = __webpack_require__(55); + /** - * Remove the given callback for `event` or all - * registered callbacks. + * @class Edge * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public + * A edge connects two nodes + * @param {Object} properties Object with properties. Must contain + * At least properties from and to. + * Available properties: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color */ + function Edge(properties, body, networkConstants) { + if (body === undefined) { + throw "No body provided"; + } + var fields = ["edges"]; + var constants = util.selectiveBridgeObject(fields, networkConstants); + this.options = constants.edges; - Emitter.prototype.off = - Emitter.prototype.removeListener = - Emitter.prototype.removeAllListeners = - Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; + this.options.smoothCurves = networkConstants.smoothCurves; + this.body = body; - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.title = undefined; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + this.value = undefined; + this.selected = false; + this.hover = false; + this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + this.dirtyLabel = true; + this.colorDirty = true; + + this.from = null; // a node + this.to = null; // a node + this.via = null; // a temp node + + this.fromBackup = null; // used to clean up after reconnect (used for manipulation) + this.toBackup = null; // used to clean up after reconnect (used for manipulation) + + // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster + // by storing the original information we can revert to the original connection when the cluser is opened. + this.fromArray = []; + this.toArray = []; + + this.connected = false; + + this.widthFixed = false; + this.lengthFixed = false; + + this.setProperties(properties); + + this.controlNodesEnabled = false; + this.controlNodes = { from: null, to: null, positions: {} }; + this.connectedNode = null; + } + + /** + * Set or overwrite properties for the edge + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ + Edge.prototype.setProperties = function (properties) { + this.colorDirty = true; + if (!properties) { + return; } + this.properties = properties; - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; + var fields = ["style", "fontSize", "fontFace", "fontColor", "fontFill", "fontStrokeWidth", "fontStrokeColor", "width", "widthSelectionMultiplier", "hoverWidth", "arrowScaleFactor", "dash", "inheritColor", "labelAlignment", "opacity", "customScalingFunction", "useGradients", "value"]; + util.selectiveDeepExtend(fields, this.options, properties); - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; + if (properties.from !== undefined) { + this.fromId = properties.from; + } + if (properties.to !== undefined) { + this.toId = properties.to; } - // 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; + if (properties.id !== undefined) { + this.id = properties.id; + } + if (properties.label !== undefined) { + this.label = properties.label;this.dirtyLabel = true; + } + + if (properties.title !== undefined) { + this.title = properties.title; + } + if (properties.value !== undefined) { + this.value = properties.value; + } + if (properties.length !== undefined) { + this.physics.springLength = properties.length; + } + + if (properties.color !== undefined) { + this.options.inheritColor = false; + if (util.isString(properties.color)) { + this.options.color.color = properties.color; + this.options.color.highlight = properties.color; + } else { + if (properties.color.color !== undefined) { + this.options.color.color = properties.color.color; + } + if (properties.color.highlight !== undefined) { + this.options.color.highlight = properties.color.highlight; + } + if (properties.color.hover !== undefined) { + this.options.color.hover = properties.color.hover; + } } } - return this; + + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); + + this.widthFixed = this.widthFixed || properties.width !== undefined; + this.lengthFixed = this.lengthFixed || properties.length !== undefined; + + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + + // set draw method based on style + switch (this.options.style) { + case "line": + this.draw = this._drawLine;break; + case "arrow": + this.draw = this._drawArrow;break; + case "arrow-center": + this.draw = this._drawArrowCenter;break; + case "dash-line": + this.draw = this._drawDashLine;break; + default: + this.draw = this._drawLine;break; + } }; + /** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} + * Connect an edge to its nodes */ + Edge.prototype.connect = function () { + this.disconnect(); - Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; + this.from = this.body.nodes[this.fromId] || null; + this.to = this.body.nodes[this.toId] || null; + this.connected = this.from !== null && this.to !== null; - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); + 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); } } - - return this; }; /** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public + * Disconnect an edge from its nodes */ + Edge.prototype.disconnect = function () { + if (this.from) { + this.from.detachEdge(this); + this.from = null; + } + if (this.to) { + this.to.detachEdge(this); + this.to = null; + } - Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; + this.connected = false; }; /** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. */ + Edge.prototype.getTitle = function () { + return typeof this.title === "function" ? this.title() : this.title; + }; - Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; + + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + Edge.prototype.getValue = function () { + return this.value; }; + /** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + */ + Edge.prototype.setValueRange = function (min, max, total) { + if (!this.widthFixed && this.value !== undefined) { + var scale = this.options.customScalingFunction(min, max, total, this.value); + var widthDiff = this.options.widthMax - this.options.widthMin; + this.options.width = this.options.widthMin + scale * widthDiff; + this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; + } + }; -/***/ }, -/* 63 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + Edge.prototype.draw = function (ctx) { + throw "Method draw not initialized in edge"; + }; - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; /** - * Created by Alex on 11/6/2014. + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge */ + Edge.prototype.isOverlappingWith = function (obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - // 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(); + var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return dist < distMax; } else { - // Browser globals (root is window) - root.keycharm = factory(); + return false; + } + }; + + Edge.prototype._getColor = function (ctx) { + var colorObj = this.options.color; + if (this.options.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 (this.from.selected == false && this.to.selected == false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.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); + return grd; + } + + if (this.colorDirty === true) { + if (this.options.inheritColor == "to") { + colorObj = { + highlight: this.to.options.color.highlight.border, + hover: this.to.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) + }; + } else if (this.options.inheritColor == "from" || this.options.inheritColor == true) { + colorObj = { + highlight: this.from.options.color.highlight.border, + hover: this.from.options.color.hover.border, + color: util.overrideOpacity(this.from.options.color.border, this.options.opacity) + }; + } + this.options.color = colorObj; + this.colorDirty = false; } - }(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; + if (this.selected == true) { + return colorObj.highlight; + } else if (this.hover == true) { + return colorObj.hover; + } else { + return colorObj.color; + } + }; - // 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}; + /** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawLine = function (ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.lineWidth = this._getLineWidth(); + if (this.from != this.to) { + // draw line + var via = this._line(ctx); + // draw label + var point; + if (this.label) { + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { + point = this._pointOnLine(0.5); + } + this._label(ctx, this.label, point.x, point.y); + } + } else { + var x, y; + var radius = this.physics.springLength / 4; + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width / 2; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - node.height / 2; + } + this._circle(ctx, x, y, radius); + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); + } + }; - var down = function(event) {handleEvent(event,'keydown');}; - var up = function(event) {handleEvent(event,'keyup');}; + /** + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private + */ + Edge.prototype._getLineWidth = function () { + if (this.selected == true) { + return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3 * this.networkScaleInv); + } else { + if (this.hover == true) { + return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3 * this.networkScaleInv); + } else { + return Math.max(this.options.width, 0.3 * this.networkScaleInv); + } + } + }; - // 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); + Edge.prototype._getViaCoordinates = function () { + if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) { + return this.via; + } else if (this.options.smoothCurves.enabled == false) { + return { x: 0, y: 0 }; + } else { + var xVia = null; + var yVia = null; + var factor = this.options.smoothCurves.roundness; + var type = this.options.smoothCurves.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 (bound[i].shift == true && event.shiftKey == true) { - bound[i].fn(event); + } 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; } - else if (bound[i].shift == false && event.shiftKey == false) { - bound[i].fn(event); + } + 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 (preventDefault == true) { - event.preventDefault(); + if (type == "discrete") { + yVia = dy < factor * dx ? this.from.y : yVia; } } - }; - - // bind a key to a callback - _exportFunctions.bind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; + } 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; } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); + } 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; } - if (_bound[type][_keys[key].code] === undefined) { - _bound[type][_keys[key].code] = []; + 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; } - _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); - }; + } else if (type == "curvedCW") { + var dx = this.to.x - this.from.x; + var 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); - // 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); - } - } - }; + 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") { + var dx = this.to.x - this.from.x; + var dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - // 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; + 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 (event.keyCode == _keys[key].code && key == 'shift') { - return key; + } 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; } } - } - 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]); - } + } 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; } } - _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 { x: xVia, y: yVia }; } + }; - return keycharm; - })); - - - - -/***/ }, -/* 64 */ -/***/ 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 - - (function (undefined) { - /************************************ - Constants - ************************************/ - - 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, - - YEAR = 0, - MONTH = 1, - DATE = 2, - HOUR = 3, - MINUTE = 4, - SECOND = 5, - MILLISECOND = 6, - - // internal storage for locale config files - locales = {}, - - // extra moment internal properties (plugins register props here) - momentProperties = [], - - // check for nodeJS - hasModule = (typeof module !== 'undefined' && module && module.exports), - - // ASP.NET json date format regex - aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, - aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, - - // 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)$/, - - // 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, - - // 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 - - //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 + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._line = function (ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + if (this.options.smoothCurves.enabled == true) { + if (this.options.smoothCurves.dynamic == false) { + var via = this._getViaCoordinates(); + if (via.x == null) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; + } else { + // this.via.x = via.x; + // this.via.y = via.y; + ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); + ctx.stroke(); + //ctx.circle(via.x,via.y,2) + //ctx.stroke(); + return via; + } + } else { + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); + ctx.stroke(); + return this.via; + } + } else { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return null; + } + }; - // 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)?)?$/, + /** + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private + */ + Edge.prototype._circle = function (ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); + }; - isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + /** + * Draw label with white background and with the middle at (x, y) + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} x + * @param {Number} y + * @private + */ + Edge.prototype._label = function (ctx, text, x, y) { + if (text) { + ctx.font = (this.from.selected || this.to.selected ? "bold " : "") + this.options.fontSize + "px " + this.options.fontFace; + var yLine; - isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ], + if (this.dirtyLabel == true) { + var lines = String(text).split("\n"); + var lineCount = lines.length; + var fontSize = Number(this.options.fontSize); + yLine = y + (1 - lineCount) / 2 * fontSize; - // 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/] - ], + var 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; + } + var height = this.options.fontSize * lineCount; + var left = x - width / 2; + var top = y - height / 2; - // timezone chunker '+10:00' > ['10', '00'] or '-1530' > ['-', '15', '30'] - parseTimezoneChunker = /([\+\-]|\d\d)/gi, + // cache + this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; + } - // 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 yLine = this.labelDimensions.yLine; - 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' - }, + ctx.save(); - camelFunctions = { - dayofyear : 'dayOfYear', - isoweekday : 'isoWeekday', - isoweek : 'isoWeek', - weekyear : 'weekYear', - isoweekyear : 'isoWeekYear' - }, + if (this.options.labelAlignment != "horizontal") { + ctx.translate(x, yLine); + this._rotateForLabelAlignment(ctx); + x = 0; + yLine = 0; + } - // format function strings - formatFunctions = {}, - // 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 - }, + this._drawLabelRect(ctx); + this._drawLabelText(ctx, x, yLine, lines, lineCount, fontSize); - // tokens to ordinalize and pad - ordinalizeTokens = 'DDD w W M D d'.split(' '), - paddedTokens = 'M D H h m s w W'.split(' '), + ctx.restore(); + } + }; - 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(); - } - }, + /** + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._rotateForLabelAlignment = function (ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); - deprecations = {}, + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; + } - lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'], + ctx.rotate(angleInDegrees); + }; - updateInProgress = false; + /** + * Draws the label rectangle + * @param {CanvasRenderingContext2D} ctx + * @param {String} labelAlignment + * @private + */ + Edge.prototype._drawLabelRect = function (ctx) { + if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { + ctx.fillStyle = this.options.fontFill; - // 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 lineMargin = 2; - function hasOwnProp(a, b) { - return hasOwnProperty.call(a, b); + if (this.options.labelAlignment == "line-center") { + ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height); + } else if (this.options.labelAlignment == "line-above") { + ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height); + } else if (this.options.labelAlignment == "line-below") { + ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height); + } else { + ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height); } + } + }; - 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 - }; - } + /** + * Draws the label text + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} yLine + * @param {Array} lines + * @param {Number} lineCount + * @param {Number} fontSize + * @private + */ + Edge.prototype._drawLabelText = function (ctx, x, yLine, lines, lineCount, fontSize) { + // draw text + ctx.fillStyle = this.options.fontColor || "black"; + ctx.textAlign = "center"; - function printMsg(msg) { - if (moment.suppressDeprecationWarnings === false && - typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); - } + // check for label alignment + if (this.options.labelAlignment != "horizontal") { + var lineMargin = 2; + if (this.options.labelAlignment == "line-above") { + ctx.textBaseline = "alphabetic"; + yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers + } else if (this.options.labelAlignment == "line-below") { + 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 = "middle"; + } - function deprecate(msg, fn) { - var firstTime = true; - return extend(function () { - if (firstTime) { - printMsg(msg); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); + // check for strokeWidth + if (this.options.fontStrokeWidth > 0) { + ctx.lineWidth = this.options.fontStrokeWidth; + ctx.strokeStyle = this.options.fontStrokeColor; + ctx.lineJoin = "round"; + } + for (var i = 0; i < lineCount; i++) { + if (this.options.fontStrokeWidth > 0) { + ctx.strokeText(lines[i], x, yLine); } + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } + }; - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - printMsg(msg); - deprecations[name] = true; - } - } + /** + * Redraw a edge as a dashed line + * Draw this edge in the given canvas + * @author David Jordan + * @date 2012-08-08 + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawDashLine = function (ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.lineWidth = this._getLineWidth(); - 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 via = null; + // 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.dash.length !== undefined && this.options.dash.gap !== undefined) { + pattern = [this.options.dash.length, this.options.dash.gap]; + } else { + pattern = [5, 5]; } - 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; + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; - 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); - } + // draw the line + via = this._line(ctx); - return -(wholeMonthDiff + adjust); - } + // restore the dash settings. + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + ctx.restore(); + } else { + // unsupporting smooth lines + // draw dashed line + ctx.beginPath(); + ctx.lineCap = "round"; + if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap, this.options.dash.altLength, this.options.dash.gap]); + } else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dash.length, this.options.dash.gap]); + } else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + } - 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); + // draw label + if (this.label) { + var point; + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { + point = this._pointOnLine(0.5); } - formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); + this._label(ctx, this.label, point.x, point.y); + } + }; + + /** + * Get a point on a line + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ + Edge.prototype._pointOnLine = function (percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + }; + }; + /** + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ + Edge.prototype._pointOnCircle = function (x, y, radius, percentage) { + var angle = (percentage - 3 / 8) * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; + }; - function meridiemFixWrap(locale, hour, meridiem) { - var isPm; + /** + * Redraw a edge as a line with an arrow halfway the line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawArrowCenter = function (ctx) { + var point; + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this._getLineWidth(); - 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; - } + if (this.from != this.to) { + // draw line + var via = this._line(ctx); + + var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + // draw an arrow halfway the line + if (this.options.smoothCurves.enabled == true && via != null) { + var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x)); + var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y)); + point = { x: midpointX, y: midpointY }; + } else { + point = this._pointOnLine(0.5); } - /************************************ - Constructors - ************************************/ + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); - function Locale() { + // draw label + if (this.label) { + this._label(ctx, this.label, point.x, point.y); + } + } else { + // draw circle + var x, y; + var radius = 0.25 * Math.max(100, this.physics.springLength); + var node = this.from; + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width * 0.5; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - node.height * 0.5; } + this._circle(ctx, x, y, radius); - // 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; - } + // draw all arrows + var angle = 0.2 * Math.PI; + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + point = this._pointOnCircle(x, y, radius, 0.5); + ctx.arrow(point.x, point.y, angle, length); + ctx.fill(); + ctx.stroke(); + + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); } + } + }; - // 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; + Edge.prototype._pointOnBezier = function (t) { + var via = this._getViaCoordinates(); - // 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 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; - this._data = {}; + return { x: x, y: y }; + }; - this._locale = moment.localeData(); + /** + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. + * + * @param from + * @param ctx + * @returns {*} + * @private + */ + Edge.prototype._findBorderPosition = function (from, ctx) { + var maxIterations = 10; + var iteration = 0; + var low = 0; + var high = 1; + var pos, angle, distanceToBorder, distanceToNodes, difference; + var threshold = 0.2; + var node = this.to; + if (from == true) { + node = this.from; + } - this._bubble(); + while (low <= high && iteration < maxIterations) { + var middle = (low + high) * 0.5; + + pos = this._pointOnBezier(middle); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToNodes = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToNodes; + 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; + } } - /************************************ - Helpers - ************************************/ + iteration++; + } + pos.t = middle; + return pos; + }; - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } - } + /** + * Redraw a edge as a line with an arrow + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + Edge.prototype._drawArrow = function (ctx) { + // set style + ctx.strokeStyle = this._getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this._getLineWidth(); - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } + // set vars + var angle, length, arrowPos; - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; - } + // if not connected to itself + if (this.from != this.to) { + // draw line + this._line(ctx); - return a; + // draw arrow head + if (this.options.smoothCurves.enabled == true) { + var via = this._getViaCoordinates(); + arrowPos = this._findBorderPosition(false, ctx); + var guidePos = this._pointOnBezier(Math.max(0, arrowPos.t - 0.1)); + angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + } else { + angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var dx = this.to.x - this.from.x; + var dy = this.to.y - this.from.y; + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = this.to.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + + arrowPos = {}; + arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; + arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; } - function copyConfig(to, from) { - var i, prop, val; + // draw arrow at the end of the line + length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + ctx.arrow(arrowPos.x, arrowPos.y, angle, length); + ctx.fill(); + ctx.stroke(); - 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; - } + // draw label + if (this.label) { + var point; + if (this.options.smoothCurves.enabled == true && via != null) { + point = this._pointOnBezier(0.5); + } else { + point = this._pointOnLine(0.5); + } + this._label(ctx, this.label, point.x, point.y); + } + } else { + // draw circle + var node = this.from; + var x, y, arrow; + var radius = 0.25 * Math.max(100, this.physics.springLength); + if (!node.width) { + node.resize(ctx); + } + if (node.width > node.height) { + x = node.x + node.width * 0.5; + y = node.y - radius; + arrow = { + x: x, + y: node.y, + angle: 0.9 * Math.PI + }; + } else { + x = node.x + radius; + y = node.y - node.height * 0.5; + arrow = { + x: node.x, + y: y, + angle: 0.6 * Math.PI + }; + } + ctx.beginPath(); + // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } - } + // draw all arrows + var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor; + ctx.arrow(arrow.x, arrow.y, arrow.angle, length); + ctx.fill(); + ctx.stroke(); - return to; + // draw label + if (this.label) { + point = this._pointOnCircle(x, y, radius, 0.5); + this._label(ctx, this.label, point.x, point.y); } + } + }; - function absRound(number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); + /** + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + * @private + */ + Edge.prototype._getDistanceToEdge = function (x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + var returnValue = 0; + if (this.from != this.to) { + if (this.options.smoothCurves.enabled == true) { + var xVia, yVia; + if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { + xVia = this.via.x; + yVia = this.via.y; + } else { + var via = this._getViaCoordinates(); + xVia = via.x; + yVia = via.y; + } + var minDistance = 1000000000; + var distance; + var i, t, x, y, lastX, lastY; + for (i = 0; i < 10; i++) { + t = 0.1 * i; + x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; + y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; + if (i > 0) { + distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); + minDistance = distance < minDistance ? distance : minDistance; } + lastX = x;lastY = y; + } + returnValue = minDistance; + } else { + returnValue = this._getDistanceToLine(x1, y1, x2, y2, x3, y3); + } + } else { + var x, y, dx, dy; + var radius = 0.25 * this.physics.springLength; + var node = this.from; + if (node.width > node.height) { + x = node.x + 0.5 * node.width; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - 0.5 * node.height; } + dx = x - x3; + dy = y - y3; + returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); + } - // 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; + if (this.labelDimensions.left < x3 && this.labelDimensions.left + this.labelDimensions.width > x3 && this.labelDimensions.top < y3 && this.labelDimensions.top + this.labelDimensions.height > y3) { + return 0; + } else { + return returnValue; + } + }; - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } + Edge.prototype._getDistanceToLine = function (x1, y1, x2, y2, x3, y3) { + var px = x2 - x1, + py = y2 - y1, + something = px * px + py * py, + u = ((x3 - x1) * px + (y3 - y1) * py) / something; - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; + if (u > 1) { + u = 1; + } else if (u < 0) { + u = 0; + } - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; - } + var x = x1 + u * px, + y = y1 + u * py, + dx = x - x3, + dy = y - y3; - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); + //# 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 res; - } + return Math.sqrt(dx * dx + dy * dy); + }; - 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; - } + /** + * This allows the zoom level of the network to influence the rendering + * + * @param scale + */ + Edge.prototype.setScale = function (scale) { + this.networkScaleInv = 1 / scale; + }; - return res; - } - // 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; - } + Edge.prototype.select = function () { + this.selected = true; + }; - val = typeof val === 'string' ? +val : val; - dur = moment.duration(val, period); - addOrSubtractDurationFromMoment(this, dur, direction); - return this; - }; - } + Edge.prototype.unselect = function () { + this.selected = false; + }; - function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; + Edge.prototype.positionBezierNode = function () { + if (this.via !== null && this.from !== null && this.to !== null) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); + } else if (this.via !== null) { + this.via.x = 0; + this.via.y = 0; + } + }; - 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); - } + /** + * This function draws the control nodes for the manipulator. + * In order to enable this, only set the this.controlNodesEnabled to true. + * @param ctx + */ + Edge.prototype._drawControlNodes = function (ctx) { + if (this.controlNodesEnabled == true) { + if (this.controlNodes.from === null && this.controlNodes.to === null) { + var nodeIdFrom = "edgeIdFrom:".concat(this.id); + var nodeIdTo = "edgeIdTo:".concat(this.id); + var constants = { + nodes: { group: "", radius: 7, borderWidth: 2, borderWidthSelected: 2 }, + physics: { damping: 0 }, + clustering: { maxNodeSizeIncrements: 0, nodeScaling: { width: 0, height: 0, radius: 0 } } + }; + this.controlNodes.from = new Node({ id: nodeIdFrom, + shape: "dot", + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } + }, {}, {}, constants); + this.controlNodes.to = new Node({ id: nodeIdTo, + shape: "dot", + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } } + }, {}, {}, constants); } - // check if is an array - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; + this.controlNodes.positions = {}; + if (this.controlNodes.from.selected == false) { + this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx); + this.controlNodes.from.x = this.controlNodes.positions.from.x; + this.controlNodes.from.y = this.controlNodes.positions.from.y; + } + if (this.controlNodes.to.selected == false) { + this.controlNodes.positions.to = this.getControlNodeToPosition(ctx); + this.controlNodes.to.x = this.controlNodes.positions.to.x; + this.controlNodes.to.y = this.controlNodes.positions.to.y; } - function isDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || - input instanceof Date; - } + this.controlNodes.from.draw(ctx); + this.controlNodes.to.draw(ctx); + } else { + this.controlNodes = { from: null, to: null, positions: {} }; + } + }; + + /** + * Enable control nodes. + * @private + */ + Edge.prototype._enableControlNodes = function () { + this.fromBackup = this.from; + this.toBackup = this.to; + this.controlNodesEnabled = true; + }; + + /** + * disable control nodes and remove from dynamicEdges from old node + * @private + */ + Edge.prototype._disableControlNodes = function () { + this.fromId = this.from.id; + this.toId = this.to.id; + if (this.fromId != this.fromBackup.id) { + // from was changed, remove edge from old 'from' node dynamic edges + this.fromBackup.detachEdge(this); + } else if (this.toId != this.toBackup.id) { + // to was changed, remove edge from old 'to' node dynamic edges + this.toBackup.detachEdge(this); + } - // 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.fromBackup = null; + this.toBackup = null; + this.controlNodesEnabled = false; + }; - function normalizeUnits(units) { - if (units) { - var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); - units = unitAliases[units] || camelFunctions[lowered] || lowered; - } - return units; - } - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; + /** + * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. + * @param x + * @param y + * @returns {null} + * @private + */ + Edge.prototype._getSelectedControlNode = function (x, y) { + var positions = this.controlNodes.positions; + var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2)); + var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2)); - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } + if (fromDistance < 15) { + this.connectedNode = this.from; + this.from = this.controlNodes.from; + return this.controlNodes.from; + } else if (toDistance < 15) { + this.connectedNode = this.to; + this.to = this.controlNodes.to; + return this.controlNodes.to; + } else { + return null; + } + }; - return normalizedInput; - } - function makeList(field) { - var count, setter; + /** + * this resets the control nodes to their original position. + * @private + */ + Edge.prototype._restoreControlNodes = function () { + if (this.controlNodes.from.selected == true) { + this.from = this.connectedNode; + this.connectedNode = null; + this.controlNodes.from.unselect(); + } else if (this.controlNodes.to.selected == true) { + this.to = this.connectedNode; + this.connectedNode = null; + this.controlNodes.to.unselect(); + } + }; - if (field.indexOf('week') === 0) { - count = 7; - setter = 'day'; - } - else if (field.indexOf('month') === 0) { - count = 12; - setter = 'month'; - } - else { - return; - } + /** + * this calculates the position of the control nodes on the edges of the parent nodes. + * + * @param ctx + * @returns {x: *, y: *} + */ + Edge.prototype.getControlNodeFromPosition = function (ctx) { + // draw arrow head + var controlnodeFromPos; + if (this.options.smoothCurves.enabled == true) { + controlnodeFromPos = this._findBorderPosition(true, ctx); + } else { + var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var dx = this.to.x - this.from.x; + var dy = this.to.y - this.from.y; + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - moment[field] = function (format, index) { - var i, getter, - method = moment._locale[field], - results = []; + var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI); + var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength; + controlnodeFromPos = {}; + controlnodeFromPos.x = fromBorderPoint * this.from.x + (1 - fromBorderPoint) * this.to.x; + controlnodeFromPos.y = fromBorderPoint * this.from.y + (1 - fromBorderPoint) * this.to.y; + } - if (typeof format === 'number') { - index = format; - format = undefined; - } + return controlnodeFromPos; + }; - getter = function (i) { - var m = moment().utc().set(setter, i); - return method.call(moment._locale, m, format || ''); - }; + /** + * this calculates the position of the control nodes on the edges of the parent nodes. + * + * @param ctx + * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}} + */ + Edge.prototype.getControlNodeToPosition = function (ctx) { + // draw arrow head + var controlnodeFromPos, controlnodeToPos; + if (this.options.smoothCurves.enabled == true) { + controlnodeToPos = this._findBorderPosition(false, ctx); + } else { + var angle = Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x); + var dx = this.to.x - this.from.x; + var dy = this.to.y - this.from.y; + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = this.to.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - if (index != null) { - return getter(index); - } - else { - for (i = 0; i < count; i++) { - results.push(getter(i)); - } - return results; - } - }; - } + controlnodeToPos = {}; + controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x; + controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y; + } - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; + return controlnodeToPos; + }; - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } + module.exports = Edge; - return value; - } +/***/ }, +/* 55 */ +/***/ function(module, exports, __webpack_require__) { - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } + "use strict"; - function weeksInYear(year, dow, doy) { - return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; - } + var util = __webpack_require__(1); - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } + /** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} properties An object containing properties for the node. All + * properties are optional, except for the id. + * {number} id Id of the node. Required + * {string} label Text label for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} shape Node shape, available: + * "database", "circle", "ellipse", + * "box", "image", "text", "dot", + * "star", "triangle", "triangleDown", + * "square", "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 properties + * @param {Object} constants An object with default values for + * example for the color + * + */ + function Node(properties, imagelist, grouplist, networkConstants) { + var constants = util.selectiveBridgeObject(["nodes"], networkConstants); + this.options = constants.nodes; - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - } + this.selected = false; + this.hover = false; - 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; + this.edges = []; // all edges connected to this node - if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } + // set defaults for the properties + this.id = undefined; + this.allowedToMoveX = false; + this.allowedToMoveY = false; + this.xFixed = false; + this.yFixed = false; + this.horizontalAlignLeft = true; // these are for the navigation controls + this.verticalAlignTop = true; // these are for the navigation controls + this.baseRadiusValue = networkConstants.nodes.radius; + this.radiusFixed = false; + this.level = -1; + this.preassignedLevel = false; + this.hierarchyEnumerated = false; + this.labelDimensions = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; - m._pf.overflow = overflow; - } - } + this.imagelist = imagelist; + this.grouplist = grouplist; - 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; + // physics properties + this.x = null; + this.y = null; + this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate - if (m._strict) { - m._isValid = m._isValid && - m._pf.charsLeftOver === 0 && - m._pf.unusedTokens.length === 0 && - m._pf.bigHour === undefined; - } - } - return m._isValid; - } + // used for reverting to previous position on stabilization + this.previousState = { vx: 0, vy: 0, x: 0, y: 0 }; - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; - } + this.fixedData = { x: null, y: null }; - // 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; + this.setProperties(properties, constants); - 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; - } + // variables to tell the node about the network. + this.networkScaleInv = 1; + this.networkScale = 1; + this.canvasTopLeft = { x: -300, y: -300 }; + this.canvasBottomRight = { x: 300, y: 300 }; + this.parentEdgeId = null; + } - 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 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(); - } - } + /** + * Attach a edge to the node + * @param {Edge} edge + */ + Node.prototype.attachEdge = function (edge) { + if (this.edges.indexOf(edge) == -1) { + this.edges.push(edge); + } + }; - /************************************ - Locale - ************************************/ + /** + * Detach a edge from the node + * @param {Edge} edge + */ + Node.prototype.detachEdge = function (edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + }; - extend(Locale.prototype, { + /** + * Set or overwrite properties for the node + * @param {Object} properties an object with properties + * @param {Object} constants and object with default, global properties + */ + Node.prototype.setProperties = function (properties, constants) { + if (!properties) { + return; + } + this.properties = properties; - 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 fields = ["borderWidth", "borderWidthSelected", "shape", "image", "brokenImage", "radius", "fontColor", "fontSize", "fontFace", "fontFill", "fontStrokeWidth", "fontStrokeColor", "group", "mass", "fontDrawThreshold", "scaleFontWithValue", "fontSizeMaxVisible", "customScalingFunction", "iconFontFace", "icon", "iconColor", "iconSize", "value"]; + util.selectiveDeepExtend(fields, this.options, properties); - _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), - months : function (m) { - return this._months[m.month()]; - }, + // basic properties + if (properties.id !== undefined) { + this.id = properties.id; + } + if (properties.label !== undefined) { + this.label = properties.label;this.originalLabel = properties.label; + } + if (properties.title !== undefined) { + this.title = properties.title; + } + if (properties.x !== undefined) { + this.x = properties.x;this.predefinedPosition = true; + } + if (properties.y !== undefined) { + this.y = properties.y;this.predefinedPosition = true; + } + if (properties.value !== undefined) { + this.value = properties.value; + } + if (properties.level !== undefined) { + this.level = properties.level;this.preassignedLevel = true; + } - _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), - monthsShort : function (m) { - return this._monthsShort[m.month()]; - }, + // navigation controls properties + if (properties.horizontalAlignLeft !== undefined) { + this.horizontalAlignLeft = properties.horizontalAlignLeft; + } + if (properties.verticalAlignTop !== undefined) { + this.verticalAlignTop = properties.verticalAlignTop; + } + if (properties.triggerFunction !== undefined) { + this.triggerFunction = properties.triggerFunction; + } - monthsParse : function (monthName, format, strict) { - var i, mom, regex; + if (this.id === undefined) { + throw "Node must have an id"; + } - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } + // copy group properties + if (typeof properties.group === "number" || typeof properties.group === "string" && properties.group != "") { + var groupObj = this.grouplist.get(properties.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 properties + if (properties.radius !== undefined) { + this.baseRadiusValue = this.options.radius; + } + if (properties.color !== undefined) { + this.options.color = util.parseColor(properties.color); + } - 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; - } - } - }, + 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"; + } + } - _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), - weekdays : function (m) { - return this._weekdays[m.day()]; - }, + if (properties.allowedToMoveX !== undefined) { + this.xFixed = !properties.allowedToMoveX; + this.allowedToMoveX = properties.allowedToMoveX; + } else if (properties.x !== undefined && this.allowedToMoveX == false) { + this.xFixed = true; + } - _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), - weekdaysShort : function (m) { - return this._weekdaysShort[m.day()]; - }, - _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), - weekdaysMin : function (m) { - return this._weekdaysMin[m.day()]; - }, + if (properties.allowedToMoveY !== undefined) { + this.yFixed = !properties.allowedToMoveY; + this.allowedToMoveY = properties.allowedToMoveY; + } else if (properties.y !== undefined && this.allowedToMoveY == false) { + this.yFixed = true; + } - weekdaysParse : function (weekdayName) { - var i, mom, regex; + this.radiusFixed = this.radiusFixed || properties.radius !== undefined; - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } + if (this.options.shape === "image" || this.options.shape === "circularImage") { + this.options.radiusMin = constants.nodes.widthMin; + this.options.radiusMax = constants.nodes.widthMax; + } - 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; - } - } - }, + // choose draw method depending on the shape + switch (this.options.shape) { + case "database": + this.draw = this._drawDatabase;this.resize = this._resizeDatabase;break; + case "box": + this.draw = this._drawBox;this.resize = this._resizeBox;break; + case "circle": + this.draw = this._drawCircle;this.resize = this._resizeCircle;break; + case "ellipse": + this.draw = this._drawEllipse;this.resize = this._resizeEllipse;break; + // TODO: add diamond shape + case "image": + this.draw = this._drawImage;this.resize = this._resizeImage;break; + case "circularImage": + this.draw = this._drawCircularImage;this.resize = this._resizeCircularImage;break; + case "text": + this.draw = this._drawText;this.resize = this._resizeText;break; + case "dot": + this.draw = this._drawDot;this.resize = this._resizeShape;break; + case "square": + this.draw = this._drawSquare;this.resize = this._resizeShape;break; + case "triangle": + this.draw = this._drawTriangle;this.resize = this._resizeShape;break; + case "triangleDown": + this.draw = this._drawTriangleDown;this.resize = this._resizeShape;break; + case "star": + this.draw = this._drawStar;this.resize = this._resizeShape;break; + case "icon": + this.draw = this._drawIcon;this.resize = this._resizeIcon;break; + default: + this.draw = this._drawEllipse;this.resize = this._resizeEllipse;break; + } + // reset the size of the node, this can be changed + this._reset(); + }; - _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; - }, + /** + * select this node + */ + Node.prototype.select = function () { + this.selected = true; + this._reset(); + }; - 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'); - }, + /** + * unselect this node + */ + Node.prototype.unselect = function () { + this.selected = false; + this._reset(); + }; - _meridiemParse : /[ap]\.?m?\.?/i, - meridiem : function (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } - }, + /** + * Reset the calculated size of the node, forces it to recalculate its size + * @private + */ + Node.prototype._reset = function () { + this.width = undefined; + this.height = undefined; + }; - _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; - }, + /** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ + Node.prototype.getTitle = function () { + return typeof this.title === "function" ? this.title() : this.title; + }; - _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' - }, + /** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ + Node.prototype.distanceToBorder = function (ctx, angle) { + var borderWidth = 1; - 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); - }, + if (!this.width) { + this.resize(ctx); + } - pastFuture : function (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - }, + switch (this.options.shape) { + case "circle": + case "dot": + return this.options.radius + borderWidth; - ordinal : function (number) { - return this._ordinal.replace('%d', number); - }, - _ordinal : '%d', - _ordinalParse : /\d{1,2}/, + case "ellipse": + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); - preparse : function (string) { - return string; - }, + // TODO: implement distanceToBorder for database + // TODO: implement distanceToBorder for triangle + // TODO: implement distanceToBorder for triangleDown - postformat : function (string) { - return string; - }, + case "box": + case "image": + case "text": + default: + if (this.width) { + return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + // TODO: reckon with border radius too in case of box + } else { + return 0; + } - week : function (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - }, + } + // TODO: implement calculation of distance to border for all shapes + }; - _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. - }, - firstDayOfWeek : function () { - return this._week.dow; - }, + /** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ + Node.prototype.isFixed = function () { + return this.xFixed && this.yFixed; + }; - firstDayOfYear : function () { - return this._week.doy; - }, + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + Node.prototype.isSelected = function () { + return this.selected; + }; - _invalidDate: 'Invalid date', - invalidDate: function () { - return this._invalidDate; - } - }); + /** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ + Node.prototype.getValue = function () { + return this.value; + }; - /************************************ - Formatting - ************************************/ + /** + * Calculate the distance from the nodes location to the given location (x,y) + * @param {Number} x + * @param {Number} y + * @return {Number} value + */ + Node.prototype.getDistance = function (x, y) { + var dx = this.x - x, + dy = this.y - y; + return Math.sqrt(dx * dx + dy * dy); + }; - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); - } - return input.replace(/\\/g, ''); + /** + * Adjust the value range of the node. The node will adjust it's radius + * based on its value. + * @param {Number} min + * @param {Number} max + */ + Node.prototype.setValueRange = function (min, max, total) { + if (!this.radiusFixed && this.value !== undefined) { + var scale = this.options.customScalingFunction(min, max, total, this.value); + var radiusDiff = this.options.radiusMax - this.options.radiusMin; + if (this.options.scaleFontWithValue == true) { + var fontDiff = this.options.fontSizeMax - this.options.fontSizeMin; + this.options.fontSize = this.options.fontSizeMin + scale * fontDiff; } + this.options.radius = this.options.radiusMin + scale * radiusDiff; + } - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; - - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } - } + this.baseRadiusValue = this.options.radius; + }; - 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; - }; - } + /** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + Node.prototype.draw = function (ctx) { + throw "Draw method not initialized for node"; + }; - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } + /** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + Node.prototype.resize = function (ctx) { + throw "Resize method not initialized for node"; + }; - format = expandFormat(format, m.localeData()); + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ + Node.prototype.isOverlappingWith = function (obj) { + return this.left < obj.right && this.left + this.width > obj.left && this.top < obj.bottom && this.top + this.height > obj.top; + }; - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } + Node.prototype._resizeImage = function (ctx) { + // TODO: pre calculate the image size - return formatFunctions[format](m); + if (!this.width || !this.height) { + // undefined or 0 + var width, height; + if (this.value) { + this.options.radius = this.baseRadiusValue; + var scale = this.imageObj.height / this.imageObj.width; + if (scale !== undefined) { + width = this.options.radius || this.imageObj.width; + height = this.options.radius * scale || this.imageObj.height; + } else { + width = 0; + height = 0; + } + } else { + width = this.imageObj.width; + height = this.imageObj.height; } + this.width = width; + this.height = height; + } + }; - function expandFormat(format, locale) { - var i = 5; + Node.prototype._drawImageAtPosition = function (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 replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } + Node.prototype._drawImageLabel = function (ctx) { + var yLabel; + var offset = 0; - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } + if (this.height) { + offset = this.height / 2; + var labelDimensions = this.getTextSize(ctx); - return format; + if (labelDimensions.lineCount >= 1) { + offset += labelDimensions.height / 2; + offset += 3; } + } + yLabel = this.y + offset; - /************************************ - Parsing - ************************************/ + this._label(ctx, this.label, this.x, yLabel, undefined); + }; + Node.prototype._drawImage = function (ctx) { + this._resizeImage(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - // 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._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); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + }; + + Node.prototype._resizeCircularImage = function (ctx) { + if (!this.imageObj.src || !this.imageObj.width || !this.imageObj.height) { + if (!this.width) { + var diameter = this.options.radius * 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); + } + }; - 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]); + Node.prototype._drawCircularImage = function (ctx) { + this._resizeCircularImage(ctx); - return parts[0] === '+' ? minutes : -minutes; - } + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - // function to convert string input to date - function addTimeToArrayFromToken(token, input, config) { - var a, datePartArray = config._a; + var centerX = this.left + this.width / 2; + var centerY = this.top + this.height / 2; + var radius = Math.abs(this.height / 2); - 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); - } + this._drawRawCircle(ctx, centerX, centerY, radius); - 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); - } - } + ctx.save(); + ctx.circle(this.x, this.y, radius); + ctx.stroke(); + ctx.clip(); - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; + this._drawImageAtPosition(ctx); - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; + ctx.restore(); - // 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; + this.boundingBox.top = this.y - this.options.radius; + this.boundingBox.left = this.x - this.options.radius; + this.boundingBox.right = this.x + this.options.radius; + this.boundingBox.bottom = this.y + this.options.radius; - weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); - week = dfl(w.w, 1); + this._drawImageLabel(ctx); - 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); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + }; - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } + Node.prototype._resizeBox = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } + }; - // 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; + Node.prototype._drawBox = function (ctx) { + this._resizeBox(ctx); - if (config._d) { - return; - } + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - currentDate = currentDateArray(config); + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.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); - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; - if (config._dayOfYear > daysInYear(yearToUse)) { - config._pf._overflowDayOfYear = true; - } + ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius); + ctx.fill(); + ctx.stroke(); - date = makeUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - // 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]; - } + this._label(ctx, this.label, this.x, this.y); + }; - // 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]; - } - // 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; - } + Node.prototype._resizeDatabase = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + } + }; - 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); - } + Node.prototype._drawDatabase = function (ctx) { + this._resizeDatabase(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - if (config._nextDay) { - config._a[HOUR] = 24; - } - } + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - function dateFromObject(config) { - var normalizedInput; + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.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); - if (config._d) { - return; - } + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + ctx.database(this.x - this.width / 2, this.y - this.height * 0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); - normalizedInput = normalizeObjectUnits(config._i); - config._a = [ - normalizedInput.year, - normalizedInput.month, - normalizedInput.day || normalizedInput.date, - normalizedInput.hour, - normalizedInput.minute, - normalizedInput.second, - normalizedInput.millisecond - ]; + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - dateFromConfig(config); - } + this._label(ctx, this.label, this.x, this.y); + }; - 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()]; - } - } - // date from string and format string - function makeDateFromStringAndFormat(config) { - if (config._f === moment.ISO_8601) { - parseISO(config); - return; - } + Node.prototype._resizeCircle = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.options.radius = diameter / 2; - config._a = []; - config._pf.empty = true; + this.width = diameter; + this.height = diameter; + } + }; - // 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; + Node.prototype._drawRawCircle = function (ctx, x, y, radius) { + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; - 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); - } - } + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - // add remaining unparsed input length to the string - config._pf.charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - config._pf.unusedInput.push(string); - } + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + ctx.circle(this.x, this.y, radius); + ctx.fill(); + ctx.stroke(); + }; - // 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); - } + Node.prototype._drawCircle = function (ctx) { + this._resizeCircle(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - function unescapeFormat(s) { - return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }); - } + this._drawRawCircle(ctx, this.x, this.y, this.options.radius); - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function regexpEscape(s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + this.boundingBox.top = this.y - this.options.radius; + this.boundingBox.left = this.x - this.options.radius; + this.boundingBox.right = this.x + this.options.radius; + this.boundingBox.bottom = this.y + this.options.radius; + + this._label(ctx, this.label, this.x, this.y); + }; + + Node.prototype._resizeEllipse = function (ctx) { + if (this.width === undefined) { + var textSize = this.getTextSize(ctx); + + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; } + var defaultSize = this.width; + } + }; - // date from string and array of format strings - function makeDateFromStringAndArray(config) { - var tempConfig, - bestMoment, + Node.prototype._drawEllipse = function (ctx) { + this._resizeEllipse(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - scoreToBeat, - i, - currentScore; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border; + + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + + ctx.ellipse(this.left, this.top, this.width, this.height); + 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._label(ctx, this.label, this.x, this.y); + }; + + Node.prototype._drawDot = function (ctx) { + this._drawShape(ctx, "circle"); + }; - if (config._f.length === 0) { - config._pf.invalidFormat = true; - config._d = new Date(NaN); - return; - } + Node.prototype._drawTriangle = function (ctx) { + this._drawShape(ctx, "triangle"); + }; - 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); + Node.prototype._drawTriangleDown = function (ctx) { + this._drawShape(ctx, "triangleDown"); + }; - if (!isValid(tempConfig)) { - continue; - } + Node.prototype._drawSquare = function (ctx) { + this._drawShape(ctx, "square"); + }; - // if there is any input that was not parsed add a penalty for that format - currentScore += tempConfig._pf.charsLeftOver; + Node.prototype._drawStar = function (ctx) { + this._drawShape(ctx, "star"); + }; - //or tokens - currentScore += tempConfig._pf.unusedTokens.length * 10; + Node.prototype._resizeShape = function (ctx) { + if (!this.width) { + this.options.radius = this.baseRadiusValue; + var size = 2 * this.options.radius; + this.width = size; + this.height = size; + } + }; - tempConfig._pf.score = currentScore; + Node.prototype._drawShape = function (ctx, shape) { + this._resizeShape(ctx); - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } - } + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - extend(config, bestMoment || tempConfig); - } + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + var radiusMultiplier = 2; - // date from iso format - function parseISO(config) { - var i, l, - string = config._i, - match = isoRegex.exec(string); + // choose draw method depending on the shape + switch (shape) { + case "dot": + radiusMultiplier = 2;break; + case "square": + radiusMultiplier = 2;break; + case "triangle": + radiusMultiplier = 3;break; + case "triangleDown": + radiusMultiplier = 3;break; + case "star": + radiusMultiplier = 4;break; + } - 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; - } - } + ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.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); - // date from iso format or fallback - function makeDateFromString(config) { - parseISO(config); - if (config._isValid === false) { - delete config._isValid; - moment.createFromInputFallback(config); - } - } + ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background; + ctx[shape](this.x, this.y, this.options.radius); + ctx.fill(); + ctx.stroke(); - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } + this.boundingBox.top = this.y - this.options.radius; + this.boundingBox.left = this.x - this.options.radius; + this.boundingBox.right = this.x + this.options.radius; + this.boundingBox.bottom = this.y + this.options.radius; - 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 (this.label) { + this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, "hanging", true); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + } + }; - 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); + Node.prototype._resizeText = function (ctx) { + if (!this.width) { + var margin = 5; + var textSize = this.getTextSize(ctx); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } + }; - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } + Node.prototype._drawText = function (ctx) { + this._resizeText(ctx); + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; - function makeUTCDate(y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } + this._label(ctx, this.label, this.x, this.y); - 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; - } + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; + }; - /************************************ - Relative Time - ************************************/ + Node.prototype._resizeIcon = function (ctx) { + if (!this.width) { + var margin = 5; + var iconSize = { + width: Number(this.options.iconSize), + height: Number(this.options.iconSize) + }; + this.width = iconSize.width + 2 * margin; + this.height = iconSize.height + 2 * margin; + } + }; + Node.prototype._drawIcon = function (ctx) { + this._resizeIcon(ctx); - // 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); - } + this.options.iconSize = this.options.iconSize || 50; - 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')), + this.left = this.x - this.width / 2; + this.top = this.y - this.height / 2; + this._icon(ctx); - 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); - } + this.boundingBox.top = this.y - this.options.iconSize / 2; + this.boundingBox.left = this.x - this.options.iconSize / 2; + this.boundingBox.right = this.x + this.options.iconSize / 2; + this.boundingBox.bottom = this.y + this.options.iconSize / 2; + if (this.label) { + var iconTextSpacing = 5; + this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, "top", true); - /************************************ - Week of Year - ************************************/ + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height); + } + }; + Node.prototype._icon = function (ctx) { + var relativeIconSize = Number(this.options.iconSize) * this.networkScale; - // 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; + if (this.options.icon && relativeIconSize > this.options.fontDrawThreshold - 1) { + var iconSize = Number(this.options.iconSize); + ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace; - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; - } + // draw icon + ctx.fillStyle = this.options.iconColor || "black"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.options.icon, this.x, this.y); + } + }; - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; - } + Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) { + var relativeFontSize = Number(this.options.fontSize) * this.networkScale; + if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) { + var fontSize = Number(this.options.fontSize); - adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; + // 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 (relativeFontSize >= this.options.fontSizeMaxVisible) { + fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; } - //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; + // fade in when relative scale is between threshold and threshold - 1 + var fontColor = this.options.fontColor || "#000000"; + var strokecolor = this.options.fontStrokeColor; + if (relativeFontSize <= this.options.fontDrawThreshold) { + var opacity = Math.max(0, Math.min(1, 1 - (this.options.fontDrawThreshold - relativeFontSize))); + fontColor = util.overrideOpacity(fontColor, opacity); + strokecolor = util.overrideOpacity(strokecolor, opacity); + } - 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; + ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; - return { - year: dayOfYear > 0 ? year : year - 1, - dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; + var lines = text.split("\n"); + var lineCount = lines.length; + var yLine = y + (1 - lineCount) / 2 * fontSize; + if (labelUnderNode == true) { + yLine = y + (1 - lineCount) / (2 * fontSize); } - /************************************ - Top Level Functions - ************************************/ - - function makeMoment(config) { - var input = config._i, - format = config._f, - res; + // font fill from edges now for nodes! + var 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; + } + var height = fontSize * lineCount; + var left = x - width / 2; + var top = y - height / 2; + if (baseline == "hanging") { + top += 0.5 * fontSize; + top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers + yLine += 4; // distance from node + } + this.labelDimensions = { top: top, left: left, width: width, height: height, yLine: yLine }; - config._locale = config._locale || moment.localeData(config._l); + // create the fontfill background + if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") { + ctx.fillStyle = this.options.fontFill; + ctx.fillRect(left, top, width, height); + } - if (input === null || (format === undefined && input === '')) { - return moment.invalid({nullInput: true}); - } + // draw text + ctx.fillStyle = fontColor; + ctx.textAlign = align || "center"; + ctx.textBaseline = baseline || "middle"; + if (this.options.fontStrokeWidth > 0) { + ctx.lineWidth = this.options.fontStrokeWidth; + ctx.strokeStyle = strokecolor; + ctx.lineJoin = "round"; + } + for (var i = 0; i < lineCount; i++) { + if (this.options.fontStrokeWidth) { + ctx.strokeText(lines[i], x, yLine); + } + ctx.fillText(lines[i], x, yLine); + yLine += fontSize; + } + } + }; - 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); - } + Node.prototype.getTextSize = function (ctx) { + if (this.label !== undefined) { + var fontSize = Number(this.options.fontSize); + if (fontSize * this.networkScale > this.options.fontSizeMaxVisible) { + fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv; + } + ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace; - res = new Moment(config); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; - } + var lines = this.label.split("\n"), + height = (fontSize + 4) * lines.length, + width = 0; - return res; + for (var i = 0, iMax = lines.length; i < iMax; i++) { + width = Math.max(width, ctx.measureText(lines[i]).width); } - moment = function (input, format, locale, strict) { - var c; - - 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(); + return { width: width, height: height, lineCount: lines.length }; + } else { + return { width: 0, height: 0, lineCount: 0 }; + } + }; - return makeMoment(c); - }; - moment.suppressDeprecationWarnings = false; + /** + * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. + * there is a safety margin of 0.3 * width; + * + * @returns {boolean} + */ + Node.prototype.inArea = function () { + if (this.width !== undefined) { + return this.x + this.width * this.networkScaleInv >= this.canvasTopLeft.x && this.x - this.width * this.networkScaleInv < this.canvasBottomRight.x && this.y + this.height * this.networkScaleInv >= this.canvasTopLeft.y && this.y - this.height * this.networkScaleInv < this.canvasBottomRight.y; + } else { + return true; + } + }; - 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; - } + /** + * This allows the zoom level of the network to influence the rendering + * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas + * + * @param scale + * @param canvasTopLeft + * @param canvasBottomRight + */ + Node.prototype.setScaleAndPos = function (scale, canvasTopLeft, canvasBottomRight) { + this.networkScaleInv = 1 / scale; + this.networkScale = scale; + this.canvasTopLeft = canvasTopLeft; + this.canvasBottomRight = canvasBottomRight; + }; - moment.min = function () { - var args = [].slice.call(arguments, 0); - return pickBy('isBefore', args); - }; + /** + * This allows the zoom level of the network to influence the rendering + * + * @param scale + */ + Node.prototype.setScale = function (scale) { + this.networkScaleInv = 1 / scale; + this.networkScale = scale; + }; - moment.max = function () { - var args = [].slice.call(arguments, 0); - return pickBy('isAfter', args); - }; - // creating with utc - moment.utc = function (input, format, locale, strict) { - var c; + /** + * set the velocity at 0. Is called when this node is contained in another during clustering + */ + Node.prototype.clearVelocity = function () { + this.vx = 0; + this.vy = 0; + }; - 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 makeMoment(c).utc(); - }; + /** + * Basic preservation of (kinectic) energy + * + * @param massBeforeClustering + */ + Node.prototype.updateVelocity = function (massBeforeClustering) { + var energyBefore = this.vx * this.vx * massBeforeClustering; + //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); + this.vx = Math.sqrt(energyBefore / this.options.mass); + energyBefore = this.vy * this.vy * massBeforeClustering; + //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass); + this.vy = Math.sqrt(energyBefore / this.options.mass); + }; - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); - }; + module.exports = Node; - // duration - moment.duration = function (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - parseIso, - diffRes; +/***/ }, +/* 56 */ +/***/ function(module, exports, __webpack_require__) { - 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)); + "use strict"; - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } + var util = __webpack_require__(1); - ret = new Duration(duration); + /** + * @class Groups + * This class can store groups and properties specific for groups. + */ + function Groups() { + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; + this.useDefaultGroups = true; + } - if (moment.isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; - } - return ret; - }; + /** + * default constants for group colors + */ + Groups.DEFAULT = [{ 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 - // version number - moment.version = VERSION; + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - // default format - moment.defaultFormat = isoFormat; + { 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 - // constant that refers to the ISO standard - moment.ISO_8601 = function () {}; + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; - // Plugins that add properties should also add the key here (null value), - // so we can properly clone ourselves. - moment.momentProperties = momentProperties; - // This function will be called whenever a moment is mutated. - // It is intended to keep the offset in sync with the timezone. - moment.updateOffset = function () {}; + /** + * Clear all groups + */ + Groups.prototype.clear = function () { + this.groups = {}; + this.groups.length = function () { + var i = 0; + for (var p in this) { + if (this.hasOwnProperty(p)) { + i++; + } + } + return i; + }; + }; - // 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; - }; - moment.lang = deprecate( - 'moment.lang is deprecated. Use moment.locale instead.', - function (key, value) { - return moment.locale(key, value); - } - ); + /** + * get group properties of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group properties + */ + Groups.prototype.get = function (groupname) { + var group = this.groups[groupname]; + if (group == undefined) { + if (this.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } else { + // create new group + var index = this.defaultIndex % Groups.DEFAULT.length; + this.defaultIndex++; + group = {}; + group.color = Groups.DEFAULT[index]; + this.groups[groupname] = group; + } + } - // 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 group; + }; - if (data) { - moment.duration._locale = moment._locale = data; - } - } + /** + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object + */ + Groups.prototype.add = function (groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; + }; - return moment._locale._abbr; - }; + module.exports = Groups; + // 20:bright red - moment.defineLocale = function (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); +/***/ }, +/* 57 */ +/***/ function(module, exports, __webpack_require__) { - // backwards compat for now: also set the locale - moment.locale(name); + "use strict"; - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } - }; + /** + * @class Images + * This class loads images and keeps them stored. + */ + function Images() { + this.images = {}; + this.imageBroken = {}; + this.callback = undefined; + } - moment.langData = deprecate( - 'moment.langData is deprecated. Use moment.localeData instead.', - function (key) { - return moment.localeData(key); - } - ); + /** + * Set an onload callback function. This will be called each time an image + * is loaded + * @param {function} callback + */ + Images.prototype.setOnloadCallback = function (callback) { + this.callback = callback; + }; - // returns locale data - moment.localeData = function (key) { - var locale; + /** + * + * @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); + } - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; - } + if (me.callback) { + me.images[url] = img; + me.callback(this); + } + }; - if (!key) { - return moment._locale; + img.onerror = function () { + if (brokenUrl === undefined) { + console.error("Could not load image:", url); + delete this.src; + if (me.callback) { + me.callback(this); } - - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; + } else { + if (me.imageBroken[url] === true) { + if (this.src == brokenUrl) { + console.error("Could not load brokenImage:", brokenUrl); + delete this.src; + if (me.callback) { + me.callback(this); } - key = [key]; + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + } + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + me.imageBroken[url] = true; } - - return chooseLocale(key); + } }; - // compare moment object - moment.isMoment = function (obj) { - return obj instanceof Moment || - (obj != null && hasOwnProp(obj, '_isAMomentObject')); - }; + img.src = url; + } - // for typechecking Duration objects - moment.isDuration = function (obj) { - return obj instanceof Duration; - }; + return img; + }; - for (i = lists.length - 1; i >= 0; --i) { - makeList(lists[i]); - } + module.exports = Images; - moment.normalizeUnits = function (units) { - return normalizeUnits(units); - }; +/***/ }, +/* 58 */ +/***/ function(module, exports, __webpack_require__) { - moment.invalid = function (flags) { - var m = moment.utc(NaN); - if (flags != null) { - extend(m._pf, flags); - } - else { - m._pf.userInvalidated = true; + "use strict"; + + /** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + * @param {Object} [style] An object containing borderColor, + * backgroundColor, etc. + */ + function Popup(container, x, y, text, style) { + if (container) { + this.container = container; + } else { + this.container = document.body; + } + + // x, y and text are optional, see if a style object was passed in their place + if (style === undefined) { + if (typeof x === "object") { + style = x; + x = undefined; + } else if (typeof text === "object") { + style = text; + text = undefined; + } else { + // for backwards compatibility, in case clients other than Network are creating Popup directly + style = { + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" } + }; + } + } - return m; - }; + this.x = 0; + this.y = 0; + this.padding = 5; + this.hidden = false; - moment.parseZone = function () { - return moment.apply(null, arguments).parseZone(); - }; + if (x !== undefined && y !== undefined) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } - moment.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; + // 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); + } - moment.isDate = isDate; + /** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ + Popup.prototype.setPosition = function (x, y) { + this.x = parseInt(x); + this.y = parseInt(y); + }; - /************************************ - Moment Prototype - ************************************/ + /** + * Set the content for the popup window. This can be HTML code or text. + * @param {string | Element} content + */ + Popup.prototype.setText = function (content) { + if (content instanceof Element) { + this.frame.innerHTML = ""; + this.frame.appendChild(content); + } else { + this.frame.innerHTML = content; // string containing text or HTML + } + }; + /** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ + Popup.prototype.show = function (show) { + if (show === undefined) { + show = true; + } - extend(moment.fn = Moment.prototype, { + if (show) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; - clone : function () { - return moment(this); - }, + var top = this.y - height; + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } - valueOf : function () { - return +this._d - ((this._offset || 0) * 60000); - }, + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; + } - unix : function () { - return Math.floor(+this / 1000); - }, + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + this.hidden = false; + } else { + this.hide(); + } + }; - toString : function () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); - }, + /** + * Hide the popup window + */ + Popup.prototype.hide = function () { + this.hidden = true; + this.frame.style.visibility = "hidden"; + }; - toDate : function () { - return this._offset ? new Date(+this) : this._d; - }, + module.exports = Popup; - 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]'); - } - }, +/***/ }, +/* 59 */ +/***/ function(module, exports, __webpack_require__) { - toArray : function () { - var m = this; - return [ - m.year(), - m.month(), - m.date(), - m.hours(), - m.minutes(), - m.seconds(), - m.milliseconds() - ]; - }, + "use strict"; - isValid : function () { - return isValid(this); - }, + /** + * Parse a text source containing data in DOT language into a JSON object. + * The object contains two lists: one with nodes and one with edges. + * + * DOT language reference: http://www.graphviz.org/doc/info/lang.html + * + * @param {String} data Text containing a graph in DOT-notation + * @return {Object} graph An object containing two parameters: + * {Object[]} nodes + * {Object[]} edges + */ + function parseDOT(data) { + dot = data; + return parseGraph(); + } - isDSTShifted : function () { - if (this._a) { - return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; - } + // token types enumeration + var TOKENTYPE = { + NULL: 0, + DELIMITER: 1, + IDENTIFIER: 2, + UNKNOWN: 3 + }; - return false; - }, + // map with all delimiters + var DELIMITERS = { + "{": true, + "}": true, + "[": true, + "]": true, + ";": true, + "=": true, + ",": true, - parsingFlags : function () { - return extend({}, this._pf); - }, + "->": true, + "--": true + }; - invalidAt: function () { - return this._pf.overflow; - }, + 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 - utc : function (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - }, + /** + * 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); + } - local : function (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; + /** + * 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); + } - if (keepLocalTime) { - this.subtract(this._dateUtcOffset(), 'm'); - } - } - return this; - }, + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } - format : function (inputString) { - var output = formatMoment(this, inputString || moment.defaultFormat); - return this.localeData().postformat(output); - }, + /** + * 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); + } - add : createAdder(1, 'add'), + /** + * Merge all properties of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + function merge(a, b) { + if (!a) { + a = {}; + } - subtract : createAdder(-1, 'subtract'), + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + return a; + } - diff : function (input, units, asFloat) { - var that = makeAs(input, this), - zoneDiff = (that.utcOffset() - this.utcOffset()) * 6e4, - anchor, diff, output, daysAdjust; + /** + * 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; + } + } + } - units = normalizeUnits(units); + /** + * 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; - 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); - }, + // 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; + } - from : function (time, withoutSuffix) { - return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - }, + // 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; + } + } + } - fromNow : function (withoutSuffix) { - return this.from(moment(), withoutSuffix); - }, + if (!current) { + // this is a new node + current = { + id: node.id + }; + if (graph.node) { + // clone default attributes + current.attr = merge(current.attr, graph.node); + } + } - 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))); - }, + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; - isLeapYear : function () { - return isLeapYear(this.year()); - }, + if (!g.nodes) { + g.nodes = []; + } + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); + } + } - isDST : function () { - return (this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset()); - }, + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); + } + } - 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; - } - }, + /** + * 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 + } + } - month : makeAccessor('Month', true), + /** + * 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 + }; - 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 */ - } + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes + } + edge.attr = merge(edge.attr || {}, attr); // merge attributes - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } + return edge; + } - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } + /** + * 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 = ""; - return this; - }, + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); + } - 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'); - }, + do { + var isComment = false; - 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); - } - }, + // skip comment + if (c == "#") { + // find the previous non-space character + var i = index - 1; + while (dot.charAt(i) == " " || dot.charAt(i) == "\t") { + i--; + } + if (dot.charAt(i) == "\n" || dot.charAt(i) == "") { + // the # is at the start of a line, this is indeed a line comment + while (c != "" && c != "\n") { + next(); + } + isComment = true; + } + } + if (c == "/" && nextPreview() == "/") { + // skip line comment + while (c != "" && c != "\n") { + next(); + } + isComment = true; + } + if (c == "/" && nextPreview() == "*") { + // skip block comment + while (c != "") { + if (c == "*" && nextPreview() == "/") { + // end of block comment found. skip these last two characters + next(); + next(); + break; + } else { + next(); + } + } + isComment = true; + } - 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; - } - }, + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); + } + } while (isComment); - isBetween: function (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - }, + // check for end of dot file + if (c == "") { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; + } - 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)); - } - }, + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; + } - 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; - } - ), + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; + } - 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; - } - ), + // 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(); - 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; - } + 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; + } - this.utcOffset(input, keepLocalTime); + // 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; + } - return this; - } else { - return -this.utcOffset(); - } - } - ), + // 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) + "\""); + } - // 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; - } - } + /** + * Parse a graph. + * @returns {Object} graph + */ + function parseGraph() { + var graph = {}; - return this; - } else { - return this._isUTC ? offset : this._dateUtcOffset(); - } - }, + first(); + getToken(); - isLocal : function () { - return !this._isUTC; - }, + // optional strict keyword + if (token == "strict") { + graph.strict = true; + getToken(); + } - isUtcOffset : function () { - return this._isUTC; - }, + // graph or digraph keyword + if (token == "graph" || token == "digraph") { + graph.type = token; + getToken(); + } - isUtc : function () { - return this._isUTC && this._offset === 0; - }, + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); + } - zoneAbbr : function () { - return this._isUTC ? 'UTC' : ''; - }, + // open angle bracket + if (token != "{") { + throw newSyntaxError("Angle bracket { expected"); + } + getToken(); - zoneName : function () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - }, + // statements + parseStatements(graph); - parseZone : function () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(utcOffsetFromString(this._i)); - } - return this; - }, + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); + } + getToken(); - hasAlignedHourOffset : function (input) { - if (!input) { - input = 0; - } - else { - input = moment(input).utcOffset(); - } + // end of file + if (token !== "") { + throw newSyntaxError("End of file expected"); + } + getToken(); - return (this.utcOffset() - input) % 60 === 0; - }, + // remove temporary default properties + delete graph.node; + delete graph.edge; + delete graph.graph; - daysInMonth : function () { - return daysInMonth(this.year(), this.month()); - }, + return graph; + } - 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'); - }, + /** + * Parse a list with statements. + * @param {Object} graph + */ + function parseStatements(graph) { + while (token !== "" && token != "}") { + parseStatement(graph); + if (token == ";") { + getToken(); + } + } + } - quarter : function (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - }, + /** + * 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); - 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'); - }, + return; + } - isoWeekYear : function (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - }, + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; + } - week : function (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - }, + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); + } + var id = token; // id can be a string or a number + getToken(); - isoWeek : function (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - }, + 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); + } + } - weekday : function (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - }, + /** + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph + */ + function parseSubgraph(graph) { + var subgraph = null; - 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); - }, + // optional subgraph keyword + if (token == "subgraph") { + subgraph = {}; + subgraph.type = "subgraph"; + getToken(); - isoWeeksInYear : function () { - return weeksInYear(this.year(), 1, 4); - }, + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } + } - weeksInYear : function () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - }, + // open angle bracket + if (token == "{") { + getToken(); - get : function (units) { - units = normalizeUnits(units); - return this[units](); - }, + if (!subgraph) { + subgraph = {}; + } + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; - 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; - }, + // statements + parseStatements(subgraph); - // 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; + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); + } + getToken(); - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = moment.localeData(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } - }, + // remove temporary default properties + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; - 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); - } - } - ), + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; + } + graph.subgraphs.push(subgraph); + } - localeData : function () { - return this._locale; - }, + return subgraph; + } - _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; - } + /** + * parse an attribute statement like "node [shape=circle fontSize=16]". + * Available keywords are 'node', 'edge', 'graph'. + * The previous list with default attributes will be replaced + * @param {Object} graph + * @returns {String | null} keyword Returns the name of the parsed attribute + * (node, edge, graph), or null if nothing + * is parsed. + */ + function parseAttributeStatement(graph) { + // attribute statements + if (token == "node") { + getToken(); - }); + // node attributes + graph.node = parseAttributeList(); + return "node"; + } else if (token == "edge") { + getToken(); - function rawMonthSetter(mom, value) { - var dayOfMonth; + // edge attributes + graph.edge = parseAttributeList(); + return "edge"; + } else if (token == "graph") { + getToken(); - // 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; - } - } + // graph attributes + graph.graph = parseAttributeList(); + return "graph"; + } - dayOfMonth = Math.min(mom.date(), - daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } + return null; + } - function rawGetter(mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } + /** + * 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); - function rawSetter(mom, unit, value) { - if (unit === 'Month') { - return rawMonthSetter(mom, value); - } else { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } - } + // edge statements + parseEdge(graph, id); + } - 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); - } - }; - } + /** + * 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(); - 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)); + 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(); + } - // 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; + // parse edge attributes + var attr = parseAttributeList(); - // add aliased format methods - moment.fn.toJSON = moment.fn.toISOString; + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); - // alias isUtc for dev-friendliness - moment.fn.isUTC = moment.fn.isUtc; + from = to; + } + } - /************************************ - Duration Prototype - ************************************/ + /** + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr + */ + function parseAttributeList() { + var attr = null; + while (token == "[") { + getToken(); + attr = {}; + while (token !== "" && token != "]") { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute name expected"); + } + var name = token; - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; - } + getToken(); + if (token != "=") { + throw newSyntaxError("Equal sign = expected"); + } + getToken(); - function yearsToDays (years) { - // years * 365 + absRound(years / 4) - - // absRound(years / 100) + absRound(years / 400); - return years * 146097 / 400; + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute value expected"); + } + var value = token; + setValue(attr, name, value); // name can be a path + + getToken(); + if (token == ",") { + getToken(); + } } - extend(moment.duration.fn = Duration.prototype, { + if (token != "]") { + throw newSyntaxError("Bracket ] expected"); + } + getToken(); + } - _bubble : function () { - var milliseconds = this._milliseconds, - days = this._days, - months = this._months, - data = this._data, - seconds, minutes, hours, years = 0; + return attr; + } - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + /** + * 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 + ")"); + } - seconds = absRound(milliseconds / 1000); - data.seconds = seconds % 60; + /** + * 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) + "..."; + } - minutes = absRound(seconds / 60); - data.minutes = minutes % 60; + /** + * 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); + } + } + } - hours = absRound(minutes / 60); - data.hours = hours % 24; + /** + * 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: {} + }; - days += absRound(hours / 24); + // 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); + }); + } - // Accurately convert days to years, assume start from year 0. - years = absRound(daysToYears(days)); - days -= absRound(yearsToDays(years)); + // 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; + }; - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absRound(days / 30); - days %= 30; + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; + } else { + from = { + id: dotEdge.from + }; + } - // 12 months -> 1 year - years += absRound(months / 12); - months %= 12; + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } else { + to = { + id: dotEdge.to + }; + } - data.days = days; - data.months = months; - data.years = years; - }, + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } - abs : function () { - this._milliseconds = Math.abs(this._milliseconds); - this._days = Math.abs(this._days); - this._months = Math.abs(this._months); + 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._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); + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); + } - return this; - }, + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; + } - weeks : function () { - return absRound(this.days() / 7); - }, + return graphData; + } - valueOf : function () { - return this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6; - }, + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; - humanize : function (withSuffix) { - var output = relativeTime(this, !withSuffix, this.localeData()); +/***/ }, +/* 60 */ +/***/ function(module, exports, __webpack_require__) { - if (withSuffix) { - output = this.localeData().pastFuture(+this, output); - } + "use strict"; - return this.localeData().postformat(output); - }, + function parseGephi(gephiJSON, options) { + var edges = []; + var nodes = []; + this.options = { + edges: { + inheritColor: true + }, + nodes: { + allowedToMove: false, + parseColor: false + } + }; - add : function (input, val) { - // supports only 2.0-style add(1, 's') or add(moment) - var dur = moment.duration(input, val); + 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._milliseconds += dur._milliseconds; - this._days += dur._days; - this._months += dur._months; + 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); + } - this._bubble(); + 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); + } - return this; - }, + return { nodes: nodes, edges: edges }; + } - subtract : function (input, val) { - var dur = moment.duration(input, val); + exports.parseGephi = parseGephi; - this._milliseconds -= dur._milliseconds; - this._days -= dur._days; - this._months -= dur._months; +/***/ }, +/* 61 */ +/***/ function(module, exports, __webpack_require__) { - this._bubble(); + "use strict"; - return this; - }, + var SelectionMixin = __webpack_require__(62); + var ManipulationMixin = __webpack_require__(63); + var NavigationMixin = __webpack_require__(64); + var HierarchicalLayoutMixin = __webpack_require__(65); - get : function (units) { - units = normalizeUnits(units); - return this[units.toLowerCase() + 's'](); - }, + /** + * Load a mixin into the network object + * + * @param {Object} sourceVariable | this object has to contain functions. + * @private + */ + exports._loadMixin = function (sourceVariable) { + for (var mixinFunction in sourceVariable) { + if (sourceVariable.hasOwnProperty(mixinFunction)) { + this[mixinFunction] = sourceVariable[mixinFunction]; + } + } + }; - 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); - } - } - }, + /** + * removes a mixin from the network object. + * + * @param {Object} sourceVariable | this object has to contain functions. + * @private + */ + exports._clearMixin = function (sourceVariable) { + for (var mixinFunction in sourceVariable) { + if (sourceVariable.hasOwnProperty(mixinFunction)) { + this[mixinFunction] = undefined; + } + } + }; - lang : moment.fn.lang, - locale : moment.fn.locale, - toIsoString : deprecate( - 'toIsoString() is deprecated. Please use toISOString() instead ' + - '(notice the capitals)', - function () { - return this.toISOString(); - } - ), + /** + * Mixin the physics system and initialize the parameters required. + * + * @private + */ + exports._loadPhysicsSystem = function () { + this._loadMixin(PhysicsMixin); + this._loadSelectedForceSolver(); + if (this.constants.configurePhysics == true) { + this._loadPhysicsConfiguration(); + } else { + this._cleanupPhysicsConfiguration(); + } + }; - 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.asSeconds()) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } - return (this.asSeconds() < 0 ? '-' : '') + - 'P' + - (years ? years + 'Y' : '') + - (months ? months + 'M' : '') + - (days ? days + 'D' : '') + - ((hours || minutes || seconds) ? 'T' : '') + - (hours ? hours + 'H' : '') + - (minutes ? minutes + 'M' : '') + - (seconds ? seconds + 'S' : ''); - }, - localeData : function () { - return this._locale; - }, + /** + * Mixin the selection system and initialize the parameters required + * + * @private + */ + exports._loadSelectionSystem = function () { + this.selectionObj = { nodes: {}, edges: {} }; - toJSON : function () { - return this.toISOString(); - } - }); + this._loadMixin(SelectionMixin); + }; - moment.duration.fn.toString = moment.duration.fn.toISOString; - function makeDurationGetter(name) { - moment.duration.fn[name] = function () { - return this._data[name]; - }; - } + /** + * Mixin the navigationUI (User Interface) system and initialize the parameters required + * + * @private + */ + exports._loadManipulationSystem = function () { + // reset global variables -- these are used by the selection of nodes and edges. + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false; - for (i in unitMillisecondFactors) { - if (hasOwnProp(unitMillisecondFactors, i)) { - makeDurationGetter(i.toLowerCase()); - } + if (this.constants.dataManipulation.enabled == true) { + // load the manipulator HTML elements. All styling done in css. + if (this.manipulationDiv === undefined) { + this.manipulationDiv = document.createElement("div"); + this.manipulationDiv.className = "network-manipulationDiv"; + if (this.editMode == true) { + this.manipulationDiv.style.display = "block"; + } else { + this.manipulationDiv.style.display = "none"; + } + this.frame.appendChild(this.manipulationDiv); } - 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 - ************************************/ - + 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.frame.appendChild(this.editModeDiv); + } - // 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.closeDiv === undefined) { + this.closeDiv = document.createElement("div"); + this.closeDiv.className = "network-manipulation-closeDiv"; + this.closeDiv.style.display = this.manipulationDiv.style.display; + this.frame.appendChild(this.closeDiv); + } - /* EMBED_LOCALES */ + // load the manipulation functions + this._loadMixin(ManipulationMixin); - /************************************ - Exposing Moment - ************************************/ + // create the manipulator toolbar + this._createManipulatorBar(); + } else { + if (this.manipulationDiv !== undefined) { + // removes all the bindings and overloads + this._createManipulatorBar(); - 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; - } + // remove the manipulation divs + this.frame.removeChild(this.manipulationDiv); + this.frame.removeChild(this.editModeDiv); + this.frame.removeChild(this.closeDiv); + + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + // remove the mixin functions + this._clearMixin(ManipulationMixin); } + } + }; - // 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; - } - 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__(79)(module))) + /** + * Mixin the navigation (User Interface) system and initialize the parameters required + * + * @private + */ + exports._loadNavigationControls = function () { + this._loadMixin(NavigationMixin); + // the clean function removes the button divs, this is done to remove the bindings. + this._cleanNavigation(); + if (this.constants.navigation.enabled == true) { + this._loadNavigationElements(); + } + }; + + + /** + * Mixin the hierarchical layout system. + * + * @private + */ + exports._loadHierarchySystem = function () { + this._loadMixin(HierarchicalLayoutMixin); + }; /***/ }, -/* 65 */ +/* 62 */ /***/ function(module, exports, __webpack_require__) { - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + "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(); + var Node = __webpack_require__(55); + + /** + * + * @param object + * @param overlappingNodes + * @private + */ + exports._getNodesOverlappingWith = function (object, overlappingNodes) { + var nodes = this.body.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } } - }(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; + /** + * 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 + */ + exports._getAllNodesOverlappingWith = function (object) { + var overlappingNodes = []; + this._getNodesOverlappingWith(object, overlappingNodes); + return overlappingNodes; + }; - 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; - } + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + exports._pointerToPositionObject = function (pointer) { + var x = this._XconvertDOMtoCanvas(pointer.x); + var y = this._YconvertDOMtoCanvas(pointer.y); - // attach to DOM element - var element = hammer.element; - element.hammer = hammer; + return { + left: x, + top: y, + right: x, + bottom: y + }; + }; - // 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 = {}; + /** + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node + * @private + */ + exports._getNodeAt = function (pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - // 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; - } - }); + // 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 null; + } + }; - /** - * 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); - }); + /** + * 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 + */ + exports._getEdgesOverlappingWith = function (object, overlappingEdges) { + var edges = this.body.edges; + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + } + }; - 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; - }) : []; + /** + * 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 + */ + exports._getAllEdgesOverlappingWith = function (object) { + var overlappingEdges = []; + this._getEdgesOverlappingWith(object, overlappingEdges); + return overlappingEdges; + }; - if (_handlers.length > 0) { - hammer._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer._off(event, propagatedHandler); - delete hammer._handlers[event]; - } - } - }); + /** + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {null} + * @private + */ + exports._getEdgeAt = function (pointer) { + var positionObject = this._pointerToPositionObject(pointer); + var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - return hammer; - }; + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return null; + } + }; - /** - * 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; + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + exports._addToSelection = function (obj) { + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } else { + this.selectionObj.edges[obj.id] = obj; + } + }; - // clear all handlers - hammer._handlers = {}; + /** + * Add object to the selection array. + * + * @param obj + * @private + */ + exports._addToHover = function (obj) { + if (obj instanceof Node) { + this.hoverObj.nodes[obj.id] = obj; + } else { + this.hoverObj.edges[obj.id] = obj; + } + }; - // call original hammer destroy - hammer._destroy(); - }; - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); - } + /** + * Remove a single option from selection. + * + * @param {Object} obj + * @private + */ + exports._removeFromSelection = function (obj) { + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } else { + delete this.selectionObj.edges[obj.id]; + } + }; - /** - * 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; - } - } + /** + * Unselect all. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + exports._unselectAll = function (doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect(); + } + } - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; + this.selectionObj = { nodes: {}, edges: {} }; - // attach firstTarget property to the event - event.firstTarget = _firstTarget; + if (doNotTrigger == false) { + this.emit("select", this.getSelection()); + } + }; - // 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); - } - } + /** + * Unselect all clusters. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + exports._unselectClusters = function (doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } - elem = elem.parentNode; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + this.selectionObj.nodes[nodeId].unselect(); + this._removeFromSelection(this.selectionObj.nodes[nodeId]); } } + } - return hammer; - }; - })); - + if (doNotTrigger == false) { + this.emit("select", this.getSelection()); + } + }; -/***/ }, -/* 66 */ -/***/ function(module, exports, __webpack_require__) { - var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ + /** + * return the number of selected nodes * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ - (function(window, document, exportName, undefined) { - 'use strict'; + * @returns {number} + * @private + */ + exports._getSelectedNodeCount = function () { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + return count; + }; - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); + /** + * return the selected node + * + * @returns {number} + * @private + */ + exports._getSelectedNode = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; + } + } + return null; + }; - var TYPE_FUNCTION = 'function'; + /** + * return the selected edge + * + * @returns {number} + * @private + */ + exports._getSelectedEdge = function () { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return this.selectionObj.edges[edgeId]; + } + } + return null; + }; - 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 + * return the number of selected edges + * * @returns {number} + * @private */ - function setTimeoutContext(fn, timeout, context) { - return setTimeout(bindFn(fn, context), timeout); - } + exports._getSelectedEdgeCount = function () { + var count = 0; + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + }; + /** - * 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} + * return the number of selected objects. + * + * @returns {number} + * @private */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; + exports._getSelectedObjectCount = function () { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; } - return false; - } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; + }; /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context + * Check if anything is selected + * + * @returns {boolean} + * @private */ - function each(obj, iterator, context) { - var i; - - if (!obj) { - return; + exports._selectionIsEmpty = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; } - - 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); - } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; } - } + } + return true; + }; + /** - * 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 + * check if one of the selected nodes is a cluster. + * + * @returns {boolean} + * @private */ - 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++; + exports._clusterInSelection = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; + } } - return dest; - } + } + return false; + }; /** - * 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 + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private */ - function merge(dest, src) { - return extend(dest, src, true); - } + exports._selectConnectedEdges = function (node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); + } + }; /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; - - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; + exports._hoverConnectedEdges = function (node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } + }; - if (properties) { - extend(childP, properties); - } - } /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} + * unselect the edges connected to the node that is being selected + * + * @param {Node} node + * @private */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } + exports._unselectConnectedEdges = function (node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.unselect(); + this._removeFromSelection(edge); + } + }; + + + /** - * 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} + * This is called when someone clicks on a node. either select or deselect it. + * If there is an existing selection and we don't want to append to it, clear the existing selection + * + * @param {Node || Edge} object + * @param {Boolean} append + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private */ - function boolOrFn(val, args) { - if (typeof val == TYPE_FUNCTION) { - return val.apply(args ? args[0] || undefined : undefined, args); + exports._selectObject = function (object, append, doNotTrigger, highlightEdges, overrideSelectable) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + if (highlightEdges === undefined) { + highlightEdges = true; + } + + if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { + this._unselectAll(true); + } + + // selectable allows the object to be selected. Override can be used if needed to bypass this. + if (object.selected == false && (this.constants.selectable == true || overrideSelectable)) { + object.select(); + this._addToSelection(object); + if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { + this._selectConnectedEdges(object); } - return val; - } + } + // do not select the object if selectable is false, only add it to selection to allow drag to work + else if (object.selected == false) { + this._addToSelection(object); + doNotTrigger = true; + } else { + object.unselect(); + this._removeFromSelection(object); + } - /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} - */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; - } + if (doNotTrigger == false) { + this.emit("select", this.getSelection()); + } + }; - /** - * 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 + * 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 */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } + exports._blurObject = function (object) { + if (object.hover == true) { + object.hover = false; + this.emit("blurNode", { node: object.id }); + } + }; /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found + * 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 */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; - } - node = node.parentNode; + exports._hoverObject = function (object) { + if (object.hover == false) { + object.hover = true; + this._addToHover(object); + if (object instanceof Node) { + this.emit("hoverNode", { node: object.id }); } - return false; - } + } + if (object instanceof Node) { + this._hoverConnectedEdges(object); + } + }; - /** - * 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 + * handles the selection part of the touch, only for navigation controls elements; + * Touch is triggered before tap, also before hold. Hold triggers after a while. + * This is the most responsive solution + * + * @param {Object} pointer + * @private */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } + exports._handleTouch = function (pointer) {}; + /** - * 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 + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private */ - function inArray(src, find, findByKey) { - if (src.indexOf && !findByKey) { - return src.indexOf(find); + exports._handleTap = function (pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node, false); + } else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge, false); } else { - var i = 0; - while (i < src.length) { - if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { - return i; - } - i++; - } - return -1; + this._unselectAll(); } - } + } + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } + }; + this.emit("click", properties); + this._requestRedraw(); + }; + /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} + * handles the selection part of the double tap and opens a cluster if needed + * + * @param {Object} pointer + * @private */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } + exports._handleDoubleTap = function (pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = { x: this._XconvertDOMtoCanvas(pointer.x), + y: this._YconvertDOMtoCanvas(pointer.y) }; + this.openCluster(node); + } + var properties = this.getSelection(); + properties.pointer = { + DOM: { x: pointer.x, y: pointer.y }, + canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } + }; + this.emit("doubleClick", properties); + }; + /** - * 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}] + * Handle the onHold selection part + * + * @param pointer + * @private */ - 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++; + exports._handleOnHold = function (pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectObject(node, true); + } else { + var edge = this._getEdgeAt(pointer); + if (edge != null) { + this._selectObject(edge, true); } + } + this._requestRedraw(); + }; - if (sort) { - if (!key) { - results = results.sort(); - } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); - } - } - return results; - } + /** + * handle the onRelease event. These functions are here for the navigation controls module + * and data manipulation module. + * + * @private + */ + exports._handleOnRelease = function (pointer) { + this._manipulationReleaseOverload(pointer); + this._navigationReleaseOverload(pointer); + }; + + exports._manipulationReleaseOverload = function (pointer) {}; + exports._navigationReleaseOverload = function (pointer) {}; /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed + * + * retrieve the currently selected objects + * @return {{nodes: Array., edges: Array.}} selection */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); + exports.getSelection = function () { + var nodeIds = this.getSelectedNodes(); + var edgeIds = this.getSelectedEdges(); + return { nodes: nodeIds, edges: edgeIds }; + }; - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + exports.getSelectedNodes = function () { + var idArray = []; + if (this.constants.selectable == true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } + } + return idArray; + }; - if (prop in obj) { - return prop; - } - i++; + /** + * + * retrieve the currently selected edges + * @return {Array} selection An array with the ids of the + * selected nodes. + */ + exports.getSelectedEdges = function () { + var idArray = []; + if (this.constants.selectable == true) { + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); + } } - return undefined; - } + } + return idArray; + }; - /** - * 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} + * select zero or more nodes DEPRICATED + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } - - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + exports.setSelection = function () { + console.log("setSelection is deprecated. Please use selectNodes instead."); + }; - 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'; + /** + * 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] + */ + exports.selectNodes = function (selection, highlightEdges) { + var i, iMax, id; - var COMPUTE_INTERVAL = 25; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - var INPUT_START = 1; - var INPUT_MOVE = 2; - var INPUT_END = 4; - var INPUT_CANCEL = 8; + // first unselect any selected node + this._unselectAll(true); - var DIRECTION_NONE = 1; - var DIRECTION_LEFT = 2; - var DIRECTION_RIGHT = 4; - var DIRECTION_UP = 8; - var DIRECTION_DOWN = 16; + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; - var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; - var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + 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(); + }; - 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 + * select zero or more edges + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. */ - 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(); + exports.selectEdges = function (selection) { + var i, iMax, id; - } + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, + // first unselect any selected node + this._unselectAll(true); - /** - * 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); - }, + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - /** - * 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 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(); }; /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} + * Validate the selection: remove ids of nodes which no longer exist + * @private */ - 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; + exports._updateSelection = function () { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } } - return new (Type)(manager, inputHandler); - } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } + } + }; + +/***/ }, +/* 63 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var Node = __webpack_require__(55); + var Edge = __webpack_require__(54); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); /** - * handle input events - * @param {Manager} manager - * @param {String} eventType - * @param {Object} input + * clears the toolbar div element of children + * + * @private */ - 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)); + exports._clearManipulatorBar = function () { + this._recursiveDOMDelete(this.manipulationDiv); + this.manipulationDOM = {}; - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; + this._cleanManipulatorHammers(); - if (isFirst) { - manager.session = {}; - } + this._manipulationReleaseOverload = function () {}; + delete this.body.sectors.support.nodes.targetNode; + delete this.body.sectors.support.nodes.targetViaNode; + this.controlNodesActive = false; + this.freezeSimulationEnabled = false; + }; - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; - // compute scale, rotation etc - computeInputData(manager, input); + exports._cleanManipulatorHammers = function () { + // clean hammer bindings + if (this.manipulationHammers.length != 0) { + for (var i = 0; i < this.manipulationHammers.length; i++) { + this.manipulationHammers[i].destroy(); + } + this.manipulationHammers = []; + } + }; - // emit secret event - manager.emit('hammer.input', input); + /** + * 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 + */ + exports._restoreOverloadedFunctions = function () { + for (var functionName in this.cachedFunctions) { + if (this.cachedFunctions.hasOwnProperty(functionName)) { + this[functionName] = this.cachedFunctions[functionName]; + delete this.cachedFunctions[functionName]; + } + } + }; - manager.recognize(input); - manager.session.prevInput = input; - } + /** + * Enable or disable edit-mode. + * + * @private + */ + exports._toggleEditMode = function () { + 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"); + } else { + toolbar.style.display = "none"; + closeDiv.style.display = "none"; + editModeDiv.style.display = "block"; + } + this._createManipulatorBar(); + }; /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input + * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * + * @private */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; + exports._createManipulatorBar = function () { + // remove bound functions + if (this.boundFunction) { + this.off("select", this.boundFunction); + } - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } + this._cleanManipulatorHammers(); - // 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 locale = this.constants.locales[this.constants.locale]; - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; + if (this.edgeBeingEdited !== undefined) { + this.edgeBeingEdited._disableControlNodes(); + this.edgeBeingEdited = undefined; + this.selectedControlNode = null; + this.controlNodesActive = false; + this._redraw(); + } - var center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; + // restore overloaded functions + this._restoreOverloadedFunctions(); - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); + // resume calculation + this.freezeSimulation(false); - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); + // reset global variables + this.blockConnectingEdgeSelection = false; + this.forceAppendSelection = false; + this.manipulationDOM = {}; - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; + if (this.editMode == true) { + while (this.manipulationDiv.hasChildNodes()) { + this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); + } - computeIntervalInputData(session, input); + this.manipulationDOM.addNodeSpan = document.createElement("div"); + this.manipulationDOM.addNodeSpan.className = "network-manipulationUI add"; - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; - } - input.target = target; - } + this.manipulationDOM.addNodeLabelSpan = document.createElement("div"); + this.manipulationDOM.addNodeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.addNodeLabelSpan.innerHTML = locale.addNode; + this.manipulationDOM.addNodeSpan.appendChild(this.manipulationDOM.addNodeLabelSpan); - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; + this.manipulationDOM.addEdgeSpan = document.createElement("div"); + this.manipulationDOM.addEdgeSpan.className = "network-manipulationUI connect"; + this.manipulationDOM.addEdgeLabelSpan = document.createElement("div"); + this.manipulationDOM.addEdgeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.addEdgeLabelSpan.innerHTML = locale.addEdge; + this.manipulationDOM.addEdgeSpan.appendChild(this.manipulationDOM.addEdgeLabelSpan); - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } + this.manipulationDiv.appendChild(this.manipulationDOM.addNodeSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.addEdgeSpan); - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this.manipulationDOM.seperatorLineDiv2 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv2.className = "network-seperatorLine"; - /** - * 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; + this.manipulationDOM.editNodeSpan = document.createElement("div"); + this.manipulationDOM.editNodeSpan.className = "network-manipulationUI edit"; + this.manipulationDOM.editNodeLabelSpan = document.createElement("div"); + this.manipulationDOM.editNodeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.editNodeLabelSpan.innerHTML = locale.editNode; + this.manipulationDOM.editNodeSpan.appendChild(this.manipulationDOM.editNodeLabelSpan); - if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv2); + this.manipulationDiv.appendChild(this.manipulationDOM.editNodeSpan); + } else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + this.manipulationDOM.seperatorLineDiv3 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv3.className = "network-seperatorLine"; - 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.manipulationDOM.editEdgeSpan = document.createElement("div"); + this.manipulationDOM.editEdgeSpan.className = "network-manipulationUI edit"; + this.manipulationDOM.editEdgeLabelSpan = document.createElement("div"); + this.manipulationDOM.editEdgeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.editEdgeLabelSpan.innerHTML = locale.editEdge; + this.manipulationDOM.editEdgeSpan.appendChild(this.manipulationDOM.editEdgeLabelSpan); - 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; + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv3); + this.manipulationDiv.appendChild(this.manipulationDOM.editEdgeSpan); } + if (this._selectionIsEmpty() == false) { + this.manipulationDOM.seperatorLineDiv4 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv4.className = "network-seperatorLine"; - input.velocity = velocity; - input.velocityX = velocityX; - input.velocityY = velocityY; - input.direction = direction; - } + this.manipulationDOM.deleteSpan = document.createElement("div"); + this.manipulationDOM.deleteSpan.className = "network-manipulationUI delete"; + this.manipulationDOM.deleteLabelSpan = document.createElement("div"); + this.manipulationDOM.deleteLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.deleteLabelSpan.innerHTML = locale.del; + this.manipulationDOM.deleteSpan.appendChild(this.manipulationDOM.deleteLabelSpan); - /** - * 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++; + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv4); + this.manipulationDiv.appendChild(this.manipulationDOM.deleteSpan); } - 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; + // bind the icons + this._bindHammerToDiv(this.manipulationDOM.addNodeSpan, "_createAddNodeToolbar"); + this._bindHammerToDiv(this.manipulationDOM.addEdgeSpan, "_createAddEdgeToolbar"); + this._bindHammerToDiv(this.closeDiv, "_toggleEditMode"); - // no need to loop when only one touch - if (pointersLength === 1) { - return { - x: round(pointers[0].clientX), - y: round(pointers[0].clientY) - }; + if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { + this._bindHammerToDiv(this.manipulationDOM.editNodeSpan, "_editNode"); + } else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { + this._bindHammerToDiv(this.manipulationDOM.editEdgeSpan, "_createEditEdgeToolbar"); + } + if (this._selectionIsEmpty() == false) { + this._bindHammerToDiv(this.manipulationDOM.deleteSpan, "_deleteSelected"); } - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; + var me = this; + this.boundFunction = me._createManipulatorBar; + this.on("select", this.boundFunction); + } else { + while (this.editModeDiv.hasChildNodes()) { + this.editModeDiv.removeChild(this.editModeDiv.firstChild); } - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } + this.manipulationDOM.editModeSpan = document.createElement("div"); + this.manipulationDOM.editModeSpan.className = "network-manipulationUI edit editmode"; + this.manipulationDOM.editModeLabelSpan = document.createElement("div"); + this.manipulationDOM.editModeLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.editModeLabelSpan.innerHTML = locale.edit; + this.manipulationDOM.editModeSpan.appendChild(this.manipulationDOM.editModeLabelSpan); + + this.editModeDiv.appendChild(this.manipulationDOM.editModeSpan); + + this._bindHammerToDiv(this.manipulationDOM.editModeSpan, "_toggleEditMode"); + } + }; - /** - * 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; - } + exports._bindHammerToDiv = function (domElement, funct) { + var hammer = new Hammer(domElement, {}); + hammerUtil.onTouch(hammer, this[funct].bind(this)); + this.manipulationHammers.push(hammer); + }; - 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 + * Create the toolbar for adding Nodes + * + * @private */ - 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]]; + exports._createAddNodeToolbar = function () { + // clear the toolbar + this._clearManipulatorBar(); + if (this.boundFunction) { + this.off("select", this.boundFunction); + } - return Math.sqrt((x * x) + (y * y)); - } + var locale = this.constants.locales[this.constants.locale]; - /** - * 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; - } + this.manipulationDOM = {}; + this.manipulationDOM.backSpan = document.createElement("div"); + this.manipulationDOM.backSpan.className = "network-manipulationUI back"; + this.manipulationDOM.backLabelSpan = document.createElement("div"); + this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.backLabelSpan.innerHTML = locale.back; + this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); - /** - * 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); - } + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; - /** - * 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); - } + this.manipulationDOM.descriptionSpan = document.createElement("div"); + this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; + this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); + this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.addDescription; + this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END + this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); + + // bind the icon + this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); + + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + var me = this; + this.boundFunction = me._addNode; + this.on("select", this.boundFunction); }; - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; /** - * Mouse events input - * @constructor - * @extends Input + * create the toolbar to connect nodes + * + * @private */ - 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 + exports._createAddEdgeToolbar = function () { + // clear the toolbar + this._clearManipulatorBar(); + this._unselectAll(true); + this.freezeSimulationEnabled = true; - Input.apply(this, arguments); - } + if (this.boundFunction) { + this.off("select", this.boundFunction); + } - inherit(MouseInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; + var locale = this.constants.locales[this.constants.locale]; - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } + this.forceAppendSelection = false; + this.blockConnectingEdgeSelection = true; - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; - } + this.manipulationDOM = {}; + this.manipulationDOM.backSpan = document.createElement("div"); + this.manipulationDOM.backSpan.className = "network-manipulationUI back"; + this.manipulationDOM.backLabelSpan = document.createElement("div"); + this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.backLabelSpan.innerHTML = locale.back; + this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; - } + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; - if (eventType & INPUT_END) { - this.pressed = false; - } + this.manipulationDOM.descriptionSpan = document.createElement("div"); + this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; + this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); + this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.edgeDescription; + this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); - } - }); + this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL - }; + // bind the icon + this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); - // 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 - }; + // we use the boundFunction so we can reference it when we unbind it from the "select" event. + var me = this; + this.boundFunction = me._handleConnect; + this.on("select", this.boundFunction); - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; + // temporarily overload functions + this.cachedFunctions._handleTouch = this._handleTouch; + this.cachedFunctions._manipulationReleaseOverload = this._manipulationReleaseOverload; + this.cachedFunctions._handleDragStart = this._handleDragStart; + this.cachedFunctions._handleDragEnd = this._handleDragEnd; + this.cachedFunctions._handleOnHold = this._handleOnHold; + this._handleTouch = this._handleConnect; + this._manipulationReleaseOverload = function () {}; + this._handleOnHold = function () {}; + this._handleDragStart = function () {}; + this._handleDragEnd = this._finishConnect; - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } + // redraw to show the unselect + this._redraw(); + }; /** - * Pointer events input - * @constructor - * @extends Input + * create the toolbar to edit edges + * + * @private */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; + exports._createEditEdgeToolbar = function () { + // clear the toolbar + this._clearManipulatorBar(); + this.controlNodesActive = true; - Input.apply(this, arguments); + if (this.boundFunction) { + this.off("select", this.boundFunction); + } - this.store = (this.manager.session.pointerEvents = []); - } + this.edgeBeingEdited = this._getSelectedEdge(); + this.edgeBeingEdited._enableControlNodes(); - inherit(PointerEventInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function PEhandler(ev) { - var store = this.store; - var removePointer = false; + var locale = this.constants.locales[this.constants.locale]; - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; + this.manipulationDOM = {}; + this.manipulationDOM.backSpan = document.createElement("div"); + this.manipulationDOM.backSpan.className = "network-manipulationUI back"; + this.manipulationDOM.backLabelSpan = document.createElement("div"); + this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.backLabelSpan.innerHTML = locale.back; + this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); - var isTouch = (pointerType == INPUT_TYPE_TOUCH); + this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); + this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + this.manipulationDOM.descriptionSpan = document.createElement("div"); + this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; + this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); + this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; + this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.editEdgeDescription; + this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); - // 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; - } + this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); + this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); + this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; - } + // bind the icon + this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); - // update the event in the store - store[storeIndex] = ev; + // temporarily overload functions + this.cachedFunctions._handleTouch = this._handleTouch; + this.cachedFunctions._manipulationReleaseOverload = this._manipulationReleaseOverload; + this.cachedFunctions._handleTap = this._handleTap; + this.cachedFunctions._handleDragStart = this._handleDragStart; + this.cachedFunctions._handleOnDrag = this._handleOnDrag; + this._handleTouch = this._selectControlNode; + this._handleTap = function () {}; + this._handleOnDrag = this._controlNodeDrag; + this._handleDragStart = function () {}; + this._manipulationReleaseOverload = this._releaseControlNode; - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); + // redraw to show the unselect + this._redraw(); + }; - 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 + /** + * 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 + */ + exports._selectControlNode = function (pointer) { + this.edgeBeingEdited.controlNodes.from.unselect(); + this.edgeBeingEdited.controlNodes.to.unselect(); + this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x), this._YconvertDOMtoCanvas(pointer.y)); + if (this.selectedControlNode !== null) { + this.selectedControlNode.select(); + this.freezeSimulation(true); + } + this._redraw(); }; - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * Touch events input - * @constructor - * @extends Input + * 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 */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; + exports._controlNodeDrag = function (event) { + var pointer = this._getPointer(event.center); + if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { + this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); + this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); + } + this._redraw(); + }; - Input.apply(this, arguments); - } - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + /** + * + * @param pointer + * @private + */ + exports._releaseControlNode = function (pointer) { + var newNode = this._getNodeAt(pointer); + if (newNode !== null) { + if (this.edgeBeingEdited.controlNodes.from.selected == true) { + this.edgeBeingEdited._restoreControlNodes(); + this._editEdge(newNode.id, this.edgeBeingEdited.to.id); + this.edgeBeingEdited.controlNodes.from.unselect(); + } + if (this.edgeBeingEdited.controlNodes.to.selected == true) { + this.edgeBeingEdited._restoreControlNodes(); + this._editEdge(this.edgeBeingEdited.from.id, newNode.id); + this.edgeBeingEdited.controlNodes.to.unselect(); + } + } else { + this.edgeBeingEdited._restoreControlNodes(); + } + this.freezeSimulation(false); + this._redraw(); + }; - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; - } + /** + * 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 + */ + exports._handleConnect = function (pointer) { + if (this._getSelectedNodeCount() == 0) { + var node = this._getNodeAt(pointer); - if (!this.started) { - return; - } + if (node != null) { + if (this.isCluster(node.id) == true) { + alert(this.constants.locales[this.constants.locale].createEdgeError); + } else { + this._selectObject(node, false); + var supportNodes = this.body.sectors.support.nodes; - var touches = normalizeSingleTouches.call(this, ev, type); + // create a node the temporary line can look at + supportNodes.targetNode = this.body.functions.createNode({ id: "targetNode" }); + var targetNode = supportNodes.targetNode; + targetNode.x = node.x; + targetNode.y = node.y; - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; - } + // create a temporary edge + this.body.edges.connectionEdge = this.body.functions.createEdge({ id: "connectionEdge", from: node.id, to: targetNode.id }); + var connectionEdge = this.body.edges.connectionEdge; + connectionEdge.from = node; + connectionEdge.connected = true; + connectionEdge.options.smoothCurves = { enabled: true, + dynamic: false, + type: "continuous", + roundness: 0.5 + }; + connectionEdge.selected = true; + connectionEdge.to = targetNode; - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); + this.cachedFunctions._handleOnDrag = this._handleOnDrag; + var me = this; + this._handleOnDrag = function (event) { + var pointer = me._getPointer(event.center); + var connectionEdge = me.body.edges.connectionEdge; + connectionEdge.to.x = me._XconvertDOMtoCanvas(pointer.x); + connectionEdge.to.y = me._YconvertDOMtoCanvas(pointer.y); + me._redraw(); + }; + } } - }); + } + }; - /** - * @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); + exports._finishConnect = function (event) { + if (this._getSelectedNodeCount() == 1) { + var pointer = this._getPointer(event.center); + // restore the drag function + this._handleOnDrag = this.cachedFunctions._handleOnDrag; + delete this.cachedFunctions._handleOnDrag; - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); + // remember the edge id + var connectFromId = this.body.edges.connectionEdge.fromId; + + // remove the temporary nodes and edge + delete this.body.edges.connectionEdge; + delete this.body.sectors.support.nodes.targetNode; + delete this.body.sectors.support.nodes.targetViaNode; + + var node = this._getNodeAt(pointer); + if (node != null) { + if (this.isCluster(node.id) === true) { + alert(this.constants.locales[this.constants.locale].createEdgeError); + } else { + this._createEdge(connectFromId, node.id); + this._createManipulatorBar(); + } } + this._unselectAll(); + } + }; - return [all, changed]; - } - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL + /** + * Adds a node on the specified location + */ + exports._addNode = function () { + if (this._selectionIsEmpty() && this.editMode == true) { + var positionObject = this._pointerToPositionObject(this.pointerPosition); + var defaultData = { id: util.randomUUID(), x: positionObject.left, y: positionObject.top, label: "new", allowedToMoveX: true, allowedToMoveY: true }; + if (this.triggerFunctions.add) { + if (this.triggerFunctions.add.length == 2) { + var me = this; + this.triggerFunctions.add(defaultData, function (finalizedData) { + me.body.data.nodes.add(finalizedData); + me._createManipulatorBar(); + me.moving = true; + me.start(); + }); + } else { + throw new Error("The function for add does not support two arguments (data,callback)"); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } else { + this.body.data.nodes.add(defaultData); + this._createManipulatorBar(); + this.moving = true; + this.start(); + } + } }; - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * Multi-user touch events input - * @constructor - * @extends Input + * connect two nodes with a new edge. + * + * @private */ - 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 + exports._createEdge = function (sourceNodeId, targetNodeId) { + if (this.editMode == true) { + var defaultData = { from: sourceNodeId, to: targetNodeId }; + if (this.triggerFunctions.connect) { + if (this.triggerFunctions.connect.length == 2) { + var me = this; + this.triggerFunctions.connect(defaultData, function (finalizedData) { + me.body.data.edges.add(finalizedData); + me.moving = true; + me.start(); }); + } else { + throw new Error("The function for connect does not support two arguments (data,callback)"); + this.moving = true; + this.start(); + } + } else { + this.body.data.edges.add(defaultData); + this.moving = true; + this.start(); } - }); + } + }; /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] + * connect two nodes with a new edge. + * + * @private */ - 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]; + exports._editEdge = function (sourceNodeId, targetNodeId) { + if (this.editMode == true) { + var defaultData = { id: this.edgeBeingEdited.id, from: sourceNodeId, to: targetNodeId }; + console.log(defaultData); + if (this.triggerFunctions.editEdge) { + if (this.triggerFunctions.editEdge.length == 2) { + var me = this; + this.triggerFunctions.editEdge(defaultData, function (finalizedData) { + me.body.data.edges.update(finalizedData); + me.moving = true; + me.start(); + }); + } else { + throw new Error("The function for edit does not support two arguments (data, callback)"); + this.moving = true; + this.start(); + } + } else { + this.body.data.edges.update(defaultData); + this.moving = true; + this.start(); } + } + }; - 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++; + /** + * 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 + */ + exports._editNode = function () { + if (this.triggerFunctions.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 } + } }; + if (this.triggerFunctions.edit.length == 2) { + var me = this; + this.triggerFunctions.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"); + } + }; - // 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. + * delete everything in the selection * - * @constructor - * @extends Input + * @private */ - 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; + exports._deleteSelected = function () { + if (!this._selectionIsEmpty() && this.editMode == true) { + if (!this._clusterInSelection()) { + var selectedNodes = this.getSelectedNodes(); + var selectedEdges = this.getSelectedEdges(); + if (this.triggerFunctions.del) { + var me = this; + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (this.triggerFunctions.del.length == 2) { + this.triggerFunctions.del(data, function (finalizedData) { + me.body.data.edges.remove(finalizedData.edges); + me.body.data.nodes.remove(finalizedData.nodes); + me._unselectAll(); + me.moving = true; + me.start(); + }); + } 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._unselectAll(); + this.moving = true; + this.start(); + } + } else { + alert(this.constants.locales[this.constants.locale].deleteClusterError); + } + } + }; - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } +/***/ }, +/* 64 */ +/***/ function(module, exports, __webpack_require__) { - this.callback(manager, inputEvent, inputData); - }, + "use strict"; - /** - * remove the event listeners - */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); + var util = __webpack_require__(1); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + + exports._cleanNavigation = function () { + // clean hammer bindings + if (this.navigationHammers.length != 0) { + for (var i = 0; i < this.navigationHammers.length; i++) { + this.navigationHammers[i].destroy(); } - }); + this.navigationHammers = []; + } - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; + this._navigationReleaseOverload = function () {}; - // 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'; + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { + this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); + } + }; /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor + * 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 */ - 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); - }, + exports._loadNavigationElements = function () { + this._cleanNavigation(); - /** - * 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.navigationDOM = {}; + var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; + var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - /** - * 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.navigationDOM.wrapper = document.createElement("div"); + this.frame.appendChild(this.navigationDOM.wrapper); - var srcEvent = input.srcEvent; - var direction = input.offsetDirection; + 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]]); - // if the touch action did prevented once this session - if (this.manager.session.prevented) { - srcEvent.preventDefault(); - return; - } + var hammer = new Hammer(this.navigationDOM[navigationDivs[i]], { prevent_default: true }); + hammerUtil.onTouch(hammer, this[navigationDivActions[i]].bind(this)); + this.navigationHammers.push(hammer); + } - 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); + this._navigationReleaseOverload = this._stopMovement; + }; - 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(); - } + /** + * this stops all movement induced by the navigation buttons + * + * @private + */ + exports._zoomExtent = function (event) { + this.zoomExtent({ duration: 700 }); + event.stopPropagation(); }; /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} + * this stops all movement induced by the navigation buttons + * + * @private */ - 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; - } + exports._stopMovement = function () { + this._xStopMoving(); + this._yStopMoving(); + this._stopZoom(); + }; - 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. + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. * - * Possible - * | - * +-----+---------------+ - * | | - * +-----+-----+ | - * | | | - * Failed Cancelled | - * +-------+------+ - * | | - * Recognized Began - * | - * Changed - * | - * Ended/Recognized + * @private */ - 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; + exports._moveUp = function (event) { + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; + /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options + * move the screen down + * @private */ - 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); + exports._moveDown = function (event) { + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - // 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; - } + /** + * move the screen left + * @private + */ + exports._moveLeft = function (event) { + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - 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; - } + /** + * move the screen right + * @private + */ + exports._moveRight = function (event) { + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - 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; - } + /** + * Zoom in, using the same method as the movement. + * @private + */ + exports._zoomIn = function (event) { + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - 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; - } + /** + * Zoom out + * @private + */ + exports._zoomOut = function (event) { + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + event.preventDefault(); + }; - 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; - }, + /** + * Stop zooming and unhighlight the zoom controls + * @private + */ + exports._stopZoom = function (event) { + this.zoomIncrement = 0; + event && event.preventDefault(); + }; - /** - * 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; + /** + * Stop moving in the Y direction and unHighlight the up and down + * @private + */ + exports._yStopMoving = function (event) { + this.yIncrement = 0; + event && event.preventDefault(); + }; - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); - } + /** + * Stop moving in the X direction and unHighlight left and right. + * @private + */ + exports._xStopMoving = function (event) { + this.xIncrement = 0; + event && event.preventDefault(); + }; - emit(); // simple 'eventName' events +/***/ }, +/* 65 */ +/***/ function(module, exports, __webpack_require__) { - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); - } - }, + "use strict"; - /** - * 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; - }, + exports._resetLevels = function () { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (node.preassignedLevel == false) { + node.level = -1; + node.hierarchyEnumerated = false; + } + } + } + }; - /** - * 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 is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * + * @private + */ + exports._setupHierarchicalLayout = function () { + if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var hubsize = 0; + var node, nodeId; + var definedLevel = false; + var undefinedLevel = false; + + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.level != -1) { + definedLevel = true; + } else { + undefinedLevel = true; } - return true; - }, + if (hubsize < node.edges.length) { + hubsize = node.edges.length; + } + } + } - /** - * update the recognizer - * @param {Object} inputData - */ - recognize: function(inputData) { - // make a new copy of the inputData - // so we can change the inputData without messing up the other recognizers - var inputDataClone = extend({}, inputData); + // if 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."); + this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); + if (!this.constants.clustering.enabled) { + this.start(); + } + } else { + // setup the system to use hierarchical method. + this._changeConstants(); - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + if (this.constants.hierarchicalLayout.layout == "hubsize") { + this._determineLevels(hubsize); + } else { + this._determineLevelsDirected(false); } + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; - } + // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize. + this._placeNodesByHierarchy(distribution); + } + } + }; - 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); - } - }, + /** + * This function places the nodes on the canvas based on the hierarchial distribution. + * + * @param {Object} distribution | obtained by the function this._getDistribution() + * @private + */ + exports._placeNodesByHierarchy = function (distribution) { + var nodeId, node; - /** - * 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 + // 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.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (node.xFixed) { + node.x = distribution[level].minPos; + node.xFixed = false; - /** - * return the preferred touch-action - * @virtual - * @returns {Array} - */ - getTouchAction: function() { }, + distribution[level].minPos += distribution[level].nodeSpacing; + } + } else { + if (node.yFixed) { + node.y = distribution[level].minPos; + node.yFixed = false; - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } + distribution[level].minPos += distribution[level].nodeSpacing; + } + } + this._placeBranchNodes(node.edges, node.id, distribution, node.level); + } + } + } + } + + // stabilize the system after positioning. This function calls zoomExtent. + this._stabilize(); }; + /** - * get a usable string, used as event postfix - * @param {Const} state - * @returns {String} state + * This function get the distribution of levels based on hubsize + * + * @returns {Object} + * @private */ - 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'; + exports._getDistribution = function () { + var distribution = {}; + var nodeId, node, level; + + // 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]; + node.xFixed = true; + node.yFixed = true; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + node.y = this.constants.hierarchicalLayout.levelSeparation * node.level; + } else { + node.x = this.constants.hierarchicalLayout.levelSeparation * node.level; + } + if (distribution[node.level] === undefined) { + distribution[node.level] = { amount: 0, nodes: {}, minPos: 0, nodeSpacing: 0 }; + } + distribution[node.level].amount += 1; + distribution[node.level].nodes[nodeId] = node; } - 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'; + // determine the largest amount of nodes of all levels + var maxCount = 0; + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + if (maxCount < distribution[level].amount) { + maxCount = distribution[level].amount; + } } - 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); + // set the initial position and spacing of each nodes accordingly + for (level in distribution) { + if (distribution.hasOwnProperty(level)) { + distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; + distribution[level].nodeSpacing /= distribution[level].amount + 1; + distribution[level].minPos = distribution[level].nodeSpacing - 0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing; } - return otherRecognizer; - } + } + + return distribution; + }; + /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize + * @private */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } - - inherit(AttrRecognizer, Recognizer, { - /** - * @namespace - * @memberof AttrRecognizer - */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, + exports._determineLevels = function (hubsize) { + var nodeId, node; - /** - * 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; - }, + // determine hubs + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length == hubsize) { + node.level = 0; + } + } + } - /** - * 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; + // branch from hubs + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.level == 0) { + this._setLevel(1, node.edges, node.id); + } + } + } + }; - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); - // on cancel input and we've recognized before, return STATE_CANCELLED - if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) { - return state | STATE_CANCELLED; - } else if (isRecognized || isValid) { - if (eventType & INPUT_END) { - return state | STATE_ENDED; - } else if (!(state & STATE_BEGAN)) { - return STATE_BEGAN; - } - return state | STATE_CHANGED; - } - return STATE_FAILED; - } - }); /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer + * this function allocates nodes in levels based on the direction of the edges + * + * @param hubsize + * @private */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); - - this.pX = null; - this.pY = null; - } - - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL - }, + exports._determineLevelsDirected = function () { + var nodeId, node, firstNode; + var minLevel = 10000; - 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; - }, + // set first node to source + firstNode = this.body.nodes[this.nodeIndices[0]]; + firstNode.level = minLevel; + this._setLevelDirected(minLevel, firstNode.edges, firstNode.id); - 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; + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + minLevel = node.level < minLevel ? node.level : minLevel; + } + } - // 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; - }, + // 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)) { + node = this.body.nodes[nodeId]; + node.level -= minLevel; + } + } + }; - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + /** + * Since hierarchical layout does not support: + * - smooth curves (based on the physics), + * - clustering (based on dynamic node counts) + * + * We disable both features so there will be no problems. + * + * @private + */ + exports._changeConstants = function () { + this.constants.clustering.enabled = false; + this.constants.physics.barnesHut.enabled = false; + this.constants.physics.hierarchicalRepulsion.enabled = true; + this._loadSelectedForceSolver(); + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.dynamic = false; + } + this._configureSmoothCurves(); - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + var config = this.constants.hierarchicalLayout; + config.levelSeparation = Math.abs(config.levelSeparation); + if (config.direction == "RL" || config.direction == "DU") { + config.levelSeparation *= -1; + } - this._super.emit.call(this, input); + if (config.direction == "RL" || config.direction == "LR") { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "vertical"; } - }); + } else { + if (this.constants.smoothCurves.enabled == true) { + this.constants.smoothCurves.type = "horizontal"; + } + } + }; + /** - * Pinch - * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). - * @constructor - * @extends AttrRecognizer + * 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 */ - function PinchRecognizer() { - AttrRecognizer.apply(this, arguments); - } + exports._placeBranchNodes = function (edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; + } - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + var nodeMoved = false; + if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { + if (childNode.xFixed && childNode.level > parentLevel) { + childNode.xFixed = false; + childNode.x = distribution[childNode.level].minPos; + nodeMoved = true; + } + } else { + if (childNode.yFixed && childNode.level > parentLevel) { + childNode.yFixed = false; + childNode.y = distribution[childNode.level].minPos; + nodeMoved = true; + } + } - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + if (nodeMoved == true) { + distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNode.level); + } + } + } + }; - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); - }, - 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); - } + /** + * 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 + */ + exports._setLevel = function (level, edges, parentId) { + for (var i = 0; i < edges.length; i++) { + var childNode = null; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; } - }); + if (childNode.level == -1 || childNode.level > level) { + childNode.level = level; + if (childNode.edges.length > 1) { + this._setLevel(level + 1, childNode.edges, childNode.id); + } + } + } + }; + /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer + * 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 */ - function PressRecognizer() { - Recognizer.apply(this, arguments); + exports._setLevelDirected = function (level, edges, parentId) { + this.body.nodes[parentId].hierarchyEnumerated = true; + var childNode, direction; + for (var i = 0; i < edges.length; i++) { + direction = 1; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + direction = -1; + } else { + childNode = edges[i].to; + } + if (childNode.level == -1) { + childNode.level = level + direction; + } + } - this._timer = null; - this._input = null; - } + for (var i = 0; i < edges.length; i++) { + if (edges[i].toId == parentId) { + childNode = edges[i].from; + } else { + childNode = edges[i].to; + } - 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 - }, + if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { + this._setLevelDirected(childNode.level, childNode.edges, childNode.id); + } + } + }; - getTouchAction: function() { - return [TOUCH_ACTION_AUTO]; - }, - 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; + /** + * Unfix nodes + * + * @private + */ + exports._restoreNodes = function () { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId].xFixed = false; + this.body.nodes[nodeId].yFixed = false; + } + } + }; - this._input = input; +/***/ }, +/* 66 */ +/***/ function(module, exports, __webpack_require__) { - // 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; - }, + "use strict"; - reset: function() { - clearTimeout(this._timer); - }, + // 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; - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; - } + // 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; - 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); - } - } - }); +/***/ }, +/* 67 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer + * Canvas shapes used by Network */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); - } + 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); + }; - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 - }, + /** + * 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); + }; - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + /** + * 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(); - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y - (h - ir)); + this.lineTo(x + s2, y + ir); + this.lineTo(x - s2, y + ir); + this.lineTo(x, y - (h - ir)); + this.closePath(); + }; + + /** + * Draw a triangle shape in downward orientation + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius + */ + CanvasRenderingContext2D.prototype.triangleDown = function (x, y, r) { + // http://en.wikipedia.org/wiki/Equilateral_triangle + this.beginPath(); + + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height + + this.moveTo(x, y + (h - ir)); + this.lineTo(x + s2, y - ir); + this.lineTo(x - s2, y - ir); + this.lineTo(x, y + (h - ir)); + this.closePath(); + }; + + /** + * Draw a star shape, a star with 5 points + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.star = function (x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); + + for (var n = 0; n < 10; n++) { + var radius = n % 2 === 0 ? r * 1.3 : r * 0.5; + this.lineTo(x + radius * Math.sin(n * 2 * Math.PI / 10), y - radius * Math.cos(n * 2 * Math.PI / 10)); } - }); - /** - * 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); - } + this.closePath(); + }; - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 - }, + /** + * 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); + }; - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, + /** + * 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 - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + 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); + }; - if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { - velocity = input.velocity; - } else if (direction & DIRECTION_HORIZONTAL) { - velocity = input.velocityX; - } else if (direction & DIRECTION_VERTICAL) { - velocity = input.velocityY; - } - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; - }, - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + /** + * 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.manager.emit(this.options.event, input); - } - }); + 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 - /** - * 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); + this.beginPath(); + this.moveTo(xe, ym); - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - this._timer = null; - this._input = null; - this.count = 0; - } + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - 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.lineTo(xe, ymb); - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; - }, + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); - process: function(input) { - var options = this.options; + this.lineTo(x, ym); + }; - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; - this.reset(); + /** + * 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); - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); - } + // 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); - // 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(); - } + // 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); - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); - this.pTime = input.timeStamp; - this.pCenter = input.center; + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } + /** + * 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._input = input; + // TODO: add diamond shape + } - // 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; - }, +/***/ }, +/* 68 */ +/***/ 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 + * Created by Alex on 2/23/2015. */ - function Hammer(element, options) { - options = options || {}; - options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); - return new Manager(element, options); - } - /** - * @const {string} - */ - Hammer.VERSION = '2.0.4'; + var BarnesHutSolver = __webpack_require__(69).BarnesHutSolver; + var Repulsion = __webpack_require__(70).Repulsion; + var HierarchicalRepulsion = __webpack_require__(71).HierarchicalRepulsion; + var SpringSolver = __webpack_require__(72).SpringSolver; + var HierarchicalSpringSolver = __webpack_require__(73).HierarchicalSpringSolver; + var CentralGravitySolver = __webpack_require__(74).CentralGravitySolver; - /** - * default settings - * @namespace - */ - Hammer.defaults = { - /** - * set if DOM events are being triggered. - * But this is slower and unused by simple implementations, so disabled by default. - * @type {Boolean} - * @default false - */ - domEvents: false, - /** - * The value for the touchAction property/fallback. - * When set to `compute` it will magically set the correct value based on the added recognizers. - * @type {String} - * @default compute - */ - touchAction: TOUCH_ACTION_COMPUTE, + var util = __webpack_require__(1); - /** - * @type {Boolean} - * @default true - */ - enable: true, - /** - * EXPERIMENTAL FEATURE -- can be removed/changed - * Change the parent input target element. - * If Null, then it is being set the to main element. - * @type {Null|EventTarget} - * @default null - */ - inputTarget: null, + var PhysicsEngine = (function () { + function PhysicsEngine(body) { + var _this = this; + _classCallCheck(this, PhysicsEngine); - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, + this.body = body; + this.physicsBody = { calculationNodes: {}, calculationNodeIndices: [], forces: {}, velocities: {} }; - /** - * Default recognizer setup when calling `Hammer()` - * When creating a new Manager these will be skipped. - * @type {Array} - */ - preset: [ - // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...] - [RotateRecognizer, { enable: false }], - [PinchRecognizer, { enable: false }, ['rotate']], - [SwipeRecognizer,{ direction: DIRECTION_HORIZONTAL }], - [PanRecognizer, { direction: DIRECTION_HORIZONTAL }, ['swipe']], - [TapRecognizer], - [TapRecognizer, { event: 'doubletap', taps: 2 }, ['tap']], - [PressRecognizer] - ], + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.renderTimer == undefined; - /** - * 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', + this.stabilized = false; + this.stabilizationIterations = 0; - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + // 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: 150, + damping: 0.09 + }, + model: "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); - /** - * 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', + this.body.emitter.on("stabilize", function () { + _this.startSimulation(); + }); + this.body.emitter.on("startSimulation", function () { + _this.stabilized = false;_this.runSimulation(); + }); + this.body.emitter.on("stopSimulation", function () { + _this.stopSimulation(); + }); + } - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + _prototypeProperties(PhysicsEngine, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + if (typeof options.stabilization == "boolean") { + options.stabilization = { + enabled: options.stabilization + }; + } + util.deepExtend(this.options, options); + } + this.init(); + }, + writable: true, + configurable: true + }, + init: { + value: function init() { + var options; + if (this.options.model == "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.model == "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); + } - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', + this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); + this.modelOptions = options; + }, + writable: true, + configurable: true + }, + startSimulation: { + value: function startSimulation() { + this.stabilized = false; + if (this.options.stabilization.enabled === true) { + this.stabilize(); + } else { + this.runSimulation(); + } + }, + writable: true, + configurable: true + }, + stopSimulation: { + value: function stopSimulation() { + this.stabilized = true; + if (this.viewFunction !== undefined) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.viewFunction = undefined; + this.body.emitter.emit("_stopRendering"); + } + }, + writable: true, + configurable: true + }, + runSimulation: { + value: function runSimulation() { + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + }, + 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; - /** - * 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)' - } - }; + // run double speed if it is a little graph + if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { + this.physicsTick(); - var STOP = 1; - var FORCED_STOP = 2; + // this makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } - /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Manager(element, options) { - options = options || {}; + 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: { - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; + /** + * A single simulation step (or "tick") in the physics simulation + * + * @private + */ + value: function physicsTick() { + if (this.stabilized === false) { + this.calculateForces(); + this.stabilized = this.moveNodes(); + + // 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; + } + } - this.handlers = {}; - this.session = {}; - this.recognizers = []; + this.stabilizationIterations++; + } + }, + writable: true, + configurable: true + }, + _updateCalculationNodes: { - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + /** + * 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 _updateCalculationNodes() { + this.physicsBody.calculationNodes = {}; + this.physicsBody.forces = {}; + this.physicsBody.calculationNodeIndices = []; - toggleCssProps(this, true); + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + this.physicsBody.calculationNodes[nodeId] = this.body.nodes[nodeId]; + } - 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); - } + // if support nodes are used, we have them here + var supportNodes = this.body.supportNodes; + for (var i = 0; i < this.body.supportNodeIndices.length; i++) { + var supportNodeId = this.body.supportNodeIndices[i]; + if (this.body.edges[supportNodes[supportNodeId].parentEdgeId] !== undefined) { + this.physicsBody.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; + } else { + console.error("Support node detected that does not have an edge!"); + } + } - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes); + for (var i = 0; i < this.physicsBody.calculationNodeIndices.length; i++) { + var nodeId = this.physicsBody.calculationNodeIndices[i]; + this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; - // Options that need a little more setup - if (options.touchAction) { - this.touchAction.update(); + // 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 }; + } } - if (options.inputTarget) { - // Clean up existing event listeners and reinitialize - this.input.destroy(); - this.input.target = options.inputTarget; - this.input.init(); + + // clean deleted nodes from the velocity vector + for (var nodeId in this.physicsBody.velocities) { + if (this.physicsBody.calculationNodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } } - return this; + }, + writable: true, + configurable: true }, + revert: { + value: function revert() { + var nodeIds = Object.keys(this.previousStates); + var nodes = this.physicsBody.calculationNodes; + var velocities = this.physicsBody.velocities; - /** - * 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; + 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.calculationNodeIndices; + 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); - /** - * 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; + 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; } - // run the touch-action polyfill - this.touchAction.preventDefaults(inputData); - - var recognizer; - var recognizers = this.recognizers; - - // this holds the recognizer that is being recognized. - // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED - // if no recognizer is detecting a thing, it is set to `null` - var curRecognizer = session.curRecognizer; - // reset when the last recognizer is recognized - // or when we're in a new session - if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) { - curRecognizer = session.curRecognizer = null; + if (nodesPresent == true) { + if (vminCorrected > 0.5 * this.options.maxVelocity) { + return false; + } else { + return stabilized; + } } + return true; + }, + writable: true, + configurable: true + }, + _performStep: { + value: function _performStep(nodeId, maxVelocity) { + var node = this.physicsBody.calculationNodes[nodeId]; + var timestep = this.options.timestep; + var forces = this.physicsBody.forces; + var velocities = this.physicsBody.velocities; - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; - - // find out if we are allowed try to recognize the input for this one. - // 1. allow if the session is NOT forced stopped (see the .stop() method) - // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one - // that is being recognized. - // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. - // this can be setup with the `recognizeWith()` method on the recognizer. - if (session.stopped !== FORCED_STOP && ( // 1 - !curRecognizer || recognizer == curRecognizer || // 2 - recognizer.canRecognizeWith(curRecognizer))) { // 3 - recognizer.recognize(inputData); - } else { - recognizer.reset(); - } + // 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 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++; + if (!node.xFixed) { + 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; } - }, - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; + if (!node.yFixed) { + 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; } - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; - } - } - return null; + 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: { - /** - * add a recognizer to the manager - * existing recognizers with the same event name will be removed - * @param {Recognizer} recognizer - * @returns {Recognizer|Manager} - */ - add: function(recognizer) { - if (invokeArrayArg(recognizer, 'add', this)) { - return this; - } - // remove existing - var existing = this.get(recognizer.options.event); - if (existing) { - this.remove(existing); - } - this.recognizers.push(recognizer); - recognizer.manager = this; - this.touchAction.update(); - return recognizer; - }, - /** - * remove a recognizer by name or instance - * @param {Recognizer|String} recognizer - * @returns {Manager} - */ - remove: function(recognizer) { - if (invokeArrayArg(recognizer, 'remove', this)) { - return this; - } - var recognizers = this.recognizers; - recognizer = this.get(recognizer); - recognizers.splice(inArray(recognizers, recognizer), 1); - this.touchAction.update(); - return this; - }, - /** - * 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; - }, - /** - * 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); + /** + * 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 != null && nodes[id].y != null) { + nodes[id].fixedData.x = nodes[id].xFixed; + nodes[id].fixedData.y = nodes[id].yFixed; + nodes[id].xFixed = true; + nodes[id].yFixed = true; } - }); - return this; + } + } + }, + writable: true, + configurable: true }, + _restoreFrozenNodes: { - /** - * 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); + /** + * 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 (nodes[id].fixedData.x != null) { + nodes[id].xFixed = nodes[id].fixedData.x; + nodes[id].yFixed = nodes[id].fixedData.y; + } + } } + }, + writable: true, + configurable: true + }, + stabilize: { - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { - return; + /** + * Find a stable position for all nodes + * @private + */ + value: function stabilize() { + if (this.options.stabilization.onlyDynamicEdges == true) { + this._freezeNodes(); } + this.stabilizationSteps = 0; - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; + 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++; + } - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; + 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 }); + } - /** - * 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); + if (this.options.stabilization.onlyDynamicEdges == true) { + this._restoreFrozenNodes(); + } - this.handlers = {}; - this.session = {}; - this.input.destroy(); - this.element = null; + this.body.emitter.emit("stabilizationIterationsDone"); + this.body.emitter.emit("_requestRedraw"); + }, + writable: true, + configurable: true } - }; - - /** - * add/remove the css properties as defined in manager.options.cssProps - * @param {Manager} manager - * @param {Boolean} add - */ - function toggleCssProps(manager, add) { - var element = manager.element; - each(manager.options.cssProps, function(value, name) { - element.style[prefixed(element.style, name)] = add ? value : ''; - }); - } - - /** - * trigger dom event - * @param {String} event - * @param {Object} data - */ - function triggerDomEvent(event, data) { - var gestureEvent = document.createEvent('Event'); - gestureEvent.initEvent(event, true, true); - gestureEvent.gesture = data; - data.target.dispatchEvent(gestureEvent); - } - - extend(Hammer, { - INPUT_START: INPUT_START, - INPUT_MOVE: INPUT_MOVE, - INPUT_END: INPUT_END, - INPUT_CANCEL: INPUT_CANCEL, - - STATE_POSSIBLE: STATE_POSSIBLE, - STATE_BEGAN: STATE_BEGAN, - STATE_CHANGED: STATE_CHANGED, - STATE_ENDED: STATE_ENDED, - STATE_RECOGNIZED: STATE_RECOGNIZED, - STATE_CANCELLED: STATE_CANCELLED, - STATE_FAILED: STATE_FAILED, - - DIRECTION_NONE: DIRECTION_NONE, - DIRECTION_LEFT: DIRECTION_LEFT, - DIRECTION_RIGHT: DIRECTION_RIGHT, - DIRECTION_UP: DIRECTION_UP, - DIRECTION_DOWN: DIRECTION_DOWN, - DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL, - DIRECTION_VERTICAL: DIRECTION_VERTICAL, - DIRECTION_ALL: DIRECTION_ALL, - - Manager: Manager, - Input: Input, - TouchAction: TouchAction, - - TouchInput: TouchInput, - MouseInput: MouseInput, - PointerEventInput: PointerEventInput, - TouchMouseInput: TouchMouseInput, - SingleTouchInput: SingleTouchInput, + }); - Recognizer: Recognizer, - AttrRecognizer: AttrRecognizer, - Tap: TapRecognizer, - Pan: PanRecognizer, - Swipe: SwipeRecognizer, - Pinch: PinchRecognizer, - Rotate: RotateRecognizer, - Press: PressRecognizer, + return PhysicsEngine; + })(); - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed + exports.PhysicsEngine = PhysicsEngine; + Object.defineProperty(exports, "__esModule", { + value: true }); - if ("function" == TYPE_FUNCTION && __webpack_require__(80)) { - !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { - return Hammer; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof module != 'undefined' && module.exports) { - module.exports = Hammer; - } else { - window[exportName] = Hammer; - } - - })(window, document, 'Hammer'); - - /***/ }, -/* 67 */ +/* 69 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32004,7 +31767,7 @@ return /******/ (function(modules) { // webpackBootstrap }); /***/ }, -/* 68 */ +/* 70 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32105,7 +31868,7 @@ return /******/ (function(modules) { // webpackBootstrap }); /***/ }, -/* 69 */ +/* 71 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32203,7 +31966,7 @@ return /******/ (function(modules) { // webpackBootstrap }); /***/ }, -/* 70 */ +/* 72 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32314,7 +32077,7 @@ return /******/ (function(modules) { // webpackBootstrap }); /***/ }, -/* 71 */ +/* 73 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32444,7 +32207,7 @@ return /******/ (function(modules) { // webpackBootstrap }); /***/ }, -/* 72 */ +/* 74 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32510,7 +32273,7 @@ return /******/ (function(modules) { // webpackBootstrap }); /***/ }, -/* 73 */ +/* 75 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32520,2801 +32283,3032 @@ 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 2/27/2015. + * Created by Alex on 24-Feb-15. */ - var Node = __webpack_require__(40); + var util = __webpack_require__(1); - var SelectionHandler = (function () { - function SelectionHandler(body) { - _classCallCheck(this, SelectionHandler); + var ClusterEngine = (function () { + function ClusterEngine(body) { + _classCallCheck(this, ClusterEngine); this.body = body; - this.selectionObj = { nodes: [], edges: [] }; - - this.options = { - select: true, - selectConnectedEdges: true - }; + this.clusteredNodes = {}; } - _prototypeProperties(SelectionHandler, null, { - setCanvas: { - value: function setCanvas(canvas) { - this.canvas = canvas; - }, - writable: true, - configurable: true - }, - selectOnPoint: { + _prototypeProperties(ClusterEngine, null, { + clusterByConnectionCount: { /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private - */ - value: function selectOnPoint(pointer) { - if (this.options.select === true) { - if (this._getSelectedObjectCount() > 0) { - this._unselectAll(); - } - - this.selectObject(pointer); - this._generateClickEvent(pointer); - - this.body.emitter.emit("_requestRedraw"); + * + * @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(); } - }, - writable: true, - configurable: true - }, - _generateClickEvent: { - value: function _generateClickEvent(pointer) { - var properties = this.getSelection(); - properties.pointer = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: this.canvas.DOMtoCanvas(pointer) - }; - this.body.emitter.emit("click", properties); - }, - writable: true, - configurable: true - }, - selectObject: { - value: function selectObject(pointer) { - var obj = this._getNodeAt(pointer); - if (obj != null) { - if (this.options.selectConnectedEdges === true) { - this._selectConnectedEdges(obj); + + 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); } - } else { - obj = this._getEdgeAt(pointer); } - if (obj !== null) { - obj.select(); - this._addToSelection(obj); - this.body.emitter.emit("selected", this.getSelection()); + for (var i = 0; i < nodesToCluster.length; i++) { + var node = this.body.nodes[nodesToCluster[i]]; + this.clusterByConnection(node, options, {}, {}, true); } - return obj; + this.body.emitter.emit("_dataChanged"); }, writable: true, configurable: true }, - _getNodesOverlappingWith: { + clusterByNodeData: { /** - * - * @param object - * @param overlappingNodes - * @private - */ - value: function _getNodesOverlappingWith(object, overlappingNodes) { - var nodes = this.body.nodes; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); - } + * loop over all nodes, check if they adhere to the condition and cluster if needed. + * @param options + * @param doNotUpdateCalculationNodes + */ + value: function clusterByNodeData() { + var options = arguments[0] === undefined ? {} : arguments[0]; + var doNotUpdateCalculationNodes = arguments[1] === undefined ? false : arguments[1]; + if (options.joinCondition === undefined) { + throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); + } + + // check if the options object is fine, append if needed + options = this._checkOptions(options); + + var childNodesObj = {}; + var childEdgesObj = {}; + + // 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]; } } - }, - writable: true, - configurable: true - }, - _getAllNodesOverlappingWith: { - /** - * 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 = []; - this._getNodesOverlappingWith(object, overlappingNodes); - return overlappingNodes; + this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); }, writable: true, configurable: true }, - _pointerToPositionObject: { + clusterOutliers: { /** - * 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, - top: canvasPos.y, - right: canvasPos.x, - bottom: canvasPos.y - }; - }, - writable: true, - configurable: true - }, - _getNodeAt: { + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param doNotUpdateCalculationNodes + */ + value: function clusterOutliers(options, doNotUpdateCalculationNodes) { + options = this._checkOptions(options); + var clusters = []; + // 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 }); + } + } + } - /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} 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); + for (var i = 0; i < clusters.length; i++) { + this._cluster(clusters[i].nodes, clusters[i].edges, options, true); + } - // 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 null; + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit("_dataChanged"); } }, writable: true, configurable: true }, - _getEdgesOverlappingWith: { - + clusterByConnection: { /** - * 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 edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); - } - } + * + * @param nodeId + * @param options + * @param doNotUpdateCalculationNodes + */ + value: function clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) { + // 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!"); } - }, - writable: true, - configurable: true - }, - _getAllEdgesOverlappingWith: { + var node = this.body.nodes[nodeId]; + options = this._checkOptions(options, node); + if (options.clusterNodeProperties.x === undefined) { + options.clusterNodeProperties.x = node.x;options.clusterNodeProperties.allowedToMoveX = !node.xFixed; + } + if (options.clusterNodeProperties.y === undefined) { + options.clusterNodeProperties.y = node.y;options.clusterNodeProperties.allowedToMoveY = !node.yFixed; + } - /** - * 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; + var childNodesObj = {}; + var childEdgesObj = {}; + var parentNodeId = node.id; + var parentClonedOptions = this._cloneOptions(parentNodeId); + childNodesObj[parentNodeId] = node; + + // 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); + + 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._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); }, writable: true, configurable: true }, - _getEdgeAt: { + _cloneOptions: { /** - * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call - * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. - * - * @param pointer - * @returns {null} - * @private - */ - value: function _getEdgeAt(pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); - - if (overlappingEdges.length > 0) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + * This returns a clone of the options or properties 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 { - return null; + util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); } + return clonedOptions; }, writable: true, configurable: true }, - _addToSelection: { + _createClusterEdges: { /** - * 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: { + * 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; - /** - * 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; + 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)); + } + } } }, writable: true, configurable: true }, - _removeFromSelection: { + _checkOptions: { /** - * 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]; + * 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 }, - _unselectAll: { + _cluster: { /** - * Unselect all. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - value: function _unselectAll() { - var doNotTrigger = arguments[0] === undefined ? false : arguments[0]; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } + * + * @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} doNotUpdateCalculationNodes | when true, do not wrap up + * @private + */ + value: function _cluster(childNodesObj, childEdgesObj, options) { + var doNotUpdateCalculationNodes = arguments[3] === undefined ? false : arguments[3]; + // kill condition: no children so cant cluster + if (Object.keys(childNodesObj).length == 0) { + return; } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); + + // 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); + + // 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); + } + + // get clusterproperties based on childNodes + var childEdgesOptions = []; + for (var edgeId in childEdgesObj) { + var clonedOptions = this._cloneOptions(edgeId, "edge"); + childEdgesOptions.push(clonedOptions); + } + + 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.selectionObj = { nodes: {}, edges: {} }; - if (doNotTrigger == false) { - this.body.emitter.emit("select", this.getSelection()); + // 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; } - }, - writable: true, - configurable: true - }, - _getSelectedNodeCount: { - /** - * 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; + // force the ID to remain the same + clusterNodeProperties.id = clusterId; + + + // create the clusterNode + var clusterNode = this.body.functions.createNode(clusterNodeProperties); + clusterNode.isCluster = true; + clusterNode.containedNodes = childNodesObj; + clusterNode.containedEdges = childEdgesObj; + + + // delete contained edges from global + for (var edgeId in childEdgesObj) { + if (childEdgesObj.hasOwnProperty(edgeId)) { + if (this.body.edges[edgeId] !== undefined) { + if (this.body.edges[edgeId].via !== null) { + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = null; + delete this.body.supportNodes[viaId]; + } + } + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } } } - return count; - }, - writable: true, - configurable: true - }, - _getSelectedNode: { - /** - * 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]; + + // remove contained nodes from global + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; + delete this.body.nodes[nodeId]; } } - return null; + + + // 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(); + } + + + // create bezier nodes for smooth curves if needed + this.body.emitter.emit("_newEdgesCreated"); + + + // set ID to undefined so no duplicates arise + clusterNodeProperties.id = undefined; + + + // wrap up + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit("_dataChanged"); + } }, writable: true, configurable: true }, - _getSelectedEdge: { + isCluster: { + /** - * 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]; - } + * 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; + } else { + console.log("Node does not exist."); + return false; } - return null; }, writable: true, configurable: true }, - _getSelectedEdgeCount: { - + _getClusterPosition: { /** - * 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; - } + * 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 count; + return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; }, writable: true, configurable: true }, - _getSelectedObjectCount: { + openCluster: { /** - * 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; - } + * Open a cluster by calling this function. + * @param {String} clusterNodeId | the ID of the cluster node + * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true + */ + value: function openCluster(clusterNodeId, doNotUpdateCalculationNodes) { + // kill conditions + if (clusterNodeId === undefined) { + throw new Error("No clusterNodeId supplied to openCluster."); } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } + if (this.body.nodes[clusterNodeId] === undefined) { + throw new Error("The clusterNodeId supplied to openCluster does not exist."); } - return count; - }, - writable: true, - configurable: true - }, - _selectionIsEmpty: { + if (this.body.nodes[clusterNodeId].containedNodes === undefined) { + console.log("The node:" + clusterNodeId + " is not a cluster.");return; + }; - /** - * 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; + var node = this.body.nodes[clusterNodeId]; + var containedNodes = node.containedNodes; + var containedEdges = node.containedEdges; + + // release nodes + for (var nodeId in containedNodes) { + if (containedNodes.hasOwnProperty(nodeId)) { + this.body.nodes[nodeId] = containedNodes[nodeId]; + // inherit position + this.body.nodes[nodeId].x = node.x; + this.body.nodes[nodeId].y = node.y; + + // inherit speed + this.body.nodes[nodeId].vx = node.vx; + this.body.nodes[nodeId].vy = node.vy; + + delete this.clusteredNodes[nodeId]; } } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; + + // release edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + this.body.edges[edgeId] = containedEdges[edgeId]; + this.body.edges[edgeId].connect(); + var edge = this.body.edges[edgeId]; + if (edge.connected === false) { + if (this.clusteredNodes[edge.fromId] !== undefined) { + this._connectEdge(edge, edge.fromId, true); + } + if (this.clusteredNodes[edge.toId] !== undefined) { + this._connectEdge(edge, edge.toId, false); + } + } } } - return true; - }, - writable: true, - configurable: true - }, - _clusterInSelection: { + this.body.emitter.emit("_newEdgesCreated", containedEdges); - /** - * 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; + + var edgeIds = []; + for (var i = 0; i < node.edges.length; i++) { + edgeIds.push(node.edges[i].id); + } + + // remove edges in clusterNode + for (var i = 0; i < edgeIds.length; i++) { + var edge = this.body.edges[edgeIds[i]]; + // if the edge should have been connected to a contained node + if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) { + // the node in the from array was contained in the cluster + if (this.body.nodes[edge.fromArray[0].id] !== undefined) { + this._connectEdge(edge, edge.fromArray[0].id, true); + } + } else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) { + // the node in the to array was contained in the cluster + if (this.body.nodes[edge.toArray[0].id] !== undefined) { + this._connectEdge(edge, edge.toArray[0].id, false); } + } else { + var edgeId = edgeIds[i]; + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = null; + delete this.body.supportNodes[viaId]; + } + // this removes the edge from node.edges, which is why edgeIds is formed + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; } } - return false; + + // remove clusterNode + delete this.body.nodes[clusterNodeId]; + + if (doNotUpdateCalculationNodes !== true) { + this.body.emitter.emit("_dataChanged"); + } }, writable: true, configurable: true }, - _selectConnectedEdges: { + _connectEdge: { + + /** - * 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); + * 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 }, - _hoverConnectedEdges: { + _getClusterStack: { /** - * 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); + * 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; }, writable: true, configurable: true }, - _unselectConnectedEdges: { + _getConnectedId: { /** - * 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); + * 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 }, - _blurObject: { - + _getHubSize: { + /** + * 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; + var variance = averageSquared - Math.pow(average, 2); + var standardDeviation = Math.sqrt(variance); + var hubThreshold = Math.floor(average + 2 * standardDeviation); - /** - * 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 }); + // always have at least one to cluster + if (hubThreshold > largestHub) { + hubThreshold = largestHub; } - }, - writable: true, - configurable: true - }, - _hoverObject: { - /** - * 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); - } + return hubThreshold; }, writable: true, configurable: true - }, - _handleDoubleTap: { + } + }); + + return ClusterEngine; + })(); + + exports.ClusterEngine = ClusterEngine; + Object.defineProperty(exports, "__esModule", { + value: true + }); +/***/ }, +/* 76 */ +/***/ 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); }; - /** - * handles the selection part of the double tap and opens a cluster if needed - * - * @param {Object} pointer - * @private - */ - value: function _handleDoubleTap(pointer) { - var node = this._getNodeAt(pointer); - if (node != null && node !== undefined) { - // we reset the areaCenter here so the opening of the node will occur - this.areaCenter = { x: this._XconvertDOMtoCanvas(pointer.x), - y: this._YconvertDOMtoCanvas(pointer.y) }; - this.openCluster(node); + 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. + */ + + if (typeof window !== "undefined") { + window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + } + + var util = __webpack_require__(1); + + + var CanvasRenderer = (function () { + function CanvasRenderer(body, canvas) { + var _this = this; + _classCallCheck(this, CanvasRenderer); + + this.body = body; + this.canvas = canvas; + + this.redrawRequested = false; + this.renderTimer = false; + this.requiresTimeout = true; + this.continueRendering = false; + this.renderRequests = 0; + + this.canvasTopLeft = { x: 0, y: 0 }; + this.canvasBottomRight = { x: 0, y: 0 }; + + this.dragging = false; + + this.body.emitter.on("dragStart", function () { + _this.dragging = true;console.log("here"); + }); + this.body.emitter.on("dragEnd", function () { + return _this.dragging = false; + }); + this.body.emitter.on("_redraw", this._redraw.bind(this)); + this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); + this.body.emitter.on("_startRendering", function () { + _this.renderRequests += 1;_this.continueRendering = true;_this.startRendering(); + }); + this.body.emitter.on("_stopRendering", function () { + _this.renderRequests -= 1;_this.continueRendering = _this.renderRequests > 0; + }); + + 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); } - var properties = this.getSelection(); - properties.pointer = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } - }; - this.body.emitter.emit("doubleClick", properties); }, writable: true, configurable: true }, - _handleOnHold: { - - - /** - * Handle the onHold selection part - * - * @param pointer - * @private - */ - value: function _handleOnHold(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node, true); - } else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge, true); + startRendering: { + value: function startRendering() { + if (this.continueRendering === 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 + } } - } - this._requestRedraw(); + } else {} }, writable: true, configurable: true }, - getSelection: { + 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(); + } + this._redraw(); - /** - * - * 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 }; + if (this.requiresTimeout == false) { + // this schedules a new simulation step + this.startRendering(); + } }, writable: true, configurable: true }, - getSelectedNodes: { + redraw: { /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. + * Redraw the network with the current data + * chart will be resized too. */ - 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; + value: function redraw() { + this.setSize(this.constants.width, this.constants.height); + this._redraw(); }, writable: true, configurable: true }, - getSelectedEdges: { + _requestRedraw: { /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. + * 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 getSelectedEdges() { - var idArray = []; - if (this.options.select == true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } + value: function _requestRedraw() { + if (this.redrawRequested !== true) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, false), 0); + } else { + window.requestAnimationFrame(this._redraw.bind(this, false)); } } - return idArray; }, writable: true, configurable: true }, - selectNodes: { + _redraw: { + value: function _redraw() { + var hidden = arguments[0] === undefined ? false : arguments[0]; + this.body.emitter.emit("_beforeRender"); + this.redrawRequested = false; + var ctx = this.canvas.frame.canvas.getContext("2d"); - /** - * 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; + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + // clear the canvas + var w = this.canvas.frame.canvas.clientWidth; + var h = this.canvas.frame.canvas.clientHeight; + ctx.clearRect(0, 0, w, h); - // first unselect any selected node - this._unselectAll(true); + this.body.emitter.emit("beforeDrawing", ctx); - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + // 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); - var node = this.body.nodes[id]; - if (!node) { - throw new RangeError("Node with id \"" + id + "\" not found"); + 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 }); + + console.log(this.dragging); + + if (hidden === false) { + if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { + this._drawEdges(ctx); } - this._selectObject(node, true, true, highlightEdges, true); } - this.redraw(); + + if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { + this._drawNodes(ctx, this.body.nodes, hidden); + } + + if (hidden === false) { + if (this.controlNodesActive == true) { + this._drawControlNodes(ctx); + } + } + + //this._drawNodes(ctx,this.body.supportNodes,true); + // this.physics.nodesSolver._debug(ctx,"#F00F0F"); + + // restore original scaling and translation + ctx.restore(); + + if (hidden === true) { + ctx.clearRect(0, 0, w, h); + } + + this.body.emitter.emit("afterDrawing", ctx); }, writable: true, configurable: true }, - selectEdges: { + _drawNodes: { /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. + * 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 selectEdges(selection) { - var i, iMax, id; - - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - - // first unselect any selected node - this._unselectAll(true); + value: function _drawNodes(ctx, nodes) { + var alwaysShow = arguments[2] === undefined ? false : arguments[2]; + // first draw the unselected nodes + var selected = []; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].setScaleAndPos(this.body.view.scale, this.canvasTopLeft, this.canvasBottomRight); + if (nodes[id].isSelected()) { + selected.push(id); + } else { + if (alwaysShow === true) { + nodes[id].draw(ctx); + } else if (nodes[id].inArea() === true) { + nodes[id].draw(ctx); + } + } + } + } - var edge = this.body.edges[id]; - if (!edge) { - throw new RangeError("Edge with id \"" + id + "\" not found"); + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + if (nodes[selected[s]].inArea() || alwaysShow) { + nodes[selected[s]].draw(ctx); } - this._selectObject(edge, true, true, false, true); } - this.redraw(); }, writable: true, configurable: true }, - _updateSelection: { + _drawEdges: { + /** - * Validate the selection: remove ids of nodes which no longer exist + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx * @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]; + value: function _drawEdges(ctx) { + var edges = this.body.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.setScale(this.body.view.scale); + if (edge.connected === true) { + edges[id].draw(ctx); } } } }, writable: true, configurable: true - } - }); - - return SelectionHandler; - })(); - - exports.SelectionHandler = SelectionHandler; - Object.defineProperty(exports, "__esModule", { - value: true - }); - -/***/ }, -/* 74 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var Node = __webpack_require__(40); - - /** - * - * @param object - * @param overlappingNodes - * @private - */ - exports._getNodesOverlappingWith = function (object, overlappingNodes) { - var nodes = this.body.nodes; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); - } - } - } - }; - - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - exports._getAllNodesOverlappingWith = function (object) { - var overlappingNodes = []; - this._getNodesOverlappingWith(object, overlappingNodes); - return overlappingNodes; - }; - - - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - exports._pointerToPositionObject = function (pointer) { - var x = this._XconvertDOMtoCanvas(pointer.x); - var y = this._YconvertDOMtoCanvas(pointer.y); - - return { - left: x, - top: y, - right: x, - bottom: y - }; - }; - - - /** - * Get the top node at the a specific point (like a click) - * - * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node - * @private - */ - exports._getNodeAt = function (pointer) { - // we first check if this is an navigation controls element - var positionObject = this._pointerToPositionObject(pointer); - var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - if (overlappingNodes.length > 0) { - return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; - } else { - return null; - } - }; + }, + _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; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + edges[id]._drawControlNodes(ctx); + } + } + }, + writable: true, + configurable: true + }, + _determineBrowserMethod: { - /** - * 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 - */ - exports._getEdgesOverlappingWith = function (object, overlappingEdges) { - var edges = this.body.edges; - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); - } + /** + * 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 CanvasRenderer; + })(); - /** - * 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 - */ - exports._getAllEdgesOverlappingWith = function (object) { - var overlappingEdges = []; - this._getEdgesOverlappingWith(object, overlappingEdges); - return overlappingEdges; - }; + exports.CanvasRenderer = CanvasRenderer; + Object.defineProperty(exports, "__esModule", { + value: true + }); - /** - * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call - * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. - * - * @param pointer - * @returns {null} - * @private - */ - exports._getEdgeAt = function (pointer) { - var positionObject = this._pointerToPositionObject(pointer); - var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); +/***/ }, +/* 77 */ +/***/ function(module, exports, __webpack_require__) { - if (overlappingEdges.length > 0) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; - } else { - return null; - } - }; + "use strict"; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - exports._addToSelection = function (obj) { - if (obj instanceof Node) { - this.selectionObj.nodes[obj.id] = obj; - } else { - this.selectionObj.edges[obj.id] = obj; - } - }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - /** - * Add object to the selection array. - * - * @param obj - * @private - */ - exports._addToHover = function (obj) { - if (obj instanceof Node) { - this.hoverObj.nodes[obj.id] = obj; - } else { - this.hoverObj.edges[obj.id] = obj; - } - }; + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + var util = __webpack_require__(1); /** - * Remove a single option from selection. - * - * @param {Object} obj + * 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 */ - exports._removeFromSelection = function (obj) { - if (obj instanceof Node) { - delete this.selectionObj.nodes[obj.id]; - } else { - delete this.selectionObj.edges[obj.id]; - } - }; + var Canvas = (function () { + function Canvas(body) { + var _this = this; + _classCallCheck(this, Canvas); - /** - * Unselect all. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._unselectAll = function (doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); - } - } + this.body = body; - this.selectionObj = { nodes: {}, edges: {} }; + this.options = {}; + this.defaultOptions = { + width: "100%", + height: "100%" + }; + util.extend(this.options, this.defaultOptions); - if (doNotTrigger == false) { - this.emit("select", this.getSelection()); - } - }; + 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; + }); - /** - * Unselect all clusters. The selectionObj is useful for this. - * - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._unselectClusters = function (doNotTrigger) { - if (doNotTrigger === undefined) { - doNotTrigger = false; + this.pixelRatio = 1; } - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - this.selectionObj.nodes[nodeId].unselect(); - this._removeFromSelection(this.selectionObj.nodes[nodeId]); - } - } - } + _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); + } + + 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.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); + } else { + var ctx = this.frame.canvas.getContext("2d"); + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - if (doNotTrigger == false) { - this.emit("select", this.getSelection()); - } - }; + //this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. + this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + } + // add the frame to the container element + this.body.container.appendChild(this.frame); - /** - * return the number of selected nodes - * - * @returns {number} - * @private - */ - exports._getSelectedNodeCount = function () { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - return count; - }; + this.body.view.scale = 1; + this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; - /** - * return the selected node - * - * @returns {number} - * @private - */ - exports._getSelectedNode = function () { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return this.selectionObj.nodes[nodeId]; - } - } - return null; - }; + this._bindHammer(); + }, + writable: true, + configurable: true + }, + _bindHammer: { - /** - * return the selected edge - * - * @returns {number} - * @private - */ - exports._getSelectedEdge = function () { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return this.selectionObj.edges[edgeId]; - } - } - return null; - }; + /** + * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * @private + */ + value: function _bindHammer() { + var me = this; + if (this.hammer !== undefined) { + this.hammer.destroy(); + } + this.drag = {}; + this.pinch = {}; + this.hammer = new Hammer(this.frame.canvas); + this.hammer.on("tap", me.body.eventListeners.onTap); + this.hammer.on("doubletap", me.body.eventListeners.onDoubleTap); + this.hammer.on("press", me.body.eventListeners.onHold); + hammerUtil.onTouch(this.hammer, me.body.eventListeners.onTouch); + this.hammer.on("panstart", me.body.eventListeners.onDragStart); + this.hammer.on("panmove", me.body.eventListeners.onDrag); + this.hammer.on("panend", me.body.eventListeners.onDragEnd); + this.hammer.on("pinch", me.body.eventListeners.onPinch.bind(me)); - /** - * return the number of selected edges - * - * @returns {number} - * @private - */ - exports._getSelectedEdgeCount = function () { - var count = 0; - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } - } - return count; - }; + // 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", me.body.eventListeners.onMouseWheel.bind(me)); + this.frame.canvas.addEventListener("DOMMouseScroll", me.body.eventListeners.onMouseWheel.bind(me)); + this.frame.canvas.addEventListener("mousemove", me.body.eventListeners.onMouseMove.bind(me)); - /** - * return the number of selected objects. - * - * @returns {number} - * @private - */ - exports._getSelectedObjectCount = function () { - var count = 0; - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - count += 1; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - count += 1; - } - } - return count; - }; + this.hammerFrame = new Hammer(this.frame); + hammerUtil.onRelease(this.hammerFrame, me.body.eventListeners.onRelease.bind(me)); + }, + writable: true, + configurable: true + }, + setSize: { - /** - * Check if anything is selected - * - * @returns {boolean} - * @private - */ - exports._selectionIsEmpty = function () { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - return false; - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - return false; - } - } - return true; - }; + /** + * 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; - /** - * check if one of the selected nodes is a cluster. - * - * @returns {boolean} - * @private - */ - exports._clusterInSelection = function () { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (this.selectionObj.nodes[nodeId].clusterSize > 1) { - return true; - } - } - } - return false; - }; + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._selectConnectedEdges = function (node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.select(); - this._addToSelection(edge); - } - }; + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - /** - * select the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._hoverConnectedEdges = function (node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.hover = true; - this._addToHover(edge); - } - }; + 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. - /** - * unselect the edges connected to the node that is being selected - * - * @param {Node} node - * @private - */ - exports._unselectConnectedEdges = function (node) { - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - edge.unselect(); - this._removeFromSelection(edge); - } - }; + 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; + } + } + 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: { + /** + * 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: { - /** - * This is called when someone clicks on a node. either select or deselect it. - * If there is an existing selection and we don't want to append to it, clear the existing selection - * - * @param {Node || Edge} object - * @param {Boolean} append - * @param {Boolean} [doNotTrigger] | ignore trigger - * @private - */ - exports._selectObject = function (object, append, doNotTrigger, highlightEdges, overrideSelectable) { - if (doNotTrigger === undefined) { - doNotTrigger = false; - } - if (highlightEdges === undefined) { - highlightEdges = true; - } + /** + * 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: { - if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { - this._unselectAll(true); - } + /** + * 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: { - // selectable allows the object to be selected. Override can be used if needed to bypass this. - if (object.selected == false && (this.constants.selectable == true || overrideSelectable)) { - object.select(); - this._addToSelection(object); - if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { - this._selectConnectedEdges(object); - } - } - // do not select the object if selectable is false, only add it to selection to allow drag to work - else if (object.selected == false) { - this._addToSelection(object); - doNotTrigger = true; - } else { - object.unselect(); - this._removeFromSelection(object); - } + /** + * 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: { - if (doNotTrigger == false) { - this.emit("select", this.getSelection()); - } - }; + /** + * + * @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: { + + /** + * + * @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 Canvas; + })(); - /** - * 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 - */ - exports._blurObject = function (object) { - if (object.hover == true) { - object.hover = false; - this.emit("blurNode", { node: object.id }); - } - }; + exports.Canvas = Canvas; + Object.defineProperty(exports, "__esModule", { + value: true + }); - /** - * 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 - */ - exports._hoverObject = function (object) { - if (object.hover == false) { - object.hover = true; - this._addToHover(object); - if (object instanceof Node) { - this.emit("hoverNode", { node: object.id }); - } - } - if (object instanceof Node) { - this._hoverConnectedEdges(object); - } - }; +/***/ }, +/* 78 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; - /** - * handles the selection part of the touch, only for navigation controls elements; - * Touch is triggered before tap, also before hold. Hold triggers after a while. - * This is the most responsive solution - * - * @param {Object} pointer - * @private - */ - exports._handleTouch = function (pointer) {}; + 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"); } }; /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private + * Created by Alex on 26-Feb-15. */ - exports._handleTap = function (pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node, false); - } else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge, false); - } else { - this._unselectAll(); - } - } - var properties = this.getSelection(); - properties.pointer = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } - }; - this.emit("click", properties); - this._requestRedraw(); - }; + var util = __webpack_require__(1); - /** - * handles the selection part of the double tap and opens a cluster if needed - * - * @param {Object} pointer - * @private - */ - exports._handleDoubleTap = function (pointer) { - var node = this._getNodeAt(pointer); - if (node != null && node !== undefined) { - // we reset the areaCenter here so the opening of the node will occur - this.areaCenter = { x: this._XconvertDOMtoCanvas(pointer.x), - y: this._YconvertDOMtoCanvas(pointer.y) }; - this.openCluster(node); - } - var properties = this.getSelection(); - properties.pointer = { - DOM: { x: pointer.x, y: pointer.y }, - canvas: { x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y) } - }; - this.emit("doubleClick", properties); - }; + var View = (function () { + function View(body, canvas) { + var _this = this; + _classCallCheck(this, View); + this.body = body; + this.canvas = canvas; - /** - * Handle the onHold selection part - * - * @param pointer - * @private - */ - exports._handleOnHold = function (pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node, true); - } else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge, true); - } - } - this._requestRedraw(); - }; + 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 = null; + this.lockedOnNodeOffset = null; + this.touchTime = 0; + this.viewFunction = undefined; - /** - * handle the onRelease event. These functions are here for the navigation controls module - * and data manipulation module. - * - * @private - */ - exports._handleOnRelease = function (pointer) { - this._manipulationReleaseOverload(pointer); - this._navigationReleaseOverload(pointer); - }; + 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)); + } - exports._manipulationReleaseOverload = function (pointer) {}; - exports._navigationReleaseOverload = function (pointer) {}; + _prototypeProperties(View, null, { + setOptions: { + value: function setOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; + this.options = options; + }, + writable: true, + configurable: true + }, + _getRange: { - /** - * - * retrieve the currently selected objects - * @return {{nodes: Array., edges: Array.}} selection - */ - exports.getSelection = function () { - var nodeIds = this.getSelectedNodes(); - var edgeIds = this.getSelectedEdges(); - return { nodes: nodeIds, edges: edgeIds }; - }; - /** - * - * retrieve the currently selected nodes - * @return {String[]} selection An array with the ids of the - * selected nodes. - */ - exports.getSelectedNodes = function () { - var idArray = []; - if (this.constants.selectable == true) { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - idArray.push(nodeId); - } - } - } - return idArray; - }; + // 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.boundingBox.left) { + minX = node.boundingBox.left; + } + if (maxX < node.boundingBox.right) { + maxX = node.boundingBox.right; + } + if (minY > node.boundingBox.bottom) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.boundingBox.top) { + maxY = node.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.boundingBox.left) { + minX = node.boundingBox.left; + } + if (maxX < node.boundingBox.right) { + maxX = node.boundingBox.right; + } + if (minY > node.boundingBox.bottom) { + minY = node.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.boundingBox.top) { + maxY = node.boundingBox.bottom; + } // top is negative, bottom is positive + } + } + } - /** - * - * retrieve the currently selected edges - * @return {Array} selection An array with the ids of the - * selected nodes. - */ - exports.getSelectedEdges = function () { - var idArray = []; - if (this.constants.selectable == true) { - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - idArray.push(edgeId); - } - } - } - return idArray; - }; + 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: { - /** - * select zero or more nodes DEPRICATED - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - exports.setSelection = function () { - console.log("setSelection is deprecated. Please use selectNodes instead."); - }; + /** + * @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: { - /** - * 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] - */ - exports.selectNodes = function (selection, highlightEdges) { - var i, iMax, id; + /** + * 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; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + 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; + } - // first unselect any selected node - this._unselectAll(true); + range = this._getRange(options.nodes); - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var numberOfNodes = this.body.nodeIndices.length; + if (this.options.smoothCurves == true) { + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } else { + zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } - 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(); - }; + // 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; + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + } - /** - * select zero or more edges - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ - exports.selectEdges = function (selection) { - var i, iMax, id; + if (zoomLevel > 1) { + zoomLevel = 1; + } - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - // first unselect any selected node - this._unselectAll(true); + var center = this._findCenter(range); + var animationOptions = { position: center, scale: zoomLevel, animation: options }; + this.moveTo(animationOptions); + }, + writable: true, + configurable: true + }, + focusOnNode: { - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + // animation - 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(); - }; + /** + * 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; - /** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ - exports._updateSelection = function () { - 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]; - } - } - } - }; + this.moveTo(options); + } else { + console.log("Node: " + nodeId + " cannot be found."); + } + }, + writable: true, + configurable: true + }, + moveTo: { -/***/ }, -/* 75 */ -/***/ function(module, exports, __webpack_require__) { + /** + * + * @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 - "use strict"; + this.animateView(options); + }, + writable: true, + configurable: true + }, + animateView: { - var util = __webpack_require__(1); - var Node = __webpack_require__(40); - var Edge = __webpack_require__(37); - var Hammer = __webpack_require__(45); + /** + * + * @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; + } - /** - * clears the toolbar div element of children - * - * @private - */ - exports._clearManipulatorBar = function () { - this._recursiveDOMDelete(this.manipulationDiv); - this.manipulationDOM = {}; + // 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._cleanManipulatorHammers(); + this.sourceScale = this.body.view.scale; + this.sourceTranslation = this.body.view.translation; + this.targetScale = options.scale; - this._manipulationReleaseOverload = function () {}; - delete this.body.sectors.support.nodes.targetNode; - delete this.body.sectors.support.nodes.targetViaNode; - this.controlNodesActive = false; - this.freezeSimulationEnabled = false; - }; + // 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 != null) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("_beforeRender", 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; - exports._cleanManipulatorHammers = function () { - // clean hammer bindings - if (this.manipulationHammers.length != 0) { - for (var i = 0; i < this.manipulationHammers.length; i++) { - this.manipulationHammers[i].dispose(); - } - this.manipulationHammers = []; - } - }; - /** - * 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 - */ - exports._restoreOverloadedFunctions = function () { - for (var functionName in this.cachedFunctions) { - if (this.cachedFunctions.hasOwnProperty(functionName)) { - this[functionName] = this.cachedFunctions[functionName]; - delete this.cachedFunctions[functionName]; - } - } - }; + this.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + }, + writable: true, + configurable: true + }, + _lockedRedraw: { - /** - * Enable or disable edit-mode. - * - * @private - */ - exports._toggleEditMode = function () { - 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"); - } else { - toolbar.style.display = "none"; - closeDiv.style.display = "none"; - editModeDiv.style.display = "block"; - } - this._createManipulatorBar(); - }; + /** + * 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 + }; - /** - * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private - */ - exports._createManipulatorBar = function () { - // remove bound functions - if (this.boundFunction) { - this.off("select", this.boundFunction); - } + this.body.view.translation = targetTranslation; + }, + writable: true, + configurable: true + }, + releaseNode: { + value: function releaseNode() { + if (this.lockedOnNodeId !== undefined) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + } + }, + writable: true, + configurable: true + }, + _transitionRedraw: { - this._cleanManipulatorHammers(); + /** + * + * @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 locale = this.constants.locales[this.constants.locale]; + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - if (this.edgeBeingEdited !== undefined) { - this.edgeBeingEdited._disableControlNodes(); - this.edgeBeingEdited = undefined; - this.selectedControlNode = null; - this.controlNodesActive = false; - this._redraw(); - } + 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 + }; - // restore overloaded functions - this._restoreOverloadedFunctions(); + // cleanup + if (this.easingTime >= 1) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.easingTime = 0; + if (this.lockedOnNodeId != null) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("_beforeRender", this.viewFunction); + } + this.body.emitter.emit("animationFinished"); + } + }, + writable: true, + configurable: true + } + }); - // resume calculation - this.freezeSimulation(false); + return View; + })(); - // reset global variables - this.blockConnectingEdgeSelection = false; - this.forceAppendSelection = false; - this.manipulationDOM = {}; + exports.View = View; + Object.defineProperty(exports, "__esModule", { + value: true + }); - if (this.editMode == true) { - while (this.manipulationDiv.hasChildNodes()) { - this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); - } +/***/ }, +/* 79 */ +/***/ function(module, exports, __webpack_require__) { - this.manipulationDOM.addNodeSpan = document.createElement("div"); - this.manipulationDOM.addNodeSpan.className = "network-manipulationUI add"; + "use strict"; - this.manipulationDOM.addNodeLabelSpan = document.createElement("div"); - this.manipulationDOM.addNodeLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.addNodeLabelSpan.innerHTML = locale.addNode; - this.manipulationDOM.addNodeSpan.appendChild(this.manipulationDOM.addNodeLabelSpan); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); - this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - this.manipulationDOM.addEdgeSpan = document.createElement("div"); - this.manipulationDOM.addEdgeSpan.className = "network-manipulationUI connect"; - this.manipulationDOM.addEdgeLabelSpan = document.createElement("div"); - this.manipulationDOM.addEdgeLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.addEdgeLabelSpan.innerHTML = locale.addEdge; - this.manipulationDOM.addEdgeSpan.appendChild(this.manipulationDOM.addEdgeLabelSpan); + /** + * Created by Alex on 2/27/2015. + */ - this.manipulationDiv.appendChild(this.manipulationDOM.addNodeSpan); - this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); - this.manipulationDiv.appendChild(this.manipulationDOM.addEdgeSpan); + var Node = __webpack_require__(55); + var util = __webpack_require__(1); - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - this.manipulationDOM.seperatorLineDiv2 = document.createElement("div"); - this.manipulationDOM.seperatorLineDiv2.className = "network-seperatorLine"; + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { + _classCallCheck(this, SelectionHandler); - this.manipulationDOM.editNodeSpan = document.createElement("div"); - this.manipulationDOM.editNodeSpan.className = "network-manipulationUI edit"; - this.manipulationDOM.editNodeLabelSpan = document.createElement("div"); - this.manipulationDOM.editNodeLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.editNodeLabelSpan.innerHTML = locale.editNode; - this.manipulationDOM.editNodeSpan.appendChild(this.manipulationDOM.editNodeLabelSpan); + this.body = body; + this.canvas = canvas; + this.selectionObj = { nodes: [], edges: [] }; - this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv2); - this.manipulationDiv.appendChild(this.manipulationDOM.editNodeSpan); - } else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - this.manipulationDOM.seperatorLineDiv3 = document.createElement("div"); - this.manipulationDOM.seperatorLineDiv3.className = "network-seperatorLine"; + this.options = {}; + this.defaultOptions = { + select: true, + selectConnectedEdges: true + }; + util.extend(this.options, this.defaultOptions); + } - this.manipulationDOM.editEdgeSpan = document.createElement("div"); - this.manipulationDOM.editEdgeSpan.className = "network-manipulationUI edit"; - this.manipulationDOM.editEdgeLabelSpan = document.createElement("div"); - this.manipulationDOM.editEdgeLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.editEdgeLabelSpan.innerHTML = locale.editEdge; - this.manipulationDOM.editEdgeSpan.appendChild(this.manipulationDOM.editEdgeLabelSpan); + _prototypeProperties(SelectionHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + }, + writable: true, + configurable: true + }, + selectOnPoint: { - this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv3); - this.manipulationDiv.appendChild(this.manipulationDOM.editEdgeSpan); - } - if (this._selectionIsEmpty() == false) { - this.manipulationDOM.seperatorLineDiv4 = document.createElement("div"); - this.manipulationDOM.seperatorLineDiv4.className = "network-seperatorLine"; - this.manipulationDOM.deleteSpan = document.createElement("div"); - this.manipulationDOM.deleteSpan.className = "network-manipulationUI delete"; - this.manipulationDOM.deleteLabelSpan = document.createElement("div"); - this.manipulationDOM.deleteLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.deleteLabelSpan.innerHTML = locale.del; - this.manipulationDOM.deleteSpan.appendChild(this.manipulationDOM.deleteLabelSpan); - this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv4); - this.manipulationDiv.appendChild(this.manipulationDOM.deleteSpan); - } + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + var selected = false; + if (this.options.select === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - // bind the icons - this._bindHammerToDiv(this.manipulationDOM.addNodeSpan, "_createAddNodeToolbar"); - this._bindHammerToDiv(this.manipulationDOM.addEdgeSpan, "_createAddEdgeToolbar"); - this._bindHammerToDiv(this.closeDiv, "_toggleEditMode"); + // deselect + if (obj === undefined) { + this.unselectAll(); + } else { + selected = this.selectObject(obj); + } - if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { - this._bindHammerToDiv(this.manipulationDOM.editNodeSpan, "_editNode"); - } else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { - this._bindHammerToDiv(this.manipulationDOM.editEdgeSpan, "_createEditEdgeToolbar"); - } - if (this._selectionIsEmpty() == false) { - this._bindHammerToDiv(this.manipulationDOM.deleteSpan, "_deleteSelected"); - } + 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);; - var me = this; - this.boundFunction = me._createManipulatorBar; - this.on("select", this.boundFunction); - } else { - while (this.editModeDiv.hasChildNodes()) { - this.editModeDiv.removeChild(this.editModeDiv.firstChild); - } + if (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); + } else { + this.selectObject(obj); + } - this.manipulationDOM.editModeSpan = document.createElement("div"); - this.manipulationDOM.editModeSpan.className = "network-manipulationUI edit editmode"; - this.manipulationDOM.editModeLabelSpan = document.createElement("div"); - this.manipulationDOM.editModeLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.editModeLabelSpan.innerHTML = locale.edit; - this.manipulationDOM.editModeSpan.appendChild(this.manipulationDOM.editModeLabelSpan); + 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._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: { - this.editModeDiv.appendChild(this.manipulationDOM.editModeSpan); - this._bindHammerToDiv(this.manipulationDOM.editModeSpan, "_toggleEditMode"); - } - }; + + /** + * 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 nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + } + return overlappingNodes; + }, + writable: true, + configurable: true + }, + _pointerToPositionObject: { - exports._bindHammerToDiv = function (domElement, funct) { - var hammer = Hammer(domElement, { prevent_default: true }); - hammer.on("touch", this[funct].bind(this)); - this.manipulationHammers.push(hammer); - }; + /** + * 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, + top: canvasPos.y, + right: canvasPos.x, + bottom: canvasPos.y + }; + }, + writable: true, + configurable: true + }, + getNodeAt: { - /** - * Create the toolbar for adding Nodes - * - * @private - */ - exports._createAddNodeToolbar = function () { - // clear the toolbar - this._clearManipulatorBar(); - if (this.boundFunction) { - this.off("select", this.boundFunction); - } + /** + * 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); - var locale = this.constants.locales[this.constants.locale]; + // 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.manipulationDOM = {}; - this.manipulationDOM.backSpan = document.createElement("div"); - this.manipulationDOM.backSpan.className = "network-manipulationUI back"; - this.manipulationDOM.backLabelSpan = document.createElement("div"); - this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.backLabelSpan.innerHTML = locale.back; - this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); - this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); - this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; + /** + * 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 edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + } + }, + writable: true, + configurable: true + }, + _getAllEdgesOverlappingWith: { - this.manipulationDOM.descriptionSpan = document.createElement("div"); - this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; - this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); - this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.addDescription; - this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); - this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); - this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); - this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); + /** + * 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: { - // bind the icon - this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - var me = this; - this.boundFunction = me._addNode; - this.on("select", this.boundFunction); - }; + /** + * 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); + if (overlappingEdges.length > 0) { + return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; + } else { + return undefined; + } + }, + writable: true, + configurable: true + }, + _addToSelection: { - /** - * create the toolbar to connect nodes - * - * @private - */ - exports._createAddEdgeToolbar = function () { - // clear the toolbar - this._clearManipulatorBar(); - this._unselectAll(true); - this.freezeSimulationEnabled = true; - if (this.boundFunction) { - this.off("select", this.boundFunction); - } + /** + * 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: { - var locale = this.constants.locales[this.constants.locale]; + /** + * 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: { - this.forceAppendSelection = false; - this.blockConnectingEdgeSelection = true; - this.manipulationDOM = {}; - this.manipulationDOM.backSpan = document.createElement("div"); - this.manipulationDOM.backSpan.className = "network-manipulationUI back"; - this.manipulationDOM.backLabelSpan = document.createElement("div"); - this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.backLabelSpan.innerHTML = locale.back; - this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); + /** + * 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: { - this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); - this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; + /** + * 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.manipulationDOM.descriptionSpan = document.createElement("div"); - this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; - this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); - this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.edgeDescription; - this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); + this.selectionObj = { nodes: {}, edges: {} }; + }, + writable: true, + configurable: true + }, + _getSelectedNodeCount: { - this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); - this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); - this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); - // bind the icon - this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); + /** + * 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: { - // we use the boundFunction so we can reference it when we unbind it from the "select" event. - var me = this; - this.boundFunction = me._handleConnect; - this.on("select", this.boundFunction); + /** + * 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: { - // temporarily overload functions - this.cachedFunctions._handleTouch = this._handleTouch; - this.cachedFunctions._manipulationReleaseOverload = this._manipulationReleaseOverload; - this.cachedFunctions._handleDragStart = this._handleDragStart; - this.cachedFunctions._handleDragEnd = this._handleDragEnd; - this.cachedFunctions._handleOnHold = this._handleOnHold; - this._handleTouch = this._handleConnect; - this._manipulationReleaseOverload = function () {}; - this._handleOnHold = function () {}; - this._handleDragStart = function () {}; - this._handleDragEnd = this._finishConnect; + /** + * 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: { - // redraw to show the unselect - this._redraw(); - }; - /** - * create the toolbar to edit edges - * - * @private - */ - exports._createEditEdgeToolbar = function () { - // clear the toolbar - this._clearManipulatorBar(); - this.controlNodesActive = true; + /** + * 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: { - if (this.boundFunction) { - this.off("select", this.boundFunction); - } - this.edgeBeingEdited = this._getSelectedEdge(); - this.edgeBeingEdited._enableControlNodes(); + /** + * 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: { - var locale = this.constants.locales[this.constants.locale]; + /** + * 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: { - this.manipulationDOM = {}; - this.manipulationDOM.backSpan = document.createElement("div"); - this.manipulationDOM.backSpan.className = "network-manipulationUI back"; - this.manipulationDOM.backLabelSpan = document.createElement("div"); - this.manipulationDOM.backLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.backLabelSpan.innerHTML = locale.back; - this.manipulationDOM.backSpan.appendChild(this.manipulationDOM.backLabelSpan); - this.manipulationDOM.seperatorLineDiv1 = document.createElement("div"); - this.manipulationDOM.seperatorLineDiv1.className = "network-seperatorLine"; + /** + * 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: { - this.manipulationDOM.descriptionSpan = document.createElement("div"); - this.manipulationDOM.descriptionSpan.className = "network-manipulationUI none"; - this.manipulationDOM.descriptionLabelSpan = document.createElement("div"); - this.manipulationDOM.descriptionLabelSpan.className = "network-manipulationLabel"; - this.manipulationDOM.descriptionLabelSpan.innerHTML = locale.editEdgeDescription; - this.manipulationDOM.descriptionSpan.appendChild(this.manipulationDOM.descriptionLabelSpan); + /** + * 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: { - this.manipulationDiv.appendChild(this.manipulationDOM.backSpan); - this.manipulationDiv.appendChild(this.manipulationDOM.seperatorLineDiv1); - this.manipulationDiv.appendChild(this.manipulationDOM.descriptionSpan); + /** + * 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: { - // bind the icon - this._bindHammerToDiv(this.manipulationDOM.backSpan, "_createManipulatorBar"); - // temporarily overload functions - this.cachedFunctions._handleTouch = this._handleTouch; - this.cachedFunctions._manipulationReleaseOverload = this._manipulationReleaseOverload; - this.cachedFunctions._handleTap = this._handleTap; - this.cachedFunctions._handleDragStart = this._handleDragStart; - this.cachedFunctions._handleOnDrag = this._handleOnDrag; - this._handleTouch = this._selectControlNode; - this._handleTap = function () {}; - this._handleOnDrag = this._controlNodeDrag; - this._handleDragStart = function () {}; - this._manipulationReleaseOverload = this._releaseControlNode; + /** + * 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: { - // redraw to show the unselect - this._redraw(); - }; - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - exports._selectControlNode = function (pointer) { - this.edgeBeingEdited.controlNodes.from.unselect(); - this.edgeBeingEdited.controlNodes.to.unselect(); - this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x), this._YconvertDOMtoCanvas(pointer.y)); - if (this.selectedControlNode !== null) { - this.selectedControlNode.select(); - this.freezeSimulation(true); - } - this._redraw(); - }; - /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * - * @private - */ - exports._controlNodeDrag = function (event) { - var pointer = this._getPointer(event.gesture.center); - if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { - this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); - this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); - } - this._redraw(); - }; + /** + * 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: { - /** - * - * @param pointer - * @private - */ - exports._releaseControlNode = function (pointer) { - var newNode = this._getNodeAt(pointer); - if (newNode !== null) { - if (this.edgeBeingEdited.controlNodes.from.selected == true) { - this.edgeBeingEdited._restoreControlNodes(); - this._editEdge(newNode.id, this.edgeBeingEdited.to.id); - this.edgeBeingEdited.controlNodes.from.unselect(); - } - if (this.edgeBeingEdited.controlNodes.to.selected == true) { - this.edgeBeingEdited._restoreControlNodes(); - this._editEdge(this.edgeBeingEdited.from.id, newNode.id); - this.edgeBeingEdited.controlNodes.to.unselect(); - } - } else { - this.edgeBeingEdited._restoreControlNodes(); - } - this.freezeSimulation(false); - this._redraw(); - }; + /** + * 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: { - /** - * 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 - */ - exports._handleConnect = function (pointer) { - if (this._getSelectedNodeCount() == 0) { - var node = this._getNodeAt(pointer); - if (node != null) { - if (this.isCluster(node.id) == true) { - alert(this.constants.locales[this.constants.locale].createEdgeError); - } else { - this._selectObject(node, false); - var supportNodes = this.body.sectors.support.nodes; - // create a node the temporary line can look at - supportNodes.targetNode = this.body.functions.createNode({ id: "targetNode" }); - var targetNode = supportNodes.targetNode; - targetNode.x = node.x; - targetNode.y = node.y; - // create a temporary edge - this.body.edges.connectionEdge = this.body.functions.createEdge({ id: "connectionEdge", from: node.id, to: targetNode.id }); - var connectionEdge = this.body.edges.connectionEdge; - connectionEdge.from = node; - connectionEdge.connected = true; - connectionEdge.options.smoothCurves = { enabled: true, - dynamic: false, - type: "continuous", - roundness: 0.5 - }; - connectionEdge.selected = true; - connectionEdge.to = targetNode; + /** + * + * 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: { - this.cachedFunctions._handleOnDrag = this._handleOnDrag; - var me = this; - this._handleOnDrag = function (event) { - var pointer = me._getPointer(event.gesture.center); - var connectionEdge = me.body.edges.connectionEdge; - connectionEdge.to.x = me._XconvertDOMtoCanvas(pointer.x); - connectionEdge.to.y = me._YconvertDOMtoCanvas(pointer.y); - me._redraw(); - }; - } - } - } - }; + /** + * + * 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: { - exports._finishConnect = function (event) { - if (this._getSelectedNodeCount() == 1) { - var pointer = this._getPointer(event.gesture.center); - // restore the drag function - this._handleOnDrag = this.cachedFunctions._handleOnDrag; - delete this.cachedFunctions._handleOnDrag; + /** + * + * 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: { - // remember the edge id - var connectFromId = this.body.edges.connectionEdge.fromId; - // remove the temporary nodes and edge - delete this.body.edges.connectionEdge; - delete this.body.sectors.support.nodes.targetNode; - delete this.body.sectors.support.nodes.targetViaNode; + /** + * 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; - var node = this._getNodeAt(pointer); - if (node != null) { - if (this.isCluster(node.id) === true) { - alert(this.constants.locales[this.constants.locale].createEdgeError); - } else { - this._createEdge(connectFromId, node.id); - this._createManipulatorBar(); - } - } - this._unselectAll(); - } - }; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + // first unselect any selected node + this.unselectAll(true); - /** - * Adds a node on the specified location - */ - exports._addNode = function () { - if (this._selectionIsEmpty() && this.editMode == true) { - var positionObject = this._pointerToPositionObject(this.pointerPosition); - var defaultData = { id: util.randomUUID(), x: positionObject.left, y: positionObject.top, label: "new", allowedToMoveX: true, allowedToMoveY: true }; - if (this.triggerFunctions.add) { - if (this.triggerFunctions.add.length == 2) { - var me = this; - this.triggerFunctions.add(defaultData, function (finalizedData) { - me.body.data.nodes.add(finalizedData); - me._createManipulatorBar(); - me.moving = true; - me.start(); - }); - } else { - throw new Error("The function for add does not support two arguments (data,callback)"); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } else { - this.body.data.nodes.add(defaultData); - this._createManipulatorBar(); - this.moving = true; - this.start(); - } - } - }; + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + 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: { - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._createEdge = function (sourceNodeId, targetNodeId) { - if (this.editMode == true) { - var defaultData = { from: sourceNodeId, to: targetNodeId }; - if (this.triggerFunctions.connect) { - if (this.triggerFunctions.connect.length == 2) { - var me = this; - this.triggerFunctions.connect(defaultData, function (finalizedData) { - me.body.data.edges.add(finalizedData); - me.moving = true; - me.start(); - }); - } else { - throw new Error("The function for connect does not support two arguments (data,callback)"); - this.moving = true; - this.start(); - } - } else { - this.body.data.edges.add(defaultData); - this.moving = true; - this.start(); - } - } - }; - /** - * connect two nodes with a new edge. - * - * @private - */ - exports._editEdge = function (sourceNodeId, targetNodeId) { - if (this.editMode == true) { - var defaultData = { id: this.edgeBeingEdited.id, from: sourceNodeId, to: targetNodeId }; - console.log(defaultData); - if (this.triggerFunctions.editEdge) { - if (this.triggerFunctions.editEdge.length == 2) { - var me = this; - this.triggerFunctions.editEdge(defaultData, function (finalizedData) { - me.body.data.edges.update(finalizedData); - me.moving = true; - me.start(); - }); - } else { - throw new Error("The function for edit does not support two arguments (data, callback)"); - this.moving = true; - this.start(); - } - } else { - this.body.data.edges.update(defaultData); - this.moving = true; - this.start(); - } - } - }; + /** + * 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; - /** - * 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 - */ - exports._editNode = function () { - if (this.triggerFunctions.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 - } - } }; - if (this.triggerFunctions.edit.length == 2) { - var me = this; - this.triggerFunctions.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"); - } - }; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + // first unselect any selected node + this.unselectAll(true); + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + var 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: { - /** - * delete everything in the selection - * - * @private - */ - exports._deleteSelected = function () { - if (!this._selectionIsEmpty() && this.editMode == true) { - if (!this._clusterInSelection()) { - var selectedNodes = this.getSelectedNodes(); - var selectedEdges = this.getSelectedEdges(); - if (this.triggerFunctions.del) { - var me = this; - var data = { nodes: selectedNodes, edges: selectedEdges }; - if (this.triggerFunctions.del.length == 2) { - this.triggerFunctions.del(data, function (finalizedData) { - me.body.data.edges.remove(finalizedData.edges); - me.body.data.nodes.remove(finalizedData.nodes); - me._unselectAll(); - me.moving = true; - me.start(); - }); - } else { - throw new Error("The function for delete does not support two arguments (data, callback)"); + /** + * 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]; + } + } } - } else { - this.body.data.edges.remove(selectedEdges); - this.body.data.nodes.remove(selectedNodes); - this._unselectAll(); - this.moving = true; - this.start(); - } - } else { - alert(this.constants.locales[this.constants.locale].deleteClusterError); + 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 } - } - }; + }); + + return SelectionHandler; + })(); + + exports.SelectionHandler = SelectionHandler; + Object.defineProperty(exports, "__esModule", { + value: true + }); /***/ }, -/* 76 */ +/* 80 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var util = __webpack_require__(1); - var Hammer = __webpack_require__(45); - - exports._cleanNavigation = function () { - // clean hammer bindings - if (this.navigationHammers.length != 0) { - for (var i = 0; i < this.navigationHammers.length; i++) { - this.navigationHammers[i].dispose(); - } - this.navigationHammers = []; - } - - this._navigationReleaseOverload = function () {}; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { - this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); - } - }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * 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. + * Created by Alex on 2/27/2015. * - * @private */ - exports._loadNavigationElements = function () { - this._cleanNavigation(); - - this.navigationDOM = {}; - var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; - var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - - this.navigationDOM.wrapper = document.createElement("div"); - this.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 = Hammer(this.navigationDOM[navigationDivs[i]], { prevent_default: true }); - hammer.on("touch", this[navigationDivActions[i]].bind(this)); - this.navigationHammers.push(hammer); - } - - this._navigationReleaseOverload = this._stopMovement; - }; - - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - exports._zoomExtent = function (event) { - this.zoomExtent({ duration: 700 }); - event.stopPropagation(); - }; - /** - * this stops all movement induced by the navigation buttons - * - * @private - */ - exports._stopMovement = function () { - this._xStopMoving(); - this._yStopMoving(); - this._stopZoom(); - }; + var util = __webpack_require__(1); + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); - /** - * move the screen up - * By using the increments, instead of adding a fixed number to the translation, we keep fluent and - * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently - * To avoid this behaviour, we do the translation in the start loop. - * - * @private - */ - exports._moveUp = function (event) { - this.yIncrement = this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; + // 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); - /** - * move the screen down - * @private - */ - exports._moveDown = function (event) { - this.yIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + this.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.pointerPosition = { x: 0, y: 0 }; + this.hoverObj = { nodes: {}, edges: {} }; - /** - * move the screen left - * @private - */ - exports._moveLeft = function (event) { - this.xIncrement = this.constants.keyboard.speed.x; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + this.options = {}; + this.defaultOptions = { + dragNodes: true, + dragView: true, + zoomView: true, + selectEnabled: true, + hoverEnabled: false + }; + util.extend(this.options, this.defaultOptions); + } + _prototypeProperties(InteractionHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + }, + writable: true, + configurable: true + }, + getPointer: { - /** - * move the screen right - * @private - */ - exports._moveRight = function (event) { - this.xIncrement = -this.constants.keyboard.speed.y; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + /** + * 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: { - /** - * Zoom in, using the same method as the movement. - * @private - */ - exports._zoomIn = function (event) { - this.zoomIncrement = this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + /** + * 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; - /** - * Zoom out - * @private - */ - exports._zoomOut = function (event) { - this.zoomIncrement = -this.constants.keyboard.speed.zoom; - this.start(); // if there is no node movement, the calculation wont be done - event.preventDefault(); - }; + // 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: { + /** + * handle tap/click event: select/unselect a node + * @private + */ + value: function onTap(event) { + var pointer = this.getPointer(event.center); - /** - * Stop zooming and unhighlight the zoom controls - * @private - */ - exports._stopZoom = function (event) { - this.zoomIncrement = 0; - event && event.preventDefault(); - }; + var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; + var selected = this.selectionHandler.selectOnPoint(pointer); + if (selected === true || previouslySelected == true && selected === false) { + // select or unselect + this.body.emitter.emit("selected", this.selectionHandler.getSelection()); + } - /** - * Stop moving in the Y direction and unHighlight the up and down - * @private - */ - exports._yStopMoving = function (event) { - this.yIncrement = 0; - event && event.preventDefault(); - }; + this.selectionHandler._generateClickEvent("click", pointer); + }, + writable: true, + configurable: true + }, + onDoubleTap: { - /** - * Stop moving in the X direction and unHighlight left and right. - * @private - */ - exports._xStopMoving = function (event) { - this.xIncrement = 0; - event && event.preventDefault(); - }; + /** + * handle doubletap event + * @private + */ + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent("doubleClick", pointer); + }, + writable: true, + configurable: true + }, + onHold: { -/***/ }, -/* 77 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; - exports._resetLevels = function () { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (node.preassignedLevel == false) { - node.level = -1; - node.hierarchyEnumerated = false; - } - } - } - }; + /** + * handle long tap event: multi select nodes + * @private + */ + value: function onHold(event) { + var pointer = this.getPointer(event.center); - /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * - * @private - */ - exports._setupHierarchicalLayout = function () { - if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var hubsize = 0; - var node, nodeId; - var definedLevel = false; - var undefinedLevel = false; + var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.level != -1) { - definedLevel = true; - } else { - undefinedLevel = true; + if (selectionChanged === true) { + // select or longpress + this.body.emitter.emit("selected", this.selectionHandler.getSelection()); } - if (hubsize < node.edges.length) { - hubsize = node.edges.length; - } - } - } - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - this.zoomExtent({ duration: 0 }, true, this.constants.clustering.enabled); - if (!this.constants.clustering.enabled) { - this.start(); - } - } else { - // setup the system to use hierarchical method. - this._changeConstants(); + this.selectionHandler._generateClickEvent("click", pointer); + }, + writable: true, + configurable: true + }, + onRelease: { - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - if (this.constants.hierarchicalLayout.layout == "hubsize") { - this._determineLevels(hubsize); - } else { - this._determineLevelsDirected(false); - } - } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); - // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize. - this._placeNodesByHierarchy(distribution); - } - } - }; + /** + * handle the release of the screen + * + * @private + */ + value: function onRelease() {}, + writable: true, + configurable: true + }, + onDragStart: { - /** - * This function places the nodes on the canvas based on the hierarchial distribution. - * - * @param {Object} distribution | obtained by the function this._getDistribution() - * @private - */ - exports._placeNodesByHierarchy = function (distribution) { - var nodeId, node; + /** + * 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); + } - // 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.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (node.xFixed) { - node.x = distribution[level].minPos; - node.xFixed = false; + var node = this.selectionHandler.getNodeAt(this.drag.pointer); + // note: drag.pointer is set in onTouch to get the initial touch location - distribution[level].minPos += distribution[level].nodeSpacing; - } - } else { - if (node.yFixed) { - node.y = distribution[level].minPos; - node.yFixed = false; + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({}, this.body.view.translation); // copy the object + this.drag.nodeId = null; + this.draggingNodes = false; - distribution[level].minPos += distribution[level].nodeSpacing; + this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); + + if (node != null && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + 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.xFixed, + yFixed: object.yFixed + }; + + object.xFixed = true; + object.yFixed = true; + + this.drag.selection.push(s); } } - this._placeBranchNodes(node.edges, node.id, distribution, node.level); } - } - } - } + }, + writable: true, + configurable: true + }, + onDrag: { - // stabilize the system after positioning. This function calls zoomExtent. - this._stabilize(); - }; + /** + * handle drag event + * @private + */ + value: function onDrag(event) { + var _this = this; + if (this.drag.pinched === true) { + return; + } - /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} - * @private - */ - exports._getDistribution = function () { - var distribution = {}; - var nodeId, node, level; + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit("unlockNode"); - // 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]; - node.xFixed = true; - node.yFixed = true; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - node.y = this.constants.hierarchicalLayout.levelSeparation * node.level; - } else { - node.x = this.constants.hierarchicalLayout.levelSeparation * node.level; - } - if (distribution[node.level] === undefined) { - distribution[node.level] = { amount: 0, nodes: {}, minPos: 0, nodeSpacing: 0 }; - } - distribution[node.level].amount += 1; - distribution[node.level].nodes[nodeId] = node; - } - } + var pointer = this.getPointer(event.center); + var selection = this.drag.selection; + if (selection && selection.length && this.options.dragNodes === true) { + // calculate delta's and new location + var deltaX = pointer.x - this.drag.pointer.x; + var deltaY = pointer.y - this.drag.pointer.y; - // determine the largest amount of nodes of all levels - var maxCount = 0; - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - if (maxCount < distribution[level].amount) { - maxCount = distribution[level].amount; - } - } - } + // update position of all selected nodes + selection.forEach(function (selection) { + var node = selection.node; - // set the initial position and spacing of each nodes accordingly - for (level in distribution) { - if (distribution.hasOwnProperty(level)) { - distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; - distribution[level].nodeSpacing /= distribution[level].amount + 1; - distribution[level].minPos = distribution[level].nodeSpacing - 0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing; - } - } + if (!selection.xFixed) { + node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } - return distribution; - }; + if (!selection.yFixed) { + node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); - /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. - * - * @param hubsize - * @private - */ - exports._determineLevels = function (hubsize) { - var nodeId, node; + // 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; - // determine hubs - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.edges.length == hubsize) { - node.level = 0; - } - } - } + this.body.view.translation = { x: this.drag.translation.x + diffX, y: this.drag.translation.y + diffY }; + this.body.emitter.emit("_redraw"); + } + } + }, + writable: true, + configurable: true + }, + onDragEnd: { - // branch from hubs - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.level == 0) { - this._setLevel(1, node.edges, node.id); - } - } - } - }; + /** + * 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.xFixed = s.xFixed; + s.node.yFixed = 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: { - /** - * this function allocates nodes in levels based on the direction of the edges - * - * @param hubsize - * @private - */ - exports._determineLevelsDirected = function () { - var nodeId, node, firstNode; - var minLevel = 10000; - // set first node to source - firstNode = this.body.nodes[this.nodeIndices[0]]; - firstNode.level = minLevel; - this._setLevelDirected(minLevel, firstNode.edges, firstNode.id); - // get the minimum level - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - minLevel = node.level < minLevel ? node.level : minLevel; - } - } + /** + * Handle pinch event + * @param event + * @private + */ + value: function onPinch(event) { + var pointer = this.getPointer(event.center); - // 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)) { - node = this.body.nodes[nodeId]; - node.level -= minLevel; - } - } - }; + this.drag.pinched = true; + if (this.pinch[scale] === undefined) { + this.pinch.scale = 1; + } + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.gesture.scale; + this.zoom(scale, pointer); + }, + writable: true, + configurable: true + }, + zoom: { - /** - * Since hierarchical layout does not support: - * - smooth curves (based on the physics), - * - clustering (based on dynamic node counts) - * - * We disable both features so there will be no problems. - * - * @private - */ - exports._changeConstants = function () { - this.constants.clustering.enabled = false; - this.constants.physics.barnesHut.enabled = false; - this.constants.physics.hierarchicalRepulsion.enabled = true; - this._loadSelectedForceSolver(); - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.dynamic = false; - } - this._configureSmoothCurves(); - var config = this.constants.hierarchicalLayout; - config.levelSeparation = Math.abs(config.levelSeparation); - if (config.direction == "RL" || config.direction == "DU") { - config.levelSeparation *= -1; - } + /** + * 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 (config.direction == "RL" || config.direction == "LR") { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "vertical"; - } - } else { - if (this.constants.smoothCurves.enabled == true) { - this.constants.smoothCurves.type = "horizontal"; - } - } - }; + var preScaleDragPointer = null; + 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 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 - */ - exports._placeBranchNodes = function (edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } else { - childNode = edges[i].to; - } + this.body.view.scale = scale; + this.body.view.translation = { x: tx, y: ty }; - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - var nodeMoved = false; - if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { - if (childNode.xFixed && childNode.level > parentLevel) { - childNode.xFixed = false; - childNode.x = distribution[childNode.level].minPos; - nodeMoved = true; - } - } else { - if (childNode.yFixed && childNode.level > parentLevel) { - childNode.yFixed = false; - childNode.y = distribution[childNode.level].minPos; - nodeMoved = true; - } - } + if (preScaleDragPointer != null) { + var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + this.drag.pointer.x = postScaleDragPointer.x; + this.drag.pointer.y = postScaleDragPointer.y; + } - if (nodeMoved == true) { - distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNode.level); - } - } - } - }; + this.body.emitter.emit("_requestRedraw"); + if (scaleOld < scale) { + this.body.emitter.emit("zoom", { direction: "+" }); + } else { + this.body.emitter.emit("zoom", { direction: "-" }); + } + } + }, + writable: true, + configurable: true + }, + onMouseWheel: { - /** - * 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 - */ - exports._setLevel = function (level, edges, parentId) { - for (var i = 0; i < edges.length; i++) { - var childNode = null; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } else { - childNode = edges[i].to; - } - if (childNode.level == -1 || childNode.level > level) { - childNode.level = level; - if (childNode.edges.length > 1) { - this._setLevel(level + 1, childNode.edges, childNode.id); - } - } - } - }; + /** + * 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) { + // calculate the new scale + var scale = this.body.view.scale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= 1 + zoom; - /** - * 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 - */ - exports._setLevelDirected = function (level, edges, parentId) { - this.body.nodes[parentId].hierarchyEnumerated = true; - var childNode, direction; - for (var i = 0; i < edges.length; i++) { - direction = 1; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - direction = -1; - } else { - childNode = edges[i].to; - } - if (childNode.level == -1) { - childNode.level = level + direction; - } - } + // calculate the pointer location + var pointer = { x: event.pageX, y: event.pageY }; - for (var i = 0; i < edges.length; i++) { - if (edges[i].toId == parentId) { - childNode = edges[i].from; - } else { - childNode = edges[i].to; - } + // apply the new scale + this.zoom(scale, pointer); + } - if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) { - this._setLevelDirected(childNode.level, childNode.edges, childNode.id); - } - } - }; + // Prevent default actions caused by mouse wheel. + event.preventDefault(); + }, + writable: true, + configurable: true + }, + onMouseMove: { - /** - * Unfix nodes - * - * @private - */ - exports._restoreNodes = function () { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.body.nodes[nodeId].xFixed = false; - this.body.nodes[nodeId].yFixed = false; - } - } - }; + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + value: function onMouseMove(event) { + var pointer = { 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); + } -/***/ }, -/* 78 */ -/***/ function(module, exports, __webpack_require__) { + // 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(); + } + } - function webpackContext(req) { - throw new Error("Cannot find module '" + req + "'."); - } - webpackContext.keys = function() { return []; }; - webpackContext.resolve = webpackContext; - module.exports = webpackContext; - webpackContext.id = 78; + // 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) { + var me = this; + var checkShow = function () { + me._checkShowPopup(pointer); + }; -/***/ }, -/* 79 */ -/***/ function(module, exports, __webpack_require__) { + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running calculationTimer + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay); + } + } - module.exports = function(module) { - if(!module.webpackPolyfill) { - module.deprecate = function() {}; - module.paths = []; - // module.parent = undefined by default - module.children = []; - module.webpackPolyfill = 1; - } - return module; - } + /** + * 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]; + } + } + // adding hover highlights + var obj = this.selectionHandler.getNodeAt(pointer); + if (obj == null) { + obj = this.selectionHandler.getEdgeAt(pointer); + } + if (obj != null) { + this._hoverObject(obj); + } -/***/ }, -/* 80 */ -/***/ function(module, exports, __webpack_require__) { + // removing all node hover highlights except for the selected one. + for (var nodeId in this.hoverObj.nodes) { + if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { + this._blurObject(this.hoverObj.nodes[nodeId]); + delete this.hoverObj.nodes[nodeId]; + } + } + } + this.body.emitter.emit("_requestRedraw"); + } + }, + writable: true, + configurable: true + } + }); - /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; - - /* WEBPACK VAR INJECTION */}.call(exports, {})) + return InteractionHandler; + })(); + + exports.TouchEventHandler = TouchEventHandler; + Object.defineProperty(exports, "__esModule", { + value: true + }); /***/ } /******/ ]) diff --git a/lib/network/Network.js b/lib/network/Network.js index eaec5265..800e0fac 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -24,7 +24,8 @@ import { ClusterEngine } from './modules/Clustering' import { CanvasRenderer } from './modules/CanvasRenderer' import { Canvas } from './modules/Canvas' import { View } from './modules/View' -import { TouchEventHandler } from './modules/TouchEventHandler' +import { TouchEventHandler } from './modules/InteractionHandler' +import { SelectionHandler } from "./modules/SelectionHandler" /** * @constructor Network @@ -132,15 +133,9 @@ function Network (container, data, options) { inheritColor: "from", // to, from, false, true (== from) useGradients: false // release in 4.0 }, - configurePhysics:false, navigation: { enabled: false }, - keyboard: { - enabled: false, - speed: {x: 10, y: 10, zoom: 0.02}, - bindToWindow: true - }, dataManipulation: { enabled: false, initiallyVisible: false @@ -152,7 +147,31 @@ function Network (container, data, options) { direction: "UD", // UD, DU, LR, RL layout: "hubsize" // hubsize, directed }, - + interaction: { + dragNodes:true, + dragView: true, + zoomView: true, + hoverEnabled: 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 + } + }, + selection: { + enabled: true, + selectConnectedEdges: true + }, smoothCurves: { enabled: true, dynamic: true, @@ -161,25 +180,6 @@ function Network (container, data, options) { }, locale: 'en', locales: locales, - tooltip: { - delay: 300, - fontColor: 'black', - fontSize: 14, // px - fontFace: 'verdana', - color: { - border: '#666', - background: '#FFFFC6' - } - }, - dragNetwork: true, - dragNodes: true, - zoomable: true, - hover: false, - hideEdgesOnDrag: false, - hideNodesOnDrag: false, - width : '100%', - height : '100%', - selectable: true, useDefaultGroups: true }; this.constants = util.extend({}, this.defaultOptions); @@ -218,20 +218,24 @@ function Network (container, data, options) { onMouseMove: function() {}, onRelease: function() {} }, - container: container + container: container, + view: { + scale:1, + translation:{x:0,y:0} + } }; // modules - this.view = new View(this.body); - this.renderer = new CanvasRenderer(this.body); + this.canvas = new Canvas(this.body); + this.selectionHandler = new SelectionHandler(this.body, this.canvas); + this.touchHandler = new TouchEventHandler(this.body, this.canvas, this.selectionHandler); + this.view = new View(this.body, this.canvas); + this.renderer = new CanvasRenderer(this.body, this.canvas); this.clustering = new ClusterEngine(this.body); this.physics = new PhysicsEngine(this.body); - this.canvas = new Canvas(this.body); - this.touchHandler = new TouchEventHandler(this.body); - this.renderer.setCanvas(this.canvas); - this.view.setCanvas(this.canvas); - this.touchHandler.setCanvas(this.canvas); + // create the DOM elements + this.canvas.create(); this.hoverObj = {nodes:{},edges:{}}; this.controlNodesActive = false; @@ -387,7 +391,7 @@ Network.prototype.setData = function(data, disableStart) { } // unselect all to ensure no selections from old data are carried over. - this._unselectAll(true); + this.selectionHandler.unselectAll(); // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added. this.initializing = true; @@ -433,7 +437,7 @@ Network.prototype.setData = function(data, disableStart) { } else { // find a stable position or start animating to a stable position - this.physics.startSimulation() + this.body.emitter.emit("stabilize"); } } else { @@ -459,7 +463,10 @@ Network.prototype.setOptions = function (options) { this.groups.useDefaultGroups = this.constants.useDefaultGroups; this.physics.setOptions(options.physics); - this.canvas.setOptions(this.constants); + this.canvas.setOptions(options.canvas); + this.renderer.setOptions(options.rendering); + this.touchHandler.setOptions(options.interaction); + this.selectionHandler.setOptions(options.selection); if (options.onAdd) {this.triggerFunctions.add = options.onAdd;} @@ -572,7 +579,7 @@ Network.prototype.setOptions = function (options) { //this._createKeyBinds(); this._markAllEdgesAsDirty(); - this.canvas.setSize(this.constants.width, this.constants.height); + this.canvas.setSize(); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); @@ -783,12 +790,12 @@ Network.prototype._checkHidePopup = function (pointer) { if (this.popup.popupTargetType == 'node') { stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); if (stillOnObj === true) { - var overNode = this._getNodeAt(pointer); + var overNode = this.getNodeAt(pointer); stillOnObj = overNode.id == this.popup.popupTargetId; } } else { - if (this._getNodeAt(pointer) === null) { + if (this.getNodeAt(pointer) === null) { stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); } } @@ -1231,7 +1238,7 @@ Network.prototype._handleNavigation = function() { x: this.frame.canvas.clientWidth / 2, y: this.frame.canvas.clientHeight / 2 }; - this._zoom(this.scale*(1 + this.zoomIncrement), center); + this.zoom(this.scale*(1 + this.zoomIncrement), center); } }; diff --git a/lib/network/Node.js b/lib/network/Node.js index 820311fd..93808418 100644 --- a/lib/network/Node.js +++ b/lib/network/Node.js @@ -216,13 +216,6 @@ Node.prototype.unselect = function() { }; -/** - * Reset the calculated size of the node, forces it to recalculate its size - */ -Node.prototype.clearSizeCache = function() { - this._reset(); -}; - /** * Reset the calculated size of the node, forces it to recalculate its size * @private @@ -619,7 +612,7 @@ Node.prototype._drawCircle = function (ctx) { }; Node.prototype._resizeEllipse = function (ctx) { - if (!this.width) { + if (this.width === undefined) { var textSize = this.getTextSize(ctx); this.width = textSize.width * 1.5; diff --git a/lib/network/mixins/SelectionMixin.js b/lib/network/mixins/SelectionMixin.js index 09d74c91..e2910f8f 100644 --- a/lib/network/mixins/SelectionMixin.js +++ b/lib/network/mixins/SelectionMixin.js @@ -104,8 +104,8 @@ exports._getAllEdgesOverlappingWith = function (object) { }; /** - * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call - * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. * * @param pointer * @returns {null} diff --git a/lib/network/modules/Canvas.js b/lib/network/modules/Canvas.js index 8f382186..aa64b02c 100644 --- a/lib/network/modules/Canvas.js +++ b/lib/network/modules/Canvas.js @@ -1,26 +1,38 @@ var Hammer = require('../../module/hammer'); var hammerUtil = require('../../hammerUtil'); +var util = require('../../util'); + +/** + * 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 + */ class Canvas { - /** - * 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 - */ - constructor(body, options) { + constructor(body) { this.body = body; - this.setOptions(options); - this.translation = {x: 0, y: 0}; - this.scale = 1.0; - this.body.emitter.on("_setScale", (scale) => {this.scale = scale}); - this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); - this.body.emitter.once("resize", (obj) => {this.translation.x = obj.width * 0.5; this.translation.y = obj.height * 0.5; this.body.emitter.emit("_setTranslation", this.translation)}); + this.options = {}; + this.defaultOptions = { + width:'100%', + height:'100%' + } + util.extend(this.options, this.defaultOptions); + + this.body.emitter.once("resize", (obj) => {this.body.view.translation.x = obj.width * 0.5; this.body.view.translation.y = obj.height * 0.5;}); this.pixelRatio = 1; + } + setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + } + + create() { // remove all elements from the container element. while (this.body.container.hasChildNodes()) { this.body.container.removeChild(this.body.container.firstChild); @@ -61,8 +73,8 @@ class Canvas { // add the frame to the container element this.body.container.appendChild(this.frame); - this.body.emitter.emit("_setScale", 1);; - this.body.emitter.emit("_setTranslation", {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight});; + 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(); } @@ -89,11 +101,9 @@ class Canvas { this.hammer.on('panend', me.body.eventListeners.onDragEnd ); this.hammer.on('pinch', me.body.eventListeners.onPinch.bind(me) ); - // TODO: neatly cleanup these handlers when re-creating the Canvas - if (this.options.zoomable == true) { - this.frame.canvas.addEventListener('mousewheel', me.body.eventListeners.onMouseWheel.bind(me)); - this.frame.canvas.addEventListener('DOMMouseScroll', me.body.eventListeners.onMouseWheel.bind(me)); - } + // 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', me.body.eventListeners.onMouseWheel.bind(me)); + this.frame.canvas.addEventListener('DOMMouseScroll', me.body.eventListeners.onMouseWheel.bind(me)); this.frame.canvas.addEventListener('mousemove', me.body.eventListeners.onMouseMove.bind(me)); @@ -101,9 +111,6 @@ class Canvas { hammerUtil.onRelease(this.hammerFrame, me.body.eventListeners.onRelease.bind(me) ); } - setOptions(options = {}) { - this.options = options; - } /** * Set a new size for the network @@ -112,7 +119,7 @@ class Canvas { * @param {string} height Height in pixels or percentage (for example '400px' * or '30%') */ - setSize(width, height) { + setSize(width = this.options.width, height = this.options.height) { var emitEvent = false; var oldWidth = this.frame.canvas.width; var oldHeight = this.frame.canvas.height; @@ -159,7 +166,7 @@ class Canvas { * @private */ _XconvertDOMtoCanvas(x) { - return (x - this.translation.x) / this.scale; + return (x - this.body.view.translation.x) / this.body.view.scale; } /** @@ -170,7 +177,7 @@ class Canvas { * @private */ _XconvertCanvasToDOM(x) { - return x * this.scale + this.translation.x; + return x * this.body.view.scale + this.body.view.translation.x; } /** @@ -181,7 +188,7 @@ class Canvas { * @private */ _YconvertDOMtoCanvas(y) { - return (y - this.translation.y) / this.scale; + return (y - this.body.view.translation.y) / this.body.view.scale; } /** @@ -192,7 +199,7 @@ class Canvas { * @private */ _YconvertCanvasToDOM(y) { - return y * this.scale + this.translation.y ; + return y * this.body.view.scale + this.body.view.translation.y; } diff --git a/lib/network/modules/CanvasRenderer.js b/lib/network/modules/CanvasRenderer.js index 50bbb4df..d66ef446 100644 --- a/lib/network/modules/CanvasRenderer.js +++ b/lib/network/modules/CanvasRenderer.js @@ -7,32 +7,49 @@ if (typeof window !== 'undefined') { window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; } +var util = require('../../util'); + + class CanvasRenderer { - constructor(body) { + constructor(body, canvas) { this.body = body; + this.canvas = canvas; this.redrawRequested = false; this.renderTimer = false; this.requiresTimeout = true; - this.continueRendering = true; + this.continueRendering = false; this.renderRequests = 0; - this.translation = {x: 0, y: 0}; - this.scale = 1.0; this.canvasTopLeft = {x: 0, y: 0}; this.canvasBottomRight = {x: 0, y: 0}; - this.body.emitter.on("_setScale", (scale) => this.scale = scale); - this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); + this.dragging = false; + + this.body.emitter.on("dragStart", () => {this.dragging = true; console.log("here")}); + this.body.emitter.on("dragEnd", () => this.dragging = false); this.body.emitter.on("_redraw", this._redraw.bind(this)); - this.body.emitter.on("_redrawHidden", this._redraw.bind(this, true)); this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.continueRendering = true; this.startRendering();}); this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.continueRendering = this.renderRequests > 0;}); + this.options = {}; + this.defaultOptions = { + hideEdgesOnDrag: false, + hideNodesOnDrag: false + } + util.extend(this.options,this.defaultOptions); + + this._determineBrowserMethod(); } + setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + } + startRendering() { if (this.continueRendering === true) { @@ -67,9 +84,6 @@ class CanvasRenderer { } } - setCanvas(canvas) { - this.canvas = canvas; - } /** * Redraw the network with the current data * chart will be resized too. @@ -84,14 +98,14 @@ class CanvasRenderer { * @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 */ - _requestRedraw(hidden) { + _requestRedraw() { if (this.redrawRequested !== true) { this.redrawRequested = true; if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, hidden),0); + window.setTimeout(this._redraw.bind(this, false),0); } else { - window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); + window.requestAnimationFrame(this._redraw.bind(this, false)); } } } @@ -109,25 +123,27 @@ class CanvasRenderer { var h = this.canvas.frame.canvas.clientHeight; ctx.clearRect(0, 0, w, h); + this.body.emitter.emit("beforeDrawing", ctx); + // set scaling and translation ctx.save(); - ctx.translate(this.translation.x, this.translation.y); - ctx.scale(this.scale, this.scale); + 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}); + console.log(this.dragging) + if (hidden === false) { - // todo: solve this - //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { + if (this.dragging === false || (this.dragging === true && this.options.hideEdgesOnDrag === false)) { this._drawEdges(ctx); - //} + } } - // todo: solve this - //if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { + if (this.dragging === false || (this.dragging === true && this.options.hideNodesOnDrag === false)) { this._drawNodes(ctx, this.body.nodes, hidden); - //} + } if (hidden === false) { if (this.controlNodesActive == true) { @@ -144,6 +160,8 @@ class CanvasRenderer { if (hidden === true) { ctx.clearRect(0, 0, w, h); } + + this.body.emitter.emit("afterDrawing", ctx); } @@ -160,7 +178,7 @@ class CanvasRenderer { for (var id in nodes) { if (nodes.hasOwnProperty(id)) { - nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight); + nodes[id].setScaleAndPos(this.body.view.scale,this.canvasTopLeft,this.canvasBottomRight); if (nodes[id].isSelected()) { selected.push(id); } @@ -195,7 +213,7 @@ class CanvasRenderer { for (var id in edges) { if (edges.hasOwnProperty(id)) { var edge = edges[id]; - edge.setScale(this.scale); + edge.setScale(this.body.view.scale); if (edge.connected === true) { edges[id].draw(ctx); } diff --git a/lib/network/modules/InteractionHandler.js b/lib/network/modules/InteractionHandler.js new file mode 100644 index 00000000..dcf20df8 --- /dev/null +++ b/lib/network/modules/InteractionHandler.js @@ -0,0 +1,462 @@ +/** + * Created by Alex on 2/27/2015. + * + */ + + +var util = require('../../util'); + +class InteractionHandler { + constructor(body, canvas, selectionHandler) { + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; + + // 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.touchTime = 0; + this.drag = {}; + this.pinch = {}; + this.pointerPosition = {x:0,y:0}; + this.hoverObj = {nodes:{},edges:{}}; + + + this.options = {}; + this.defaultOptions = { + dragNodes:true, + dragView: true, + zoomView: true, + selectEnabled: true, + hoverEnabled: false + } + util.extend(this.options,this.defaultOptions); + } + + setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + } + + + /** + * Get the pointer location from a touch location + * @param {{x: Number, y: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private + */ + getPointer(touch) { + return { + x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), + y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) + }; + } + + + /** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ + 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(); + } + } + + /** + * handle tap/click event: select/unselect a node + * @private + */ + onTap(event) { + var pointer = this.getPointer(event.center); + + var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; + var selected = this.selectionHandler.selectOnPoint(pointer); + + if (selected === true || (previouslySelected == true && selected === false)) { // select or unselect + this.body.emitter.emit('selected', this.selectionHandler.getSelection()); + } + + this.selectionHandler._generateClickEvent("click",pointer); + } + + + /** + * handle doubletap event + * @private + */ + onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent("doubleClick",pointer); + } + + + + /** + * handle long tap event: multi select nodes + * @private + */ + onHold(event) { + var pointer = this.getPointer(event.center); + + var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); + + if (selectionChanged === true) { // select or longpress + this.body.emitter.emit('selected', this.selectionHandler.getSelection()); + } + + this.selectionHandler._generateClickEvent("click",pointer); + } + + + /** + * handle the release of the screen + * + * @private + */ + onRelease() {} + + + /** + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private + */ + 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); + } + + var node = this.selectionHandler.getNodeAt(this.drag.pointer); + // note: drag.pointer is set in onTouch to get the initial touch location + + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({},this.body.view.translation); // copy the object + this.drag.nodeId = null; + this.draggingNodes = false; + + this.body.emitter.emit("dragStart", {nodeIds: this.selectionHandler.getSelection().nodes}); + + if (node != null && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + this.selectionHandler.selectObject(node); + } + + var selection = this.selectionHandler.selectionObj.nodes; + // create an array with the selected nodes and their original location and status + for (let 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.xFixed, + yFixed: object.yFixed + }; + + object.xFixed = true; + object.yFixed = true; + + this.drag.selection.push(s); + } + } + } + } + + + /** + * handle drag event + * @private + */ + onDrag(event) { + if (this.drag.pinched === true) { + return; + } + + // 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) { + // 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((selection) => { + var node = selection.node; + + if (!selection.xFixed) { + node.x = this.canvas._XconvertDOMtoCanvas(this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } + + if (!selection.yFixed) { + node.y = this.canvas._YconvertDOMtoCanvas(this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); + + + // 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; + + this.body.view.translation = {x:this.drag.translation.x + diffX, y:this.drag.translation.y + diffY}; + this.body.emitter.emit("_redraw"); + } + } + } + + + /** + * handle drag start event + * @private + */ + 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.xFixed = s.xFixed; + s.node.yFixed = s.yFixed; + }); + this.body.emitter.emit("startSimulation"); + } + else { + this.body.emitter.emit("_requestRedraw"); + } + + this.body.emitter.emit("dragEnd", {nodeIds: this.selectionHandler.getSelection().nodes}); + } + + + + /** + * Handle pinch event + * @param event + * @private + */ + onPinch(event) { + var pointer = this.getPointer(event.center); + + this.drag.pinched = true; + if (this.pinch[scale] === undefined) { + this.pinch.scale = 1; + } + + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.gesture.scale; + this.zoom(scale, pointer) + } + + + /** + * 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 + */ + 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 preScaleDragPointer = null; + 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.body.view.scale = scale; + this.body.view.translation = {x:tx, y:ty}; + + if (preScaleDragPointer != null) { + var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + this.drag.pointer.x = postScaleDragPointer.x; + this.drag.pointer.y = postScaleDragPointer.y; + } + + this.body.emitter.emit("_requestRedraw"); + + if (scaleOld < scale) { + this.body.emitter.emit("zoom", {direction: "+"}); + } + else { + this.body.emitter.emit("zoom", {direction: "-"}); + } + } + } + + + /** + * 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 + */ + 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) { + + // 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 = {x:event.pageX, y:event.pageY}; + + // apply the new scale + this.zoom(scale, pointer); + } + + // Prevent default actions caused by mouse wheel. + event.preventDefault(); + } + + + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + onMouseMove(event) { + var pointer = {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(); + } + } + + // 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) { + var me = this; + var checkShow = function() { + me._checkShowPopup(pointer); + }; + + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running calculationTimer + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(checkShow, this.options.tooltip.delay); + } + } + + /** + * 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]; + } + } + + // adding hover highlights + var obj = this.selectionHandler.getNodeAt(pointer); + if (obj == null) { + obj = this.selectionHandler.getEdgeAt(pointer); + } + if (obj != null) { + this._hoverObject(obj); + } + + // removing all node hover highlights except for the selected one. + for (var nodeId in this.hoverObj.nodes) { + if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { + this._blurObject(this.hoverObj.nodes[nodeId]); + delete this.hoverObj.nodes[nodeId]; + } + } + } + this.body.emitter.emit("_requestRedraw"); + } + } +} + +export {TouchEventHandler}; diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index a7194e15..ec1a3ad2 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -15,13 +15,9 @@ var util = require('../../util'); class PhysicsEngine { - constructor(body, options) { + constructor(body) { this.body = body; this.physicsBody = {calculationNodes: {}, calculationNodeIndices:[], forces: {}, velocities: {}}; - this.scale = 1; - this.viewFunction = undefined; - - this.body.emitter.on("_setScale", (scale) => this.scale = scale); this.simulationInterval = 1000 / 60; this.requiresTimeout = true; @@ -32,7 +28,8 @@ class PhysicsEngine { this.stabilizationIterations = 0; // default options - this.options = { + this.options = {}; + this.defaultOptions = { barnesHut: { thetaInverted: 1 / 0.5, // inverted to save time during calculation gravitationalConstant: -2000, @@ -67,8 +64,11 @@ class PhysicsEngine { zoomExtent: true } } + util.extend(this.options, this.defaultOptions); - this.setOptions(options); + this.body.emitter.on("stabilize", () => {this.startSimulation();}); + this.body.emitter.on("startSimulation", () => {this.stabilized = false; this.runSimulation();}); + this.body.emitter.on("stopSimulation", () => {this.stopSimulation();}); } setOptions(options) { @@ -116,6 +116,15 @@ class PhysicsEngine { } } + stopSimulation() { + this.stabilized = true; + if (this.viewFunction !== undefined) { + this.body.emitter.off("_beforeRender", this.viewFunction); + this.viewFunction = undefined; + this.body.emitter.emit("_stopRendering"); + } + } + runSimulation() { if (this.viewFunction === undefined) { this.viewFunction = this.simulationStep.bind(this); @@ -157,7 +166,7 @@ class PhysicsEngine { else { this.stabilizationIterations = 0; } - this.body.emitter.emit("_stopRendering"); + this.stopSimulation(); } } @@ -261,7 +270,7 @@ class PhysicsEngine { var nodeIndices = this.physicsBody.calculationNodeIndices; var maxVelocity = this.options.maxVelocity === 0 ? 1e9 : this.options.maxVelocity; var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.scale,0.05); + var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale,0.05); for (let i = 0; i < nodeIndices.length; i++) { let nodeId = nodeIndices[i]; diff --git a/lib/network/modules/components/SelectionHandler.js b/lib/network/modules/SelectionHandler.js similarity index 82% rename from lib/network/modules/components/SelectionHandler.js rename to lib/network/modules/SelectionHandler.js index 800395ba..2375496d 100644 --- a/lib/network/modules/components/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -2,24 +2,31 @@ * Created by Alex on 2/27/2015. */ -var Node = require("../../Node"); +var Node = require("../Node"); +var util = require('../../util'); class SelectionHandler { - constructor(body) { + constructor(body, canvas) { this.body = body; + this.canvas = canvas; this.selectionObj = {nodes:[], edges:[]}; - this.options = { + this.options = {}; + this.defaultOptions = { select: true, selectConnectedEdges: true } + util.extend(this.options, this.defaultOptions); } - setCanvas(canvas) { - this.canvas = canvas; + setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } } + /** * handles the selection part of the tap; * @@ -27,52 +34,84 @@ class SelectionHandler { * @private */ selectOnPoint(pointer) { + var selected = false; if (this.options.select === true) { - if (this._getSelectedObjectCount() > 0) {this._unselectAll();} - this.selectObject(pointer); - this._generateClickEvent(pointer); + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; + + // deselect + if (obj === undefined) { + this.unselectAll(); + } + else { + selected = this.selectObject(obj); + } this.body.emitter.emit("_requestRedraw"); } + return selected; + } + + 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; } - _generateClickEvent(pointer) { + _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("click", properties); + this.body.emitter.emit(eventType, properties); } - selectObject(pointer) { - var obj = this._getNodeAt(pointer); - if (obj != null) { - if (this.options.selectConnectedEdges === true) { - this._selectConnectedEdges(obj); + selectObject(obj) { + if (obj !== undefined) { + if (obj instanceof Node) { + if (this.options.selectConnectedEdges === true) { + this._selectConnectedEdges(obj); + } } - } - else { - obj = this._getEdgeAt(pointer); - } - - if (obj !== null) { obj.select(); this._addToSelection(obj); - this.body.emitter.emit('selected', this.getSelection()) + return true; + } + return false; + } + + deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); } - return obj; } + /** - * - * @param object - * @param overlappingNodes + * 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 */ - _getNodesOverlappingWith(object, overlappingNodes) { + _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; var nodes = this.body.nodes; for (var nodeId in nodes) { if (nodes.hasOwnProperty(nodeId)) { @@ -81,17 +120,6 @@ class SelectionHandler { } } } - } - - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - _getAllNodesOverlappingWith(object) { - var overlappingNodes = []; - this._getNodesOverlappingWith(object,overlappingNodes); return overlappingNodes; } @@ -118,10 +146,10 @@ class SelectionHandler { * Get the top node at the a specific point (like a click) * * @param {{x: Number, y: Number}} pointer - * @return {Node | null} node + * @return {Node | undefined} node * @private */ - _getNodeAt(pointer) { + getNodeAt(pointer) { // we first check if this is an navigation controls element var positionObject = this._pointerToPositionObject(pointer); var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); @@ -132,7 +160,7 @@ class SelectionHandler { return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; } else { - return null; + return undefined; } } @@ -169,14 +197,14 @@ class SelectionHandler { /** - * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call - * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call + * getNodeAt and _getEdgesAt, then priortize the selection to user preferences. * * @param pointer - * @returns {null} + * @returns {undefined} * @private */ - _getEdgeAt(pointer) { + getEdgeAt(pointer) { var positionObject = this._pointerToPositionObject(pointer); var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); @@ -184,7 +212,7 @@ class SelectionHandler { return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; } else { - return null; + return undefined; } } @@ -238,10 +266,9 @@ class SelectionHandler { /** * Unselect all. The selectionObj is useful for this. * - * @param {Boolean} [doNotTrigger] | ignore trigger * @private */ - _unselectAll(doNotTrigger = false) { + unselectAll() { for(var nodeId in this.selectionObj.nodes) { if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { this.selectionObj.nodes[nodeId].unselect(); @@ -254,10 +281,6 @@ class SelectionHandler { } this.selectionObj = {nodes:{},edges:{}}; - - if (doNotTrigger == false) { - this.body.emitter.emit('select', this.getSelection()); - } } @@ -289,7 +312,7 @@ class SelectionHandler { return this.selectionObj.nodes[nodeId]; } } - return null; + return undefined; } /** @@ -304,7 +327,7 @@ class SelectionHandler { return this.selectionObj.edges[edgeId]; } } - return null; + return undefined; } @@ -469,50 +492,6 @@ class SelectionHandler { - /** - * handles the selection part of the double tap and opens a cluster if needed - * - * @param {Object} pointer - * @private - */ - _handleDoubleTap(pointer) { - var node = this._getNodeAt(pointer); - if (node != null && node !== undefined) { - // we reset the areaCenter here so the opening of the node will occur - this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), - "y" : this._YconvertDOMtoCanvas(pointer.y)}; - this.openCluster(node); - } - var properties = this.getSelection(); - properties['pointer'] = { - DOM: {x: pointer.x, y: pointer.y}, - canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)} - } - this.body.emitter.emit("doubleClick", properties); - } - - - /** - * Handle the onHold selection part - * - * @param pointer - * @private - */ - _handleOnHold(pointer) { - var node = this._getNodeAt(pointer); - if (node != null) { - this._selectObject(node,true); - } - else { - var edge = this._getEdgeAt(pointer); - if (edge != null) { - this._selectObject(edge,true); - } - } - this._requestRedraw(); - } - - /** * * retrieve the currently selected objects @@ -574,7 +553,7 @@ class SelectionHandler { throw 'Selection must be an array with ids'; // first unselect any selected node - this._unselectAll(true); + this.unselectAll(true); for (i = 0, iMax = selection.length; i < iMax; i++) { id = selection[i]; @@ -601,7 +580,7 @@ class SelectionHandler { throw 'Selection must be an array with ids'; // first unselect any selected node - this._unselectAll(true); + this.unselectAll(true); for (i = 0, iMax = selection.length; i < iMax; i++) { id = selection[i]; diff --git a/lib/network/modules/TouchEventHandler.js b/lib/network/modules/TouchEventHandler.js deleted file mode 100644 index 96e5ed97..00000000 --- a/lib/network/modules/TouchEventHandler.js +++ /dev/null @@ -1,460 +0,0 @@ -/** - * Created by Alex on 2/27/2015. - * - */ - -import {SelectionHandler} from "./components/SelectionHandler" -var util = require('../../util'); - -class TouchEventHandler { - constructor(body) { - this.body = body; - - 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.touchTime = 0; - this.drag = {}; - this.pinch = {}; - this.pointerPosition = {x:0,y:0}; - - this.scale = 1.0; - this.body.emitter.on("_setScale", (scale) => this.scale = scale); - - this.selectionHandler = new SelectionHandler(body); - } - - setCanvas(canvas) { - this.canvas = canvas; - this.selectionHandler.setCanvas(canvas); - } - - /** - * Get the pointer location from a touch location - * @param {{x: Number, y: Number}} touch - * @return {{x: Number, y: Number}} pointer - * @private - */ - getPointer(touch) { - return { - x: touch.x - util.getAbsoluteLeft(this.canvas.frame.canvas), - y: touch.y - util.getAbsoluteTop(this.canvas.frame.canvas) - }; - } - - - /** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ - onTouch(event) { - console.log('touch!') - if (new Date().valueOf() - this.touchTime > 100) { - this.drag.pointer = this.getPointer(event.center); - this.drag.pinched = false; - this.pinch.scale = this.scale; - - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); - } - } - - /** - * handle tap/click event: select/unselect a node - * @private - */ - onTap(event) { - var pointer = this.getPointer(event.center); - this.pointerPosition = pointer; - this.selectionHandler.selectOnPoint(pointer); - } - - /** - * handle drag start event - * @private - */ - - /** - * This function is called by onDragStart. - * It is separated out because we can then overload it for the datamanipulation system. - * - * @private - */ - 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); - //} - // - //var node = this._getNodeAt(this.drag.pointer); - //// note: drag.pointer is set in onTouch to get the initial touch location - // - //this.drag.dragging = true; - //this.drag.selection = []; - //this.drag.translation = this._getTranslation(); - //this.drag.nodeId = null; - //this.draggingNodes = false; - // - //if (node != null && this.constants.dragNodes == true) { - // this.draggingNodes = true; - // this.drag.nodeId = node.id; - // // select the clicked node if not yet selected - // if (!node.isSelected()) { - // this._selectObject(node, false); - // } - // - // this.emit("dragStart", {nodeIds: this.getSelection().nodes}); - // - // // create an array with the selected nodes and their original location and status - // for (var objectId in this.selectionObj.nodes) { - // if (this.selectionObj.nodes.hasOwnProperty(objectId)) { - // var object = this.selectionObj.nodes[objectId]; - // var s = { - // id: object.id, - // node: object, - // - // // store original x, y, xFixed and yFixed, make the node temporarily Fixed - // x: object.x, - // y: object.y, - // xFixed: object.xFixed, - // yFixed: object.yFixed - // }; - // - // object.xFixed = true; - // object.yFixed = true; - // - // this.drag.selection.push(s); - // } - // } - //} - } - - - /** - * handle drag event - * @private - */ - onDrag(event) { - //if (this.drag.pinched) { - // return; - //} - // - //// remove the focus on node if it is focussed on by the focusOnNode - //this.releaseNode(); - // - //var pointer = this.getPointer(event.gesture.center); - //var me = this; - //var drag = this.drag; - //var selection = drag.selection; - //if (selection && selection.length && this.constants.dragNodes == true) { - // // calculate delta's and new location - // var deltaX = pointer.x - drag.pointer.x; - // var deltaY = pointer.y - drag.pointer.y; - // - // // update position of all selected nodes - // selection.forEach(function (s) { - // var node = s.node; - // - // if (!s.xFixed) { - // node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); - // } - // - // if (!s.yFixed) { - // node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); - // } - // }); - // - // - // // start _animationStep if not yet running - // if (!this.moving) { - // this.moving = true; - // this.start(); - // } - //} - //else { - // // move the network - // if (this.constants.dragNetwork == 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; - // - // this._setTranslation( - // this.drag.translation.x + diffX, - // this.drag.translation.y + diffY - // ); - // this._redraw(); - // } - //} - } - - - /** - * handle drag start event - * @private - */ - 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.xFixed = s.xFixed; - // s.node.yFixed = s.yFixed; - // }); - // this.moving = true; - // this.start(); - //} - //else { - // this._redraw(); - //} - //if (this.draggingNodes == false) { - // this.emit("dragEnd", {nodeIds: []}); - //} - //else { - // this.emit("dragEnd", {nodeIds: this.getSelection().nodes}); - //} - } - - /** - * handle doubletap event - * @private - */ - onDoubleTap(event) { - //var pointer = this.getPointer(event.gesture.center); - //this._handleDoubleTap(pointer); - } - - - - /** - * handle long tap event: multi select nodes - * @private - */ - onHold(event) { - //var pointer = this.getPointer(event.gesture.center); - //this.pointerPosition = pointer; - //this._handleOnHold(pointer); - } - - - /** - * handle the release of the screen - * - * @private - */ - onRelease(event) { - //var pointer = this.getPointer(event.gesture.center); - //this._handleOnRelease(pointer); - } - - - /** - * Handle pinch event - * @param event - * @private - */ - onPinch(event) { - //var pointer = this.getPointer(event.gesture.center); - // - //this.drag.pinched = true; - //if (!('scale' in this.pinch)) { - // this.pinch.scale = 1; - //} - // - //// TODO: enabled moving while pinching? - //var scale = this.pinch.scale * event.gesture.scale; - //this._zoom(scale, pointer) - } - - - /** - * Zoom the 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 - */ - _zoom(scale, pointer) { - //if (this.constants.zoomable == true) { - // var scaleOld = this._getScale(); - // if (scale < 0.00001) { - // scale = 0.00001; - // } - // if (scale > 10) { - // scale = 10; - // } - // - // var preScaleDragPointer = null; - // if (this.drag !== undefined) { - // if (this.drag.dragging == true) { - // preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); - // } - // } - // // + this.canvas.frame.canvas.clientHeight / 2 - // var translation = this._getTranslation(); - // - // var scaleFrac = scale / scaleOld; - // var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - // var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; - // - // this._setScale(scale); - // this._setTranslation(tx, ty); - // - // if (preScaleDragPointer != null) { - // var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); - // this.drag.pointer.x = postScaleDragPointer.x; - // this.drag.pointer.y = postScaleDragPointer.y; - // } - // - // this._redraw(); - // - // if (scaleOld < scale) { - // this.emit("zoom", {direction: "+"}); - // } - // else { - // this.emit("zoom", {direction: "-"}); - // } - // - // return scale; - //} - } - - - /** - * 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 - */ - 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) { - // - // // calculate the new scale - // var scale = this._getScale(); - // var zoom = delta / 10; - // if (delta < 0) { - // zoom = zoom / (1 - zoom); - // } - // scale *= (1 + zoom); - // - // // calculate the pointer location - // var gesture = hammerUtil.fakeGesture(this, event); - // var pointer = this.getPointer(gesture.center); - // - // // apply the new scale - // this._zoom(scale, pointer); - //} - // - //// Prevent default actions caused by mouse wheel. - //event.preventDefault(); - } - - - /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private - */ - onMouseMove(event) { - //var gesture = hammerUtil.fakeGesture(this, event); - //var pointer = this.getPointer(gesture.center); - //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(); - // } - //} - // - //// if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over - //if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { - // this.canvas.frame.focus(); - //} - // - //// start a timeout that will check if the mouse is positioned above an element - //if (popupVisible === false) { - // var me = this; - // var checkShow = function() { - // me._checkShowPopup(pointer); - // }; - // - // if (this.popupTimer) { - // clearInterval(this.popupTimer); // stop any running calculationTimer - // } - // if (!this.drag.dragging) { - // this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); - // } - //} - // - ///** - // * Adding hover highlights - // */ - //if (this.constants.hover == true) { - // // removing all hover highlights - // for (var edgeId in this.hoverObj.edges) { - // if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - // this.hoverObj.edges[edgeId].hover = false; - // delete this.hoverObj.edges[edgeId]; - // } - // } - // - // // adding hover highlights - // var obj = this._getNodeAt(pointer); - // if (obj == null) { - // obj = this._getEdgeAt(pointer); - // } - // if (obj != null) { - // this._hoverObject(obj); - // } - // - // // removing all node hover highlights except for the selected one. - // for (var nodeId in this.hoverObj.nodes) { - // if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - // if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { - // this._blurObject(this.hoverObj.nodes[nodeId]); - // delete this.hoverObj.nodes[nodeId]; - // } - // } - // } - // this.redraw(); - //} - } -} - -export {TouchEventHandler}; diff --git a/lib/network/modules/View.js b/lib/network/modules/View.js index 58a3cb45..81f9e5ba 100644 --- a/lib/network/modules/View.js +++ b/lib/network/modules/View.js @@ -5,9 +5,9 @@ var util = require('../../util'); class View { - constructor(body, options) { + constructor(body, canvas) { this.body = body; - this.setOptions(options); + this.canvas = canvas; this.animationSpeed = 1/this.renderRefreshRate; this.animationEasingFunction = "easeInOutQuint"; @@ -20,14 +20,9 @@ class View { this.lockedOnNodeOffset = null; this.touchTime = 0; - this.translation = {x: 0, y: 0}; - this.scale = 1.0; - this.viewFunction = undefined; this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); - this.body.emitter.on("_setScale", (scale) => this.scale = scale); - this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); this.body.emitter.on("animationFinished", () => {this.body.emitter.emit("_stopRendering");}); this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); } @@ -37,9 +32,6 @@ class View { this.options = options; } - setCanvas(canvas) { - this.canvas = canvas; - } // zoomExtent /** @@ -144,7 +136,7 @@ class View { zoomLevel *= factor; } else { - this.body.emitter.emit("_redrawHidden"); + 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; @@ -200,8 +192,8 @@ class View { 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.scale; } - if (options.position === undefined) {options.position = this.translation;} + 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 = {}; } @@ -239,13 +231,13 @@ class View { this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. } - this.sourceScale = this.scale; - this.sourceTranslation = this.translation; + 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.emitter.emit("_setScale",this.targetScale); + 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, @@ -263,8 +255,8 @@ class View { this.body.emitter.on("_beforeRender", this.viewFunction); } else { - this.body.emitter.emit("_setScale", this.targetScale);; - this.body.emitter.emit("_setTranslation", this.targetTranslation); + this.body.view.scale = this.targetScale; + this.body.view.translation = this.targetTranslation; this.body.emitter.emit("_requestRedraw"); } } @@ -290,13 +282,13 @@ class View { x: viewCenter.x - nodePosition.x, y: viewCenter.y - nodePosition.y }; - var sourceTranslation = this.translation; + var sourceTranslation = this.body.view.translation; var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y + 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.emitter.emit("_setTranslation", targetTranslation); + this.body.view.translation = targetTranslation; } releaseNode() { @@ -318,11 +310,11 @@ class View { var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - this.body.emitter.emit("_setScale", this.sourceScale + (this.targetScale - this.sourceScale) * progress); - this.body.emitter.emit("_setTranslation", { + 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.0) { diff --git a/lib/util.js b/lib/util.js index 8184f42b..eb266105 100644 --- a/lib/util.js +++ b/lib/util.js @@ -114,7 +114,6 @@ exports.extend = function (a, b) { } } } - return a; };