From b954d9c9ebe7ee3272229f1fdb4cdecf93a1d35c Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 26 Mar 2015 16:45:53 +0100 Subject: [PATCH] start of configuration system --- dist/vis.js | 58020 ++++++++-------- examples/network/01_basic_usage.html | 17 +- lib/network/Network.js | 8 +- lib/network/css/configuration.css | 11 + lib/network/modules/Clustering.js | 6 + lib/network/modules/ConfigurationSystem.js | 316 + lib/network/modules/PhysicsEngine.js | 38 +- lib/network/modules/SelectionHandler.js | 3 +- .../modules/components/edges/util/EdgeBase.js | 2 +- .../components/physics/BarnesHutSolver.js | 19 +- 10 files changed, 29560 insertions(+), 28880 deletions(-) create mode 100644 lib/network/css/configuration.css create mode 100644 lib/network/modules/ConfigurationSystem.js diff --git a/dist/vis.js b/dist/vis.js index f16e39c0..ade3c861 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -85,63 +85,63 @@ return /******/ (function(modules) { // webpackBootstrap // utils exports.util = __webpack_require__(1); - exports.DOMutil = __webpack_require__(2); + exports.DOMutil = __webpack_require__(6); // data - exports.DataSet = __webpack_require__(3); - exports.DataView = __webpack_require__(4); - exports.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 = { - Images: __webpack_require__(37), - dotparser: __webpack_require__(38), - gephiParser: __webpack_require__(39) + Images: __webpack_require__(57), + dotparser: __webpack_require__(55), + gephiParser: __webpack_require__(56) }; // Deprecated since v3.0.0 @@ -150,9 +150,9 @@ return /******/ (function(modules) { // webpackBootstrap }; // bundled external libraries - exports.moment = __webpack_require__(40); - exports.hammer = __webpack_require__(41); // TODO: deprecate exports.hammer some day - exports.Hammer = __webpack_require__(41); + exports.moment = __webpack_require__(2); + exports.hammer = __webpack_require__(19); // TODO: deprecate exports.hammer some day + exports.Hammer = __webpack_require__(19); /***/ }, /* 1 */ @@ -164,7 +164,7 @@ return /******/ (function(modules) { // webpackBootstrap // first check if moment.js is already loaded in the browser window, if so, // use this instance. Else, load via commonjs. - var moment = __webpack_require__(40); + var moment = __webpack_require__(2); /** * Test whether given object is a number @@ -1427,30104 +1427,29809 @@ 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); - } - - return addedIds.concat(updatedIds); - }; - - /** - * Get a data item or multiple items. - * - * Usage: - * - * get() - * get(options: Object) - * get(options: Object, data: Array | DataTable) - * - * get(id: Number | String) - * get(id: Number | String, options: Object) - * get(id: Number | String, options: Object, data: Array | DataTable) - * - * get(ids: Number[] | String[]) - * get(ids: Number[] | String[], options: Object) - * get(ids: Number[] | String[], options: Object, data: Array | DataTable) - * - * Where: - * - * {Number | String} id The id of an item - * {Number[] | String{}} ids An array with ids of items - * {Object} options An Object with options. Available options: - * {String} [returnType] Type of data to be - * returned. Can be 'DataTable' or 'Array' (default) - * {Object.} [type] - * {String[]} [fields] field names to be returned - * {function} [filter] filter items - * {String | function} [order] Order the items by - * a field name or custom sort function. - * {Array | DataTable} [data] If provided, items will be appended to this - * array or table. Required in case of Google - * DataTable. - * - * @throws Error - */ - DataSet.prototype.get = function (args) { - var me = this; - - // parse the arguments - var id, ids, options, data; - var firstType = util.getType(arguments[0]); - if (firstType == "String" || firstType == "Number") { - // get(id [, options] [, data]) - id = arguments[0]; - options = arguments[1]; - data = arguments[2]; - } else if (firstType == "Array") { - // get(ids [, options] [, data]) - ids = arguments[0]; - options = arguments[1]; - data = arguments[2]; - } else { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; - } - - // determine the return type - var returnType; - if (options && options.returnType) { - var allowedValues = ["DataTable", "Array", "Object"]; - returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; - - if (data && returnType != util.getType(data)) { - throw new Error("Type of parameter \"data\" (" + util.getType(data) + ") " + "does not correspond with specified options.type (" + options.type + ")"); - } - if (returnType == "DataTable" && !util.isDataTable(data)) { - throw new Error("Parameter \"data\" must be a DataTable " + "when options.type is \"DataTable\""); + function hasOwnProp(a, b) { + return hasOwnProperty.call(a, b); } - } else if (data) { - returnType = util.getType(data) == "DataTable" ? "DataTable" : "Array"; - } else { - returnType = "Array"; - } - // build options - var type = options && options.type || this._options.type; - var filter = options && options.filter; - var items = [], - item, - itemId, - i, - len; - - // convert items - if (id != undefined) { - // return a single item - item = me._getItem(id, type); - if (filter && !filter(item)) { - item = null; - } - } else if (ids != undefined) { - // return a subset of items - for (i = 0, len = ids.length; i < len; i++) { - item = me._getItem(ids[i], type); - if (!filter || filter(item)) { - items.push(item); - } + 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 + }; } - } else { - // return all items - for (itemId in this._data) { - if (this._data.hasOwnProperty(itemId)) { - item = me._getItem(itemId, type); - if (!filter || filter(item)) { - items.push(item); + + function printMsg(msg) { + if (moment.suppressDeprecationWarnings === false && + typeof console !== 'undefined' && console.warn) { + console.warn('Deprecation warning: ' + msg); } - } } - } - // order the results - if (options && options.order && id == undefined) { - this._sort(items, options.order); - } + function deprecate(msg, fn) { + var firstTime = true; + return extend(function () { + if (firstTime) { + printMsg(msg); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); + } - // 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); - } + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + printMsg(msg); + deprecations[name] = true; + } } - } - // 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]); - } + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; } - return data; - } else if (returnType == "Object") { - var result = {}; - for (i = 0; i < items.length; i++) { - result[items[i].id] = items[i]; + function ordinalizeToken(func, period) { + return function (a) { + return this.localeData().ordinal(func.call(this, a), period); + }; } - 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; - } + + function monthDiff(a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + return -(wholeMonthDiff + adjust); } - } - }; - /** - * 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 = []; + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); + } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); - 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); + this._bubble(); } - } else { - // unordered - for (id in data) { - if (data.hasOwnProperty(id)) { - item = this._getItem(id, type); - if (!filter || filter(item)) { - callback(item, id); + + /************************************ + Helpers + ************************************/ + + + function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } } - } - } - } - }; - /** - * 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; + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } - // 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)); - } + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } + + return a; } - } - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); - } + function copyConfig(to, from) { + var i, prop, val; - return mappedItems; - }; + 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; + } - /** - * 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 (momentProperties.length > 0) { + for (i in momentProperties) { + prop = momentProperties[i]; + val = from[prop]; + if (typeof val !== 'undefined') { + to[prop] = val; + } + } + } - var filteredItem = {}; + return to; + } - for (var field in item) { - if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) { - filteredItem[field] = item[field]; + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } } - } - return filteredItem; - }; + // 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; - /** - * 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"); - } - }; + while (output.length < targetLength) { + output = '0' + output; + } + return (sign ? (forceSign ? '+' : '') : '-') + output; + } - /** - * 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; + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - 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); - } - } + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } - if (removedIds.length) { - this._trigger("remove", { items: removedIds }, senderId); - } + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - return removedIds; - }; + return res; + } - /** - * Remove an item by its id - * @param {Number | String | Object} id id or item - * @returns {Number | String | null} id - * @private - */ - DataSet.prototype._remove = function (id) { - if (util.isNumber(id) || util.isString(id)) { - if (this._data[id]) { - delete this._data[id]; - this.length--; - return id; + function 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; + } + + return res; } - } else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - this.length--; - return itemId; + + // 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; + } + + val = typeof val === 'string' ? +val : val; + dur = moment.duration(val, period); + addOrSubtractDurationFromMoment(this, dur, direction); + return this; + }; } - } - 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 addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = duration._days, + months = duration._months; + updateOffset = updateOffset == null ? true : updateOffset; - this._data = {}; - this.length = 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._trigger("remove", { items: ids }, senderId); + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } - return ids; - }; + function isDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || + input instanceof Date; + } - /** - * 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; + // 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; + } - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!max || itemField > maxField)) { - max = item; - maxField = itemField; - } + function normalizeUnits(units) { + if (units) { + var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); + units = unitAliases[units] || camelFunctions[lowered] || lowered; + } + return units; } - } - return max; - }; + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; - /** - * Find the item with minimum value of a specified field - * @param {String} field - * @return {Object | null} item Item containing max value, or null if no items - */ - DataSet.prototype.min = function (field) { - var data = this._data, - min = null, - minField = null; + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } - for (var id in data) { - if (data.hasOwnProperty(id)) { - var item = data[id]; - var itemField = item[field]; - if (itemField != null && (!min || itemField < minField)) { - min = item; - minField = itemField; - } + return normalizedInput; } - } - - return min; - }; - /** - * Find all distinct values of a specified field - * @param {String} field - * @return {Array} values Array containing all distinct values. If data items - * do not contain the specified field are ignored. - * The returned array is unordered. - */ - DataSet.prototype.distinct = function (field) { - var data = this._data; - var values = []; - var fieldType = this._options.type && this._options.type[field] || null; - var count = 0; - var i; + function makeList(field) { + var count, setter; - 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 (field.indexOf('week') === 0) { + count = 7; + setter = 'day'; } - } - if (!exists && value !== undefined) { - values[count] = value; - count++; - } + 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 = []; + + if (typeof format === 'number') { + index = format; + format = undefined; + } + + getter = function (i) { + var m = moment().utc().set(setter, i); + return method.call(moment._locale, m, format || ''); + }; + + if (index != null) { + return getter(index); + } + else { + for (i = 0; i < count; i++) { + results.push(getter(i)); + } + return results; + } + }; } - } - if (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + if (coercedNumber >= 0) { + value = Math.floor(coercedNumber); + } else { + value = Math.ceil(coercedNumber); + } + } + + return value; } - } - return values; - }; + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } - /** - * 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]; + function weeksInYear(year, dow, doy) { + return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; + } - 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"); + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; } - } else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; - } - var d = {}; - for (var field in item) { - if (item.hasOwnProperty(field)) { - var fieldType = this._type[field]; // type may be undefined - d[field] = util.convert(item[field], fieldType); + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } - } - this._data[id] = d; - this.length++; - 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 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 (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; - } + m._pf.overflow = overflow; + } + } - // 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 isValid(m) { + if (m._isValid == null) { + m._isValid = !isNaN(m._d.getTime()) && + m._pf.overflow < 0 && + !m._pf.empty && + !m._pf.invalidMonth && + !m._pf.nullInput && + !m._pf.invalidFormat && + !m._pf.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + m._pf.charsLeftOver === 0 && + m._pf.unusedTokens.length === 0 && + m._pf.bigHour === undefined; + } + } + return m._isValid; } - } else { - // no field types specified, no converting needed - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - converted[field] = value; - } + + function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; } - } - 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"); - } + // 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; - // 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); + 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; } - } - - return id; - }; - /** - * Get an array with the column names of a Google DataTable - * @param {DataTable} dataTable - * @return {String[]} columnNames - * @private - */ - DataSet.prototype._getColumnNames = function (dataTable) { - var columns = []; - for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) { - columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col); - } - return columns; - }; + 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]; + } - /** - * 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(); + // 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(); + } + } - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - dataTable.setValue(row, col, item[field]); - } - }; + /************************************ + Locale + ************************************/ - module.exports = DataSet; -/***/ }, -/* 4 */ -/***/ function(module, exports, __webpack_require__) { + extend(Locale.prototype, { - "use strict"; + 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 util = __webpack_require__(1); - var DataSet = __webpack_require__(3); + _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + months : function (m) { + return this._months[m.month()]; + }, - /** - * 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 + _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + monthsParse : function (monthName, format, strict) { + var i, mom, regex; - this.setData(data); - } + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } - // TODO: implement a function .config() to dynamically update things like configured filter - // and trigger changes accordingly + 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; + } + } + }, - /** - * Set a data source for the view - * @param {DataSet | DataView} data - */ - DataView.prototype.setData = function (data) { - var ids, i, len; + _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, - if (this._data) { - // unsubscribe from current dataset - if (this._data.unsubscribe) { - this._data.unsubscribe("*", this.listener); - } + _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, - // 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 }); - } + _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, - this._data = data; + weekdaysParse : function (weekdayName) { + var i, mom, regex; - if (this._data) { - // update fieldId - this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || "id"; + if (!this._weekdaysParse) { + this._weekdaysParse = []; + } - // 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 }); + 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; + } + } + }, - // subscribe to new dataset - if (this._data.on) { - this._data.on("*", this.listener); - } - } - }; + _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; + }, - /** - * 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 = []; + 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'); + }, - // 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++; - } - } + _meridiemParse : /[ap]\.?m?\.?/i, + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, - // check for removals - for (id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - if (!newIds[id]) { - removed.push(id); - delete this._ids[id]; - this.length--; - } - } - } - // trigger events - if (added.length) { - this._trigger("add", { items: added }); - } - if (removed.length) { - this._trigger("remove", { items: removed }); - } - }; + _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 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; + _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' + }, - // 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]; - } + 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); + }, - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, - // 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); - }; - } + ordinal : function (number) { + return this._ordinal.replace('%d', number); + }, + _ordinal : '%d', + _ordinalParse : /\d{1,2}/, - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); - } - getArguments.push(viewOptions); - getArguments.push(data); + preparse : function (string) { + return string; + }, - return this._data && this._data.get.apply(this._data, getArguments); - }; + postformat : function (string) { + return string; + }, - /** - * 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; + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + }, - if (this._data) { - var defaultFilter = this._options.filter; - var filter; + _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. + }, - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); - }; - } else { - filter = options.filter; - } - } else { - filter = defaultFilter; - } + firstDayOfWeek : function () { + return this._week.dow; + }, - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); - } else { - ids = []; - } + firstDayOfYear : function () { + return this._week.doy; + }, - return ids; - }; + _invalidDate: 'Invalid date', + invalidDate: function () { + return this._invalidDate; + } + }); - /** - * 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; - }; + /************************************ + Formatting + ************************************/ - /** - * Event listener. Will propagate all events from the connected data set to - * the subscribers of the DataView, but will filter the items and only trigger - * when there are changes in the filtered data set. - * @param {String} event - * @param {Object | null} params - * @param {String} senderId - * @private - */ - DataView.prototype._onEvent = function (event, params, senderId) { - var i, - len, - id, - item, - ids = params && params.items, - data = this._data, - added = [], - updated = [], - removed = []; - if (ids && data) { - switch (event) { - case "add": - // filter the ids of the added items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); - if (item) { - this._ids[id] = true; - added.push(id); - } + function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); } + return input.replace(/\\/g, ''); + } - break; - - case "update": - // determine the event from the views viewpoint: an updated - // item can be added, updated, or removed from this view. - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - item = this.get(id); + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; - if (item) { - if (this._ids[id]) { - updated.push(id); + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; } else { - this._ids[id] = true; - added.push(id); + array[i] = removeFormattingTokens(array[i]); } - } else { - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } else {} - } } - break; + 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; + }; + } - case "remove": - // filter the ids of the removed items - for (i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - if (this._ids[id]) { - delete this._ids[id]; - removed.push(id); - } + // format date using native date object + function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); } - break; - } + format = expandFormat(format, m.localeData()); - this.length += added.length - removed.length; + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } - 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); + return formatFunctions[format](m); } - } - }; - - // copy subscription functionality from DataSet - DataView.prototype.on = DataSet.prototype.on; - DataView.prototype.off = DataSet.prototype.off; - DataView.prototype._trigger = DataSet.prototype._trigger; - - // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) - DataView.prototype.subscribe = DataView.prototype.on; - DataView.prototype.unsubscribe = DataView.prototype.off; - - module.exports = DataView; - // nothing interesting for me :-( - -/***/ }, -/* 5 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - /** - * A queue - * @param {Object} options - * Available options: - * - delay: number When provided, the queue will be flushed - * automatically after an inactivity of this delay - * in milliseconds. - * Default value is null. - * - max: number When the queue exceeds the given maximum number - * of entries, the queue is flushed automatically. - * Default value of max is Infinity. - * @constructor - */ - function Queue(options) { - // options - this.delay = null; - this.max = Infinity; - // properties - this._queue = []; - this._timeout = null; - this._extended = null; + function expandFormat(format, locale) { + var i = 5; - this.setOptions(options); - } + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } - /** - * 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; - } + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + localFormattingTokens.lastIndex = 0; + i -= 1; + } - this._flushIfNeeded(); - }; + return format; + } - /** - * 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(); - }; + /************************************ + Parsing + ************************************/ - var methods = [{ - name: "flush", - original: undefined - }]; - if (options && options.replace) { - for (var i = 0; i < options.replace.length; i++) { - var name = options.replace[i]; - methods.push({ - name: name, - original: object[name] - }); - queue.replace(object, name); + // 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; + } } - } - - queue._extended = { - object: object, - methods: methods - }; - return queue; - }; - - /** - * Destroy the queue. The queue will first flush all queued actions, and in - * case it has extended an object, will restore the original object. - */ - Queue.prototype.destroy = function () { - this.flush(); + 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 (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]; - } + return parts[0] === '+' ? minutes : -minutes; } - 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 to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, datePartArray = config._a; - object[method] = function () { - // create an Array with the arguments - var args = []; - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i]; - } + 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); + } - // add this call to the queue - me.queue({ - args: args, - fn: original, - context: this - }); - }; - }; + 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); + } + } - /** - * 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); - } + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp; - this._flushIfNeeded(); - }; + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - /** - * 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(); - } + // 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; - // 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); - } - }; + weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); + week = dfl(w.w, 1); - /** - * 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 (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); - module.exports = Queue; + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { + // 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; - "use strict"; + if (config._d) { + return; + } - 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); + currentDate = currentDateArray(config); - /** - * @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"); - } + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } - // create variables and set default values - this.containerElement = container; - this.width = "400px"; - this.height = "400px"; - this.margin = 10; // px - this.defaultXCenter = "55%"; - this.defaultYCenter = "50%"; + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); - this.xLabel = "x"; - this.yLabel = "y"; - this.zLabel = "z"; + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } - var passValueFn = function (v) { - return v; - }; - this.xValueLabel = passValueFn; - this.yValueLabel = passValueFn; - this.zValueLabel = passValueFn; + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } - this.filterLabel = "time"; - this.legendLabel = "value"; + // 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.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' + // 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]; + } - this.animationInterval = 1000; // milliseconds - this.animationPreload = false; + // 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.camera = new Camera(); - this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? + 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.dataTable = null; // The original data table - this.dataPoints = null; // The table with point objects + if (config._nextDay) { + config._a[HOUR] = 24; + } + } - // the column indexes - this.colX = undefined; - this.colY = undefined; - this.colZ = undefined; - this.colValue = undefined; - this.colFilter = undefined; + function dateFromObject(config) { + var normalizedInput; - 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 + if (config._d) { + return; + } - // constants - this.colorAxis = "#4D4D4D"; - this.colorGrid = "#D3D3D3"; - this.colorDot = "#7DC1FF"; - this.colorDotBorder = "#3267D2"; + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day || normalizedInput.date, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; - // create a frame and canvas - this.create(); + dateFromConfig(config); + } - // apply options (also when undefined) - this.setOptions(options); + 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()]; + } + } - // apply data - if (data) { - this.setData(data); - } - } + // date from string and format string + function makeDateFromStringAndFormat(config) { + if (config._f === moment.ISO_8601) { + parseISO(config); + return; + } - // Extend Graph3d with an Emitter mixin - Emitter(Graph3d.prototype); + config._a = []; + config._pf.empty = true; - /** - * 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)); + // 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; - // 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; - } - } + tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - // scale the vertical axis - this.scale.z *= this.verticalRatio; - // TODO: can this be automated? verticalRatio? + 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); + } + } - // determine scale for (optional) value - this.scale.value = 1 / (this.valueMax - this.valueMin); + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); + } - // 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); - }; + // 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); + } + 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')), - 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 = 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); } - } - 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; + } - if (this.defaultYBarWidth !== undefined) { - this.yBarWidth = this.defaultYBarWidth; - } else { - var dataY = this.getDistinctValues(data, this.colY); - this.yBarWidth = dataY[1] - dataY[0] || 1; + 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 = function (input, format, locale, strict) { + var c; - 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; + if (typeof(locale) === 'boolean') { + strict = locale; + locale = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._i = input; + c._f = format; + c._l = locale; + c._strict = strict; + c._isUTC = false; + c._pf = defaultParsingFlags(); - var 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 makeMoment(c); + }; - if (this.colValue !== undefined) { - var valueRange = this.getColumnRange(data, this.colValue); - this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min; - this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max; - if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1; - } + moment.suppressDeprecationWarnings = false; - // set the scale dependent on the ranges. - this._setScale(); - }; + 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; + } + moment.min = function () { + var args = [].slice.call(arguments, 0); - /** - * Filter the data based on the current filter - * @param {Array} data - * @return {Array} dataPoints Array with point objects which can be drawn on screen - */ - Graph3d.prototype._getDataPoints = function (data) { - // TODO: store the created matrix dataPoints in the filters instead of reloading each time - var x, y, i, z, obj, point; + return pickBy('isBefore', args); + }; - var dataPoints = []; + moment.max = function () { + var args = [].slice.call(arguments, 0); - 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 + return pickBy('isAfter', args); + }; - // 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; + // creating with utc + moment.utc = function (input, format, locale, strict) { + var c; - if (dataX.indexOf(x) === -1) { - dataX.push(x); - } - if (dataY.indexOf(y) === -1) { - dataY.push(y); - } - } + 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(); - var sortNumber = function (a, b) { - return a - b; + return makeMoment(c).utc(); }; - dataX.sort(sortNumber); - dataY.sort(sortNumber); - - // create a grid, a 2d matrix, with all values. - var dataMatrix = []; // temporary data matrix - for (i = 0; i < data.length; i++) { - x = data[i][this.colX] || 0; - y = data[i][this.colY] || 0; - z = data[i][this.colZ] || 0; - - var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer - var yIndex = dataY.indexOf(y); - if (dataMatrix[xIndex] === undefined) { - dataMatrix[xIndex] = []; - } + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; - var point3d = new Point3d(); - point3d.x = x; - point3d.y = y; - point3d.z = z; + // duration + moment.duration = function (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso, + diffRes; - obj = {}; - obj.point = point3d; - obj.trans = undefined; - obj.screen = undefined; - obj.bottom = new Point3d(x, y, this.zMin); + 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)); - dataMatrix[xIndex][yIndex] = obj; + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } - dataPoints.push(obj); - } + ret = new Duration(duration); - // 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; + if (moment.isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; } - } - } - } 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 ret; + }; - obj = {}; - obj.point = point; - obj.bottom = new Point3d(point.x, point.y, this.zMin); - obj.trans = undefined; - obj.screen = undefined; + // version number + moment.version = VERSION; - dataPoints.push(obj); - } - } + // default format + moment.defaultFormat = isoFormat; - return dataPoints; - }; + // constant that refers to the ISO standard + moment.ISO_8601 = function () {}; - /** - * 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); - } + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + moment.momentProperties = momentProperties; - this.frame = document.createElement("div"); - this.frame.style.position = "relative"; - this.frame.style.overflow = "hidden"; + // 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 () {}; - // create the graph canvas (HTML canvas element) - this.frame.canvas = document.createElement("canvas"); - this.frame.canvas.style.position = "relative"; - this.frame.appendChild(this.frame.canvas); - //if (!this.frame.canvas.getContext) { - { - var noCanvas = document.createElement("DIV"); - noCanvas.style.color = "red"; - noCanvas.style.fontWeight = "bold"; - noCanvas.style.padding = "10px"; - noCanvas.innerHTML = "Error: your browser does not support HTML canvas"; - this.frame.canvas.appendChild(noCanvas); - } + // This 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; + }; - this.frame.filter = document.createElement("div"); - this.frame.filter.style.position = "absolute"; - this.frame.filter.style.bottom = "0px"; - this.frame.filter.style.left = "0px"; - this.frame.filter.style.width = "100%"; - this.frame.appendChild(this.frame.filter); + moment.lang = deprecate( + 'moment.lang is deprecated. Use moment.locale instead.', + function (key, value) { + return moment.locale(key, value); + } + ); - // 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 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); + } - 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); + if (data) { + moment.duration._locale = moment._locale = data; + } + } - // add the new graph to the container element - this.containerElement.appendChild(this.frame); - }; + return moment._locale._abbr; + }; + moment.defineLocale = function (name, values) { + if (values !== null) { + values.abbr = name; + if (!locales[name]) { + locales[name] = new Locale(); + } + locales[name].set(values); - /** - * 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; + // backwards compat for now: also set the locale + moment.locale(name); - this._resizeCanvas(); - }; + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } + }; - /** - * 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.langData = deprecate( + 'moment.langData is deprecated. Use moment.localeData instead.', + function (key) { + return moment.localeData(key); + } + ); - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; + // returns locale data + moment.localeData = function (key) { + var locale; - // adjust with for margin - this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + "px"; - }; + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } - /** - * Start animation - */ - Graph3d.prototype.animationStart = function () { - if (!this.frame.filter || !this.frame.filter.slider) throw "No animation available"; + if (!key) { + return moment._locale; + } - this.frame.filter.slider.play(); - }; + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + return chooseLocale(key); + }; - /** - * Stop animation - */ - Graph3d.prototype.animationStop = function () { - if (!this.frame.filter || !this.frame.filter.slider) return; + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment || + (obj != null && hasOwnProp(obj, '_isAMomentObject')); + }; - this.frame.filter.slider.stop(); - }; + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; + + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); + } + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; - /** - * Resize the center position based on the current values in this.defaultXCenter - * and this.defaultYCenter (which are strings with a percentage or a value - * in pixels). The center positions are the variables this.xCenter - * and this.yCenter - */ - Graph3d.prototype._resizeCenter = function () { - // calculate the horizontal center position - if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === "%") { - this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth; - } else { - this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px - } + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; + } - // 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 - } - }; + return m; + }; - /** - * Set the rotation and distance of the camera - * @param {Object} pos An object with the camera position. The object - * contains three parameters: - * - horizontal {Number} - * The horizontal rotation, between 0 and 2*PI. - * Optional, can be left undefined. - * - vertical {Number} - * The vertical rotation, between 0 and 0.5*PI - * if vertical=0.5*PI, the graph is shown from the - * top. Optional, can be left undefined. - * - distance {Number} - * The (normalized) distance of the camera to the - * center of the graph, a value between 0.71 and 5.0. - * Optional, can be left undefined. - */ - Graph3d.prototype.setCameraPosition = function (pos) { - if (pos === undefined) { - return; - } + moment.parseZone = function () { + return moment.apply(null, arguments).parseZone(); + }; - if (pos.horizontal !== undefined && pos.vertical !== undefined) { - this.camera.setArmRotation(pos.horizontal, pos.vertical); - } + moment.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; - if (pos.distance !== undefined) { - this.camera.setArmLength(pos.distance); - } + moment.isDate = isDate; - this.redraw(); - }; + /************************************ + Moment Prototype + ************************************/ - /** - * 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; - }; + extend(moment.fn = Moment.prototype, { - /** - * Load data into the 3D Graph - */ - Graph3d.prototype._readData = function (data) { - // read the data - this._dataInitialize(data, this.style); + clone : function () { + return moment(this); + }, + valueOf : function () { + return +this._d - ((this._offset || 0) * 60000); + }, - if (this.dataFilter) { - // apply filtering - this.dataPoints = this.dataFilter._getDataPoints(); - } else { - // no filtering. load all data - this.dataPoints = this._getDataPoints(this.dataTable); - } + unix : function () { + return Math.floor(+this / 1000); + }, - // draw the filter - this._redrawFilter(); - }; + toString : function () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + }, - /** - * Replace the dataset of the Graph3d - * @param {Array | DataSet | DataView} data - */ - Graph3d.prototype.setData = function (data) { - this._readData(data); - this.redraw(); + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + 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]'); + } + }, - /** - * Update the options. Options will be merged with current options - * @param {Object} options - */ - Graph3d.prototype.setOptions = function (options) { - var cameraPosition = undefined; + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, - this.animationStop(); + isValid : function () { + return isValid(this); + }, - if (options !== undefined) { - // retrieve parameter values - if (options.width !== undefined) this.width = options.width; - if (options.height !== undefined) this.height = options.height; + isDSTShifted : function () { + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } - if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter; - if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter; + return false; + }, - 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; + parsingFlags : function () { + return extend({}, this._pf); + }, - if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel; - if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel; - if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel; + invalidAt: function () { + return this._pf.overflow; + }, - 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; + utc : function (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + }, - if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval; - if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload; - if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart; + local : function (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; - if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth; - if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth; + if (keepLocalTime) { + this.subtract(this._dateUtcOffset(), 'm'); + } + } + return this; + }, - if (options.xMin !== undefined) this.defaultXMin = options.xMin; - if (options.xStep !== undefined) this.defaultXStep = options.xStep; - if (options.xMax !== undefined) this.defaultXMax = options.xMax; - if (options.yMin !== undefined) this.defaultYMin = options.yMin; - if (options.yStep !== undefined) this.defaultYStep = options.yStep; - if (options.yMax !== undefined) this.defaultYMax = options.yMax; - if (options.zMin !== undefined) this.defaultZMin = options.zMin; - if (options.zStep !== undefined) this.defaultZStep = options.zStep; - if (options.zMax !== undefined) this.defaultZMax = options.zMax; - if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin; - if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax; + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.localeData().postformat(output); + }, - if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition; + add : createAdder(1, 'add'), - 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); - } - } + subtract : createAdder(-1, 'subtract'), - this._setBackgroundColor(options && options.backgroundColor); + diff : function (input, units, asFloat) { + var that = makeAs(input, this), + zoneDiff = (that.utcOffset() - this.utcOffset()) * 6e4, + anchor, diff, output, daysAdjust; - this.setSize(this.width, this.height); + units = normalizeUnits(units); - // re-load the data - if (this.dataTable) { - this.setData(this.dataTable); - } + 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); + }, - // start animation when option is true - if (this.animationAutoStart && this.dataFilter) { - this.animationStart(); - } - }; + from : function (time, withoutSuffix) { + return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + }, - /** - * Redraw the Graph. - */ - Graph3d.prototype.redraw = function () { - if (this.dataPoints === undefined) { - throw "Error: graph data not initialized"; - } + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, - this._resizeCanvas(); - this._resizeCenter(); - this._redrawSlider(); - this._redrawClear(); - this._redrawAxis(); + 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 (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(); - } + isLeapYear : function () { + return isLeapYear(this.year()); + }, - this._redrawInfo(); - this._redrawLegend(); - }; + isDST : function () { + return (this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset()); + }, - /** - * Clear the canvas before redrawing - */ - Graph3d.prototype._redrawClear = function () { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); + 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; + } + }, - ctx.clearRect(0, 0, canvas.width, canvas.height); - }; + month : makeAccessor('Month', true), + startOf : function (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + /* falls through */ + } - /** - * Redraw the legend showing the colors - */ - Graph3d.prototype._redrawLegend = function () { - var y; + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } - if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) { - var dotSize = this.frame.clientWidth * 0.02; + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } - 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 - } + return this; + }, - 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; - } + 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'); + }, - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); - ctx.lineWidth = 1; - ctx.font = "14px arial"; // TODO: put in options + 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); + } + }, - 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); + isBefore: function (input, units) { + var inputMs; + units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); + if (units === 'millisecond') { + input = moment.isMoment(input) ? input : moment(input); + return +this < +input; + } else { + inputMs = moment.isMoment(input) ? +input : +moment(input); + return +this.clone().endOf(units) < inputMs; + } + }, - //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function - var hue = f * 240; - var color = this._hsv2rgb(hue, 1, 1); + isBetween: function (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + }, - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(left, top + y); - ctx.lineTo(right, top + y); - ctx.stroke(); - } + 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)); + } + }, - ctx.strokeStyle = this.colorAxis; - ctx.strokeRect(left, top, widthMax, height); - } + 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; + } + ), - 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(); - } + 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; + } + ), - 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; + zone : deprecate( + 'moment().zone is deprecated, use moment().utcOffset instead. ' + + 'https://github.com/moment/moment/issues/1779', + function (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } - ctx.beginPath(); - ctx.moveTo(left - gridLineLen, y); - ctx.lineTo(left, y); - ctx.stroke(); + this.utcOffset(input, keepLocalTime); - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.colorAxis; - ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); + return this; + } else { + return -this.utcOffset(); + } + } + ), + + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + utcOffset : function (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (input != null) { + if (typeof input === 'string') { + input = utcOffsetFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = this._dateUtcOffset(); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + addOrSubtractDurationFromMoment(this, + moment.duration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + moment.updateOffset(this, true); + this._changeInProgress = null; + } + } - step.next(); - } + return this; + } else { + return this._isUTC ? offset : this._dateUtcOffset(); + } + }, - ctx.textAlign = "right"; - ctx.textBaseline = "top"; - var label = this.legendLabel; - ctx.fillText(label, right, bottom + this.margin); - } - }; + isLocal : function () { + return !this._isUTC; + }, - /** - * Redraw the filter - */ - Graph3d.prototype._redrawFilter = function () { - this.frame.filter.innerHTML = ""; + isUtcOffset : function () { + return this._isUTC; + }, - if (this.dataFilter) { - var options = { - visible: this.showAnimationControls - }; - var slider = new Slider(this.frame.filter, options); - this.frame.filter.slider = slider; + isUtc : function () { + return this._isUTC && this._offset === 0; + }, - // TODO: css here is not nice here... - this.frame.filter.style.padding = "10px"; - //this.frame.filter.style.backgroundColor = '#EFEFEF'; + zoneAbbr : function () { + return this._isUTC ? 'UTC' : ''; + }, - slider.setValues(this.dataFilter.values); - slider.setPlayInterval(this.animationInterval); + zoneName : function () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + }, - // create an event handler - var me = this; - var onchange = function () { - var index = slider.getIndex(); + parseZone : function () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(utcOffsetFromString(this._i)); + } + return this; + }, - me.dataFilter.selectValue(index); - me.dataPoints = me.dataFilter._getDataPoints(); + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; + } + else { + input = moment(input).utcOffset(); + } - me.redraw(); - }; - slider.setOnChangeCallback(onchange); - } else { - this.frame.filter.slider = undefined; - } - }; + return (this.utcOffset() - input) % 60 === 0; + }, - /** - * Redraw the slider - */ - Graph3d.prototype._redrawSlider = function () { - if (this.frame.filter.slider !== undefined) { - this.frame.filter.slider.redraw(); - } - }; + daysInMonth : function () { + return daysInMonth(this.year(), this.month()); + }, + dayOfYear : function (input) { + var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); + }, - /** - * Redraw common information - */ - Graph3d.prototype._redrawInfo = function () { - if (this.dataFilter) { - var canvas = this.frame.canvas; - var ctx = canvas.getContext("2d"); + quarter : function (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + }, - ctx.font = "14px arial"; // TODO: put in options - ctx.lineStyle = "gray"; - ctx.fillStyle = "gray"; - ctx.textAlign = "left"; - ctx.textBaseline = "top"; + 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'); + }, - var x = this.margin; - var y = this.margin; - ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); - } - }; + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add((input - year), 'y'); + }, + week : function (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + }, - /** - * 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; + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + }, - // TODO: get the actual rendered style of the containerElement - //ctx.font = this.containerElement.style.font; - ctx.font = 24 / this.camera.getArmLength() + "px arial"; + weekday : function (input) { + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + }, - // 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; + 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); + }, - // 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(); + isoWeeksInYear : function () { + return weeksInYear(this.year(), 1, 4); + }, - 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(); + weeksInYear : function () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + }, - 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(); - } + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, - 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); + 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; + }, - step.next(); - } + // 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; - // 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(); + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = moment.localeData(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + }, - 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(); - } + 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); + } + } + ), - 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); + localeData : function () { + return this._locale; + }, - step.next(); - } + _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; + } - // draw z-grid lines and axis - ctx.lineWidth = 1; - prettyStep = this.defaultZStep === undefined; - step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep); - step.start(); - if (step.getCurrent() < this.zMin) { - step.next(); - } - xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; - yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; - while (!step.end()) { - // TODO: make z-grid lines really 3d? - from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(from.x - textMargin, from.y); - ctx.stroke(); + }); - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.colorAxis; - ctx.fillText(this.zValueLabel(step.getCurrent()) + " ", from.x - 5, from.y); + function rawMonthSetter(mom, value) { + var dayOfMonth; - 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(); + // 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; + } + } - // 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(); + dayOfMonth = Math.min(mom.date(), + daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } - // draw y-axis - ctx.lineWidth = 1; - // line at xMin - from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); - // line at xMax - from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin)); - to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin)); - ctx.strokeStyle = this.colorAxis; - ctx.beginPath(); - ctx.moveTo(from.x, from.y); - ctx.lineTo(to.x, to.y); - ctx.stroke(); + function rawGetter(mom, unit) { + return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); + } - // 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 rawSetter(mom, unit, value) { + if (unit === 'Month') { + return rawMonthSetter(mom, value); + } else { + return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } } - 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 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(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); - } - }; + moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false); + moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false); + moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false); + // Setting the hour should keep the time, because the user explicitly + // specified which hour he wants. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true); + // moment.fn.month is defined separately + moment.fn.date = makeAccessor('Date', true); + moment.fn.dates = deprecate('dates accessor is deprecated. Use date instead.', makeAccessor('Date', true)); + moment.fn.year = makeAccessor('FullYear', true); + moment.fn.years = deprecate('years accessor is deprecated. Use year instead.', makeAccessor('FullYear', true)); + + // add plural methods + moment.fn.days = moment.fn.day; + moment.fn.months = moment.fn.month; + moment.fn.weeks = moment.fn.week; + moment.fn.isoWeeks = moment.fn.isoWeek; + moment.fn.quarters = moment.fn.quarter; + + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; - /** - * 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; + // alias isUtc for dev-friendliness + moment.fn.isUTC = moment.fn.isUtc; - C = V * S; - Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 - X = C * (1 - Math.abs(H / 60 % 2 - 1)); + /************************************ + Duration Prototype + ************************************/ - 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; - } + function daysToYears (days) { + // 400 years have 146097 days (taking into account leap year rules) + return days * 400 / 146097; + } - return "RGB(" + parseInt(R * 255) + "," + parseInt(G * 255) + "," + parseInt(B * 255) + ")"; - }; + function yearsToDays (years) { + // years * 365 + absRound(years / 4) - + // absRound(years / 100) + absRound(years / 400); + return years * 146097 / 400; + } + extend(moment.duration.fn = Duration.prototype, { - /** - * 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; + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years = 0; + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; - // 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); + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + hours = absRound(minutes / 60); + data.hours = hours % 24; - // 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; - } + days += absRound(hours / 24); - // 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); + // Accurately convert days to years, assume start from year 0. + years = absRound(daysToYears(days)); + days -= absRound(yearsToDays(years)); - 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; + // 30 days to a month + // TODO (iskren): Use anchor date (like 1st Jan) to compute this. + months += absRound(days / 30); + days %= 30; - 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) + // 12 months -> 1 year + years += absRound(months / 12); + months %= 12; - topSideVisible = crossproduct.z > 0; - } else { - topSideVisible = true; - } + data.days = days; + data.months = months; + data.years = years; + }, - 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 + abs : function () { + this._milliseconds = Math.abs(this._milliseconds); + this._days = Math.abs(this._days); + this._months = Math.abs(this._months); - if (this.showShadow) { - v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = fillStyle; - } else { - v = 1; - fillStyle = this._hsv2rgb(h, s, v); - strokeStyle = this.colorAxis; - } - } else { - fillStyle = "gray"; - strokeStyle = this.colorAxis; - } - lineWidth = 0.5; + this._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); - 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; + return this; + }, - if (point !== undefined) { - if (this.showPerspective) { - lineWidth = 2 / -point.trans.z; - } else { - lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); - } - } + weeks : function () { + return absRound(this.days() / 7); + }, - 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; + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; + }, - 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(); - } + humanize : function (withSuffix) { + var output = relativeTime(this, !withSuffix, this.localeData()); - if (point !== undefined && top !== undefined) { - // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 - zAvg = (point.point.z + top.point.z) / 2; - h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + if (withSuffix) { + output = this.localeData().pastFuture(+this, output); + } - ctx.lineWidth = lineWidth; - ctx.strokeStyle = this._hsv2rgb(h, 1, 1); - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - ctx.lineTo(top.screen.x, top.screen.y); - ctx.stroke(); - } - } - } - }; + return this.localeData().postformat(output); + }, + add : function (input, val) { + // supports only 2.0-style add(1, 's') or add(moment) + var dur = moment.duration(input, val); - /** - * 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; + this._milliseconds += dur._milliseconds; + this._days += dur._days; + this._months += dur._months; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? + this._bubble(); - // calculate the translations of all points - for (i = 0; i < this.dataPoints.length; i++) { - var trans = this._convertPointToTranslation(this.dataPoints[i].point); - var screen = this._convertTranslationToScreen(trans); - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; + return this; + }, - // 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; - } + subtract : function (input, val) { + var dur = moment.duration(input, val); - // order the translated points by depth - var sortDepth = function (a, b) { - return b.dist - a.dist; - }; - this.dataPoints.sort(sortDepth); + this._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; - // draw the datapoints as colored circles - var dotSize = this.frame.clientWidth * 0.02; // px - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; + this._bubble(); - 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(); - } + return this; + }, - // calculate radius for the circle - var size; - if (this.style === Graph3d.STYLE.DOTSIZE) { - size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); - } else { - size = dotSize; - } + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); + }, - var radius; - if (this.showPerspective) { - radius = size / -point.trans.z; - } else { - radius = size * -(this.eye.z / this.camera.getArmLength()); - } - if (radius < 0) { - radius = 0; - } + as : function (units) { + var days, months; + units = normalizeUnits(units); - 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); - } + 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); + } + } + }, - // 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(); - } - }; + lang : moment.fn.lang, + locale : moment.fn.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; + 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 bars - var xWidth = this.xBarWidth / 2; - var yWidth = this.yBarWidth / 2; - for (i = 0; i < this.dataPoints.length; i++) { - var point = this.dataPoints[i]; + toJSON : function () { + return this.toISOString(); + } + }); - // 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); + moment.duration.fn.toString = moment.duration.fn.toISOString; + + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; } - // calculate size for the bar - if (this.style === Graph3d.STYLE.BARSIZE) { - xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); - yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2); + for (i in unitMillisecondFactors) { + if (hasOwnProp(unitMillisecondFactors, i)) { + makeDurationGetter(i.toLowerCase()); + } } - // 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) }]; + 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'); + }; - // 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); + /************************************ + Default Locale + ************************************/ + + + // 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; + } }); - // 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; + /* EMBED_LOCALES */ - // 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}) + /************************************ + Exposing Moment + ************************************/ + + function makeGlobal(shouldDeprecate) { + /*global ender:false */ + if (typeof ender !== 'undefined') { + return; + } + oldGlobalMoment = globalScope.moment; + if (shouldDeprecate) { + globalScope.moment = deprecate( + 'Accessing Moment through the global scope is ' + + 'deprecated, and will be removed in an upcoming ' + + 'release.', + moment); + } else { + globalScope.moment = moment; + } } - // order the surfaces by their (translated) depth - surfaces.sort(function (a, b) { - var diff = b.dist - a.dist; - if (diff) return diff; + // 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; + } - // if equal depth, sort the top surface last - if (a.corners === top) return 1; - if (b.corners === top) return -1; + 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); - // both are equal - return 0; - }); + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(5)(module))) - // 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(); - } - } - }; +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { + function webpackContext(req) { + throw new Error("Cannot find module '" + req + "'."); + } + webpackContext.keys = function() { return []; }; + webpackContext.resolve = webpackContext; + module.exports = webpackContext; + webpackContext.id = 4; - /** - * Draw a line through all datapoints. - * This function can be used when the style is 'line' - */ - Graph3d.prototype._redrawDataLine = function () { - var canvas = this.frame.canvas, - ctx = canvas.getContext("2d"), - point, - i; - if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { - // 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); + module.exports = function(module) { + if(!module.webpackPolyfill) { + module.deprecate = function() {}; + module.paths = []; + // module.parent = undefined by default + module.children = []; + module.webpackPolyfill = 1; + } + return module; + } - this.dataPoints[i].trans = trans; - this.dataPoints[i].screen = screen; - } - // start the line - if (this.dataPoints.length > 0) { - point = this.dataPoints[0]; +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { - ctx.lineWidth = 1; // TODO: make customizable - ctx.strokeStyle = "blue"; // TODO: make customizable - ctx.beginPath(); - ctx.moveTo(point.screen.x, point.screen.y); - } + "use strict"; - // 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); - } + // DOM utility methods - // finish the line - if (this.dataPoints.length > 0) { - 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 = []; + } } }; /** - * Start a moving operation inside the provided parent element - * @param {Event} event The event that occurred (required for - * retrieving the mouse position) + * 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._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.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 = []; + } + } } + }; - // only react on left mouse button down - this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; - if (!this.leftButtonDown && !this.touchDown) return; - - // get mouse position (different code for IE and all other browsers) - this.startMouseX = getMouseX(event); - this.startMouseY = getMouseY(event); - - this.startStart = new Date(this.start); - this.startEnd = new Date(this.end); - this.startArmRotation = this.camera.getArmRotation(); - - this.frame.style.cursor = "move"; - - // add event listeners to handle moving the contents - // we store the function onmousemove and onmouseup in the graph, so we can - // remove the eventlisteners lateron in the function mouseUp() - var me = this; - this.onmousemove = function (event) { - me._onMouseMove(event); - }; - this.onmouseup = function (event) { - me._onMouseUp(event); - }; - util.addEventListener(document, "mousemove", me.onmousemove); - util.addEventListener(document, "mouseup", me.onmouseup); - util.preventDefault(event); + /** + * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer + * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. + * + * @param elementType + * @param JSONcontainer + * @param 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; }; /** - * Perform moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {Event} event Well, eehh, the event + * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer + * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this. + * + * @param elementType + * @param JSONcontainer + * @param DOMContainer + * @returns {*} + * @private */ - Graph3d.prototype._onMouseMove = function (event) { - event = event || window.event; + exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) { + var element; + // allocate DOM element, if it doesnt yet exist, create one. + if (JSONcontainer.hasOwnProperty(elementType)) { + // this element has been created before + // check if there is an redundant element + if (JSONcontainer[elementType].redundant.length > 0) { + element = JSONcontainer[elementType].redundant[0]; + JSONcontainer[elementType].redundant.shift(); + } else { + // create a new element and add it to the SVG + element = document.createElement(elementType); + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); + } else { + DOMContainer.appendChild(element); + } + } + } else { + // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it. + element = document.createElement(elementType); + JSONcontainer[elementType] = { used: [], redundant: [] }; + if (insertBefore !== undefined) { + DOMContainer.insertBefore(element, insertBefore); + } else { + DOMContainer.appendChild(element); + } + } + JSONcontainer[elementType].used.push(element); + return element; + }; - // 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 (Math.abs(Math.cos(horizontalNew)) < snapValue) { - horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; + /** + * draw a point object. this is a seperate function because it can also be called by the legend. + * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions + * as well. + * + * @param x + * @param y + * @param group + * @param JSONcontainer + * @param svgContainer + * @param labelObj + * @returns {*} + */ + exports.drawPoint = function (x, y, group, JSONcontainer, svgContainer, labelObj) { + var point; + if (group.options.drawPoints.style == "circle") { + point = exports.getSVGElement("circle", JSONcontainer, svgContainer); + point.setAttributeNS(null, "cx", x); + point.setAttributeNS(null, "cy", y); + point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size); + } else { + point = exports.getSVGElement("rect", JSONcontainer, svgContainer); + point.setAttributeNS(null, "x", x - 0.5 * group.options.drawPoints.size); + point.setAttributeNS(null, "y", y - 0.5 * group.options.drawPoints.size); + point.setAttributeNS(null, "width", group.options.drawPoints.size); + point.setAttributeNS(null, "height", group.options.drawPoints.size); } - // snap 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; + 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.camera.setArmRotation(horizontalNew, verticalNew); - this.redraw(); + if (labelObj.yOffset) { + y = y + labelObj.yOffset; + } + if (labelObj.content) { + label.textContent = labelObj.content; + } - // fire a cameraPositionChange event - var parameters = this.getCameraPosition(); - this.emit("cameraPositionChange", parameters); + if (labelObj.className) { + label.setAttributeNS(null, "class", labelObj.className + " label"); + } - util.preventDefault(event); + } + label.setAttributeNS(null, "x", x); + label.setAttributeNS(null, "y", y); + return point; }; - /** - * Stop moving operating. - * This function activated from within the funcion Graph.mouseDown(). - * @param {event} event The event + * draw a bar SVG element centered on the X coordinate + * + * @param x + * @param y + * @param className */ - 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); + exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) { + if (height != 0) { + if (height < 0) { + height *= -1; + y -= height; + } + var rect = exports.getSVGElement("rect", JSONcontainer, svgContainer); + rect.setAttributeNS(null, "x", x - 0.5 * width); + rect.setAttributeNS(null, "y", y); + rect.setAttributeNS(null, "width", width); + rect.setAttributeNS(null, "height", height); + rect.setAttributeNS(null, "class", className); + } }; +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var Queue = __webpack_require__(8); + /** - * 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 + * DataSet + * + * Usage: + * var dataSet = new DataSet({ + * fieldId: '_id', + * type: { + * // ... + * } + * }); + * + * dataSet.add(item); + * dataSet.add(data); + * dataSet.update(item); + * dataSet.update(data); + * dataSet.remove(id); + * dataSet.remove(ids); + * var data = dataSet.get(); + * var data = dataSet.get(id); + * var data = dataSet.get(ids); + * var data = dataSet.get(ids, options, data); + * dataSet.clear(); + * + * A data set can: + * - add/remove/update data + * - gives triggers upon changes in the data + * - can import/export data in various data formats + * + * @param {Array | DataTable} [data] Optional array with initial data + * @param {Object} [options] Available options: + * {String} fieldId Field name of the id in the + * items, 'id' by default. + * {Object. 0 ? 1 : x < 0 ? -1 : 0; + var addOrUpdate = function (item) { + var id = item[fieldId]; + if (me._data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); + updatedData.push(item); + } else { + // add new item + id = me._addItem(item); + addedIds.push(id); + } + }; + + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + addOrUpdate(data[i]); + } + } else if (util.isDataTable(data)) { + // Google DataTable + var columns = this._getColumnNames(data); + for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) { + var item = {}; + for (var col = 0, cols = columns.length; col < cols; col++) { + var field = columns[col]; + item[field] = data.getValue(row, col); + } + + addOrUpdate(item); + } + } else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } else { + throw new Error("Unknown dataType"); } - 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)); + if (addedIds.length) { + this._trigger("add", { items: addedIds }, senderId); + } + if (updatedIds.length) { + this._trigger("update", { items: updatedIds, data: updatedData }, senderId); + } - // 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); + return addedIds.concat(updatedIds); }; /** - * 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 + * 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 */ - Graph3d.prototype._dataPointFromXY = function (x, y) { - var i, - distMax = 100, - // px - dataPoint = null, - closestDataPoint = null, - closestDist = null, - center = new Point2d(x, y); + DataSet.prototype.get = function (args) { + var me = this; - 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; - } - } - } + // parse the arguments + var id, ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == "String" || firstType == "Number") { + // get(id [, options] [, data]) + id = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } else if (firstType == "Array") { + // get(ids [, options] [, data]) + ids = arguments[0]; + options = arguments[1]; + data = arguments[2]; + } else { + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; + } + + // determine the return type + var returnType; + if (options && options.returnType) { + var allowedValues = ["DataTable", "Array", "Object"]; + returnType = allowedValues.indexOf(options.returnType) == -1 ? "Array" : options.returnType; + + if (data && returnType != util.getType(data)) { + throw new Error("Type of parameter \"data\" (" + util.getType(data) + ") " + "does not correspond with specified options.type (" + options.type + ")"); + } + if (returnType == "DataTable" && !util.isDataTable(data)) { + throw new Error("Parameter \"data\" must be a DataTable " + "when options.type is \"DataTable\""); } + } else if (data) { + returnType = util.getType(data) == "DataTable" ? "DataTable" : "Array"; } else { - // 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); + returnType = "Array"; + } - if ((closestDist === null || dist < closestDist) && dist < distMax) { - closestDist = dist; - closestDataPoint = dataPoint; + // build options + var type = options && options.type || this._options.type; + var filter = options && options.filter; + var items = [], + item, + itemId, + i, + len; + + // convert items + if (id != undefined) { + // return a single item + item = me._getItem(id, type); + if (filter && !filter(item)) { + item = null; + } + } else if (ids != undefined) { + // return a subset of items + for (i = 0, len = ids.length; i < len; i++) { + item = me._getItem(ids[i], type); + if (!filter || filter(item)) { + items.push(item); + } + } + } else { + // return all items + for (itemId in this._data) { + if (this._data.hasOwnProperty(itemId)) { + item = me._getItem(itemId, type); + if (!filter || filter(item)) { + items.push(item); } } } } + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } + + // filter fields of the items + if (options && options.fields) { + var fields = options.fields; + if (id != undefined) { + item = this._filterFields(item, fields); + } else { + for (i = 0, len = items.length; i < len; i++) { + items[i] = this._filterFields(items[i], fields); + } + } + } - return 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); - }; - - - /** - * Return the label - * @return {string} label - */ - Filter.prototype.getLabel = function () { - return this.graph.filterLabel; - }; - + 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 the columnIndex of the filter - * @return {Number} columnIndex - */ - Filter.prototype.getColumn = function () { - return this.column; + return id; }; /** - * Return the currently selected value. Returns undefined if there is no selection - * @return {*} value + * 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.getSelectedValue = function () { - if (this.index === undefined) return undefined; + DataSet.prototype._getItem = function (id, types) { + var field, value; - return this.values[this.index]; + // get the item from the dataset + var raw = this._data[id]; + if (!raw) { + return null; + } + + // convert the items field types + var converted = {}; + if (types) { + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = util.convert(value, types[field]); + } + } + } else { + // no field types specified, no converting needed + for (field in raw) { + if (raw.hasOwnProperty(field)) { + value = raw[field]; + converted[field] = value; + } + } + } + return converted; }; /** - * Retrieve all values of the filter - * @return {Array} values + * 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.getValues = function () { - return this.values; + DataSet.prototype._updateItem = function (item) { + var id = item[this._fieldId]; + if (id == undefined) { + throw new Error("Cannot update item: item has no id (item: " + JSON.stringify(item) + ")"); + } + var d = this._data[id]; + if (!d) { + // item doesn't exist + throw new Error("Cannot update item: no item with id " + id + " found"); + } + + // merge with current item + for (var field in item) { + if (item.hasOwnProperty(field)) { + var fieldType = this._type[field]; // type may be undefined + d[field] = util.convert(item[field], fieldType); + } + } + + return id; }; /** - * 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); } + // TODO: implement a function .config() to dynamically update things like configured filter + // and trigger changes accordingly + /** - * Select the previous index + * Set a data source for the view + * @param {DataSet | DataView} data */ - Slider.prototype.prev = function () { - var index = this.getIndex(); - if (index > 0) { - index--; - this.setIndex(index); + DataView.prototype.setData = function (data) { + var ids, i, len; + + if (this._data) { + // unsubscribe from current dataset + if (this._data.unsubscribe) { + this._data.unsubscribe("*", this.listener); + } + + // trigger a remove of all items in memory + ids = []; + for (var id in this._ids) { + if (this._ids.hasOwnProperty(id)) { + ids.push(id); + } + } + this._ids = {}; + this.length = 0; + this._trigger("remove", { items: ids }); } - }; - /** - * Select the next index - */ - Slider.prototype.next = function () { - var index = this.getIndex(); - if (index < this.values.length - 1) { - index++; - this.setIndex(index); + this._data = data; + + if (this._data) { + // update fieldId + this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || "id"; + + // trigger an add of all added items + ids = this._data.getIds({ filter: this._options && this._options.filter }); + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + this._ids[id] = true; + } + this.length = ids.length; + this._trigger("add", { items: ids }); + + // subscribe to new dataset + if (this._data.on) { + this._data.on("*", this.listener); + } } }; /** - * Select the next index + * Refresh the DataView. Useful when the DataView has a filter function + * containing a variable parameter. */ - Slider.prototype.playNext = function () { - var start = new Date(); + DataView.prototype.refresh = function () { + var id; + var ids = this._data.getIds({ filter: this._options && this._options.filter }); + var newIds = {}; + var added = []; + var removed = []; - 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); + // 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++; + } } - 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 + // 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--; + } + } + } - var me = this; - this.playTimeout = setTimeout(function () { - me.playNext(); - }, interval); + // trigger events + if (added.length) { + this._trigger("add", { items: added }); + } + if (removed.length) { + this._trigger("remove", { items: removed }); + } }; /** - * Toggle start or stop playing + * 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.togglePlay = function () { - if (this.playTimeout === undefined) { - this.play(); + DataView.prototype.get = function (args) { + var me = this; + + // parse the arguments + var ids, options, data; + var firstType = util.getType(arguments[0]); + if (firstType == "String" || firstType == "Number" || firstType == "Array") { + // get(id(s) [, options] [, data]) + ids = arguments[0]; // can be a single id or an array with ids + options = arguments[1]; + data = arguments[2]; } else { - this.stop(); + // get([, options] [, data]) + options = arguments[0]; + data = arguments[1]; } - }; - /** - * Start playing - */ - Slider.prototype.play = function () { - // Test whether already playing - if (this.playTimeout) return; + // extend the options with the default options and provided options + var viewOptions = util.extend({}, this._options, options); - this.playNext(); + // 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); + }; + } - if (this.frame) { - this.frame.play.value = "Stop"; + // 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); }; /** - * Stop playing + * 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.stop = function () { - clearInterval(this.playTimeout); - this.playTimeout = undefined; + DataView.prototype.getIds = function (options) { + var ids; - if (this.frame) { - this.frame.play.value = "Play"; - } - }; + if (this._data) { + var defaultFilter = this._options.filter; + var filter; - /** - * 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; - }; + if (options && options.filter) { + if (defaultFilter) { + filter = function (item) { + return defaultFilter(item) && options.filter(item); + }; + } else { + filter = options.filter; + } + } else { + filter = defaultFilter; + } - /** - * Set the interval for playing the list - * @param {Number} interval The interval in milliseconds - */ - Slider.prototype.setPlayInterval = function (interval) { - this.playInterval = interval; + ids = this._data.getIds({ + filter: filter, + order: options && options.order + }); + } else { + ids = []; + } + + return ids; }; /** - * Retrieve the current play interval - * @return {Number} interval The interval in milliseconds + * 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.getPlayInterval = function (interval) { - return this.playInterval; + DataView.prototype.getDataSet = function () { + var dataSet = this; + while (dataSet instanceof DataView) { + dataSet = dataSet._data; + } + return dataSet || null; }; /** - * 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. + * 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.setPlayLoop = function (doLoop) { - this.playLoop = doLoop; - }; + DataView.prototype._onEvent = function (event, params, senderId) { + var i, + len, + id, + item, + ids = params && params.items, + data = this._data, + added = [], + updated = [], + removed = []; + if (ids && data) { + switch (event) { + case "add": + // filter the ids of the added items + for (i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + item = this.get(id); + if (item) { + this._ids[id] = true; + added.push(id); + } + } - /** - * Execute the onchange callback function - */ - Slider.prototype.onChange = function () { - if (this.onChangeCallback !== undefined) { - this.onChangeCallback(); - } - }; + break; - /** - * 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"; + 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); - // position the slider button - var left = this.indexToLeft(this.index); - this.frame.slide.style.left = left + "px"; - } - }; + 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; - /** - * 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; + 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); + } + } - if (this.values.length > 0) this.setIndex(0);else this.index = undefined; - }; + break; + } - /** - * Select a value by its index - * @param {Number} index - */ - Slider.prototype.setIndex = function (index) { - if (index < this.values.length) { - this.index = index; + this.length += added.length - removed.length; - this.redraw(); - this.onChange(); - } else { - throw "Error: index out of range"; + 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); + } } }; - /** - * retrieve the index of the currently selected vaue - * @return {Number} index - */ - Slider.prototype.getIndex = function () { - return this.index; - }; + // copy subscription functionality from DataSet + DataView.prototype.on = DataSet.prototype.on; + DataView.prototype.off = DataSet.prototype.off; + DataView.prototype._trigger = DataSet.prototype._trigger; + // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) + DataView.prototype.subscribe = DataView.prototype.on; + DataView.prototype.unsubscribe = DataView.prototype.off; - /** - * retrieve the currently selected value - * @return {*} value - */ - Slider.prototype.get = function () { - return this.values[this.index]; - }; + module.exports = DataView; + // nothing interesting for me :-( +/***/ }, +/* 10 */ +/***/ 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 Emitter = __webpack_require__(11); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); + var util = __webpack_require__(1); + var Point3d = __webpack_require__(12); + var Point2d = __webpack_require__(13); + var Camera = __webpack_require__(14); + var Filter = __webpack_require__(15); + var Slider = __webpack_require__(16); + var StepNumber = __webpack_require__(17); - this.frame.style.cursor = "move"; + /** + * @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"); + } - // 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); - }; + // create variables and set default values + this.containerElement = container; + this.width = "400px"; + this.height = "400px"; + this.margin = 10; // px + this.defaultXCenter = "55%"; + this.defaultYCenter = "50%"; + this.xLabel = "x"; + this.yLabel = "y"; + this.zLabel = "z"; - Slider.prototype.leftToIndex = function (left) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; - var x = left - 3; + var passValueFn = function (v) { + return v; + }; + this.xValueLabel = passValueFn; + this.yValueLabel = passValueFn; + this.zValueLabel = passValueFn; - 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; + this.filterLabel = "time"; + this.legendLabel = "value"; - return index; - }; + 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' - Slider.prototype.indexToLeft = function (index) { - var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + this.animationInterval = 1000; // milliseconds + this.animationPreload = false; - var x = index / (this.values.length - 1) * width; - var left = x + 3; + this.camera = new Camera(); + this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window? - return left; - }; + 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 - Slider.prototype._onMouseMove = function (event) { - var diff = event.clientX - this.startClientX; - var x = this.startSlideX + diff; + // constants + this.colorAxis = "#4D4D4D"; + this.colorGrid = "#D3D3D3"; + this.colorDot = "#7DC1FF"; + this.colorDotBorder = "#3267D2"; - var index = this.leftToIndex(x); + // create a frame and canvas + this.create(); - this.setIndex(index); + // apply options (also when undefined) + this.setOptions(options); - util.preventDefault(); - }; + // apply data + if (data) { + this.setData(data); + } + } + // Extend Graph3d with an Emitter mixin + Emitter(Graph3d.prototype); - Slider.prototype._onMouseUp = function (event) { - this.frame.style.cursor = "auto"; + /** + * 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)); - // remove event listeners - util.removeEventListener(document, "mousemove", this.onmousemove); - util.removeEventListener(document, "mouseup", this.onmouseup); + // 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; + } + } - util.preventDefault(); - }; + // scale the vertical axis + this.scale.z *= this.verticalRatio; + // TODO: can this be automated? verticalRatio? - module.exports = Slider; + // determine scale for (optional) value + this.scale.value = 1 / (this.valueMax - this.valueMin); -/***/ }, -/* 12 */ -/***/ function(module, exports, __webpack_require__) { + // 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); + }; - "use strict"; /** - * @prototype StepNumber - * The class StepNumber is an iterator for Numbers. You provide a start and end - * value, and a best step size. StepNumber itself rounds to fixed values and - * a finds the step that best fits the provided step. - * - * If prettyStep is true, the step size is chosen as close as possible to the - * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... - * - * Example usage: - * var step = new StepNumber(0, 10, 2.5, true); - * step.start(); - * while (!step.end()) { - * alert(step.getCurrent()); - * step.next(); - * } - * - * Version: 1.0 - * - * @param {Number} start The start value - * @param {Number} end The end value - * @param {Number} step Optional. Step size. Must be a positive value. - * @param {boolean} prettyStep Optional. If true, the step size is rounded - * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + * 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 */ - 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); + Graph3d.prototype._convert3Dto2D = function (point3d) { + var translation = this._convertPointToTranslation(point3d); + return this._convertTranslationToScreen(translation); }; /** - * Set a new range: start, end and step. - * - * @param {Number} start The start value - * @param {Number} end The end value - * @param {Number} step Optional. Step size. Must be a positive value. - * @param {boolean} prettyStep Optional. If true, the step size is rounded - * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) + * Convert a 3D location 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.setRange = function (start, end, step, prettyStep) { - this._start = start ? start : 0; - this._end = end ? end : 0; + 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, - this.setStep(step, prettyStep); - }; - /** - * Set a new step size - * @param {Number} step New step size. Must be a positive value - * @param {boolean} prettyStep Optional. If true, the provided step is rounded - * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) - */ - StepNumber.prototype.setStep = function (step, prettyStep) { - if (step === undefined || step <= 0) return; + // 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 (prettyStep !== undefined) this.prettyStep = prettyStep; - if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step; + // 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); }; /** - * Calculate a nice step size, closest to the desired step size. - * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an - * integer Number. For example 1, 2, 5, 10, 20, 50, etc... - * @param {Number} step Desired step size - * @return {Number} Nice step size + * Convert a translation point to a point on the screen + * @param {Point3d} translation A 3D point with parameters x, y, z This is + * the translation of the point, seen from the + * camera + * @return {Point2d} point2d A 2D point with parameters x, y */ - StepNumber.calculatePrettyStep = function (step) { - var log10 = function (x) { - return Math.log(x) / Math.LN10; - }; - - // try three steps (multiple of 1, 2, or 5 - var step1 = Math.pow(10, Math.round(log10(step))), - step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), - step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); - - // choose the best step (closest to minimum step) - var prettyStep = step1; - if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; - if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; + Graph3d.prototype._convertTranslationToScreen = function (translation) { + var ex = this.eye.x, + ey = this.eye.y, + ez = this.eye.z, + dx = translation.x, + dy = translation.y, + dz = translation.z; - // for safety - if (prettyStep <= 0) { - prettyStep = 1; + // calculate position on screen from translation + var bx; + var by; + if (this.showPerspective) { + bx = (dx - ex) * (ez / dz); + by = (dy - ey) * (ez / dz); + } else { + bx = dx * -(ez / this.camera.getArmLength()); + by = dy * -(ez / this.camera.getArmLength()); } - return prettyStep; + // shift and scale the point to the center of the screen + // use the width of the graph to scale both horizontally and vertically. + return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth); }; /** - * returns the current value of the step - * @return {Number} current value + * Set the background styling for the graph + * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor */ - StepNumber.prototype.getCurrent = function () { - return parseFloat(this._current.toPrecision(this.precision)); - }; + Graph3d.prototype._setBackgroundColor = function (backgroundColor) { + var fill = "white"; + var stroke = "gray"; + var strokeWidth = 1; - /** - * returns the current step size - * @return {Number} current step size - */ - StepNumber.prototype.getStep = function () { - return this._step; - }; + if (typeof backgroundColor === "string") { + fill = backgroundColor; + stroke = "none"; + strokeWidth = 0; + } else if (typeof backgroundColor === "object") { + if (backgroundColor.fill !== undefined) fill = backgroundColor.fill; + if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke; + if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth; + } else if (backgroundColor === undefined) {} else { + throw "Unsupported type of backgroundColor"; + } - /** - * 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; + this.frame.style.backgroundColor = fill; + this.frame.style.borderColor = stroke; + this.frame.style.borderWidth = strokeWidth + "px"; + this.frame.style.borderStyle = "solid"; }; - /** - * Do a step, add the step size to the current value - */ - StepNumber.prototype.next = function () { - this._current += this._step; + + /// enumerate the available styles + Graph3d.STYLE = { + BAR: 0, + BARCOLOR: 1, + BARSIZE: 2, + DOT: 3, + DOTLINE: 4, + DOTCOLOR: 5, + DOTSIZE: 6, + GRID: 7, + LINE: 8, + SURFACE: 9 }; /** - * Returns true whether the end is reached - * @return {boolean} True if the current value has passed the end value. + * Retrieve the style index from given styleName + * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line' + * @return {Number} styleNumber Enumeration value representing the style, or -1 + * when not found */ - StepNumber.prototype.end = function () { - return this._current > this._end; - }; - - module.exports = StepNumber; - -/***/ }, -/* 13 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; + Graph3d.prototype._getStyleNumber = function (styleName) { + switch (styleName) { + case "dot": + return Graph3d.STYLE.DOT; + case "dot-line": + return Graph3d.STYLE.DOTLINE; + case "dot-color": + return Graph3d.STYLE.DOTCOLOR; + case "dot-size": + return Graph3d.STYLE.DOTSIZE; + case "line": + return Graph3d.STYLE.LINE; + case "grid": + return Graph3d.STYLE.GRID; + case "surface": + return Graph3d.STYLE.SURFACE; + case "bar": + return Graph3d.STYLE.BAR; + case "bar-color": + return Graph3d.STYLE.BARCOLOR; + case "bar-size": + return Graph3d.STYLE.BARSIZE; + } - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(17); - var Core = __webpack_require__(42); - var TimeAxis = __webpack_require__(35); - var CurrentTime = __webpack_require__(26); - var CustomTime = __webpack_require__(27); - var ItemSet = __webpack_require__(32); + return -1; + }; /** - * 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 + * Determine the indexes of the data columns, based on the given style and data + * @param {DataSet} data + * @param {Number} style */ - function Timeline(container, items, groups, options) { - if (!(this instanceof Timeline)) { - throw new SyntaxError("Constructor must be called with the new operator"); - } + Graph3d.prototype._determineColumnIndexes = function (data, style) { + if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) { + // 3 columns expected, and optionally a 4th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = undefined; - // if the third element is options, the forth is groups (optionally); - if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { - var forthArgument = options; - options = groups; - groups = forthArgument; + if (data.getNumberOfColumns() > 3) { + this.colFilter = 3; + } + } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) { + // 4 columns expected, and optionally a 5th with filter values + this.colX = 0; + this.colY = 1; + this.colZ = 2; + this.colValue = 3; + + if (data.getNumberOfColumns() > 4) { + this.colFilter = 4; + } + } else { + throw "Unknown style \"" + this.style + "\""; } + }; - var me = this; - this.defaultOptions = { - start: null, - end: null, + Graph3d.prototype.getNumberOfRows = function (data) { + return data.length; + }; - autoResize: true, - orientation: "bottom", - width: null, - height: null, - maxHeight: null, - minHeight: null - }; - this.options = util.deepExtend({}, this.defaultOptions); + Graph3d.prototype.getNumberOfColumns = function (data) { + var counter = 0; + for (var column in data[0]) { + if (data[0].hasOwnProperty(column)) { + counter++; + } + } + return counter; + }; - // Create the DOM, props, and emitter - this._create(container); - // all components listed here will be repainted automatically - this.components = []; + 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; + }; - 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) + 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; + }; - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; + /** + * 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; - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); + // unsubscribe from the dataTable + if (this.dataSet) { + this.dataSet.off("*", this._onChange); + } - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + if (rawData === undefined) return; - // 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 (Array.isArray(rawData)) { + rawData = new DataSet(rawData); + } - // item set - this.itemSet = new ItemSet(this.body); - this.components.push(this.itemSet); + var data; + if (rawData instanceof DataSet || rawData instanceof DataView) { + data = rawData.get(); + } else { + throw new Error("Array, DataSet, or DataView expected"); + } - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + if (data.length == 0) return; - // apply options - if (options) { - this.setOptions(options); - } + this.dataSet = rawData; + this.dataTable = data; - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + // subscribe to changes in the dataset + this._onChange = function () { + me.setData(me.dataSet); + }; + this.dataSet.on("*", this._onChange); - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); - } - } + // _determineColumnIndexes + // getNumberOfRows (points) + // getNumberOfColumns (x,y,z,v,t,t1,t2...) + // getDistinctValues (unique values?) + // getColumnRange - // Extend the functionality from Core - Timeline.prototype = new Core(); + // 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"; - /** - * Force a redraw. The size of all items will be recalculated. - * Can be useful to manually redraw when option autoResize=false and the window - * has been resized, or when the items CSS has been changed. - */ - Timeline.prototype.redraw = function () { - this.itemSet && this.itemSet.markDirty({ refreshItems: true }); - this._redraw(); - }; - /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items - */ - Timeline.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; - // convert to type DataSet when needed - var newDataSet; - if (!items) { - newDataSet = null; - } else if (items instanceof DataSet || items instanceof DataView) { - newDataSet = items; - } else { - // turn an array into a dataset - newDataSet = new DataSet(items, { - type: { - start: "Date", - end: "Date" - } - }); + // 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(); + }); + } } - // set items - this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); - if (initialLoad) { - if (this.options.start != undefined || this.options.end != undefined) { - if (this.options.start == undefined || this.options.end == undefined) { - var dataRange = this._getDataRange(); - } + var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE; - var start = this.options.start != undefined ? this.options.start : dataRange.start; - var end = this.options.end != undefined ? this.options.end : dataRange.end; + // 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; + } - this.setWindow(start, end, { animate: false }); + if (this.defaultYBarWidth !== undefined) { + this.yBarWidth = this.defaultYBarWidth; } else { - this.fit({ animate: false }); + var dataY = this.getDistinctValues(data, this.colY); + this.yBarWidth = dataY[1] - dataY[0] || 1; } } - }; - /** - * 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); + // 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; - this.groupsData = newDataSet; - this.itemSet.setGroups(newDataSet); - }; + 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; - /** - * 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); + var zRange = this.getColumnRange(data, this.colZ); + this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min; + this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max; + if (this.zMax <= this.zMin) this.zMax = this.zMin + 1; + this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5; - if (options && options.focus) { - this.focus(ids, options); + 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; } - }; - /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items - */ - Timeline.prototype.getSelection = function () { - return this.itemSet && this.itemSet.getSelection() || []; + // set the scale dependent on the ranges. + this._setScale(); }; + + /** - * 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 + * Filter the data based on the current filter + * @param {Array} data + * @return {Array} dataPoints Array with point objects which can be drawn on screen */ - Timeline.prototype.focus = function (id, options) { - if (!this.itemsData || id == undefined) return; + Graph3d.prototype._getDataPoints = function (data) { + // TODO: store the created matrix dataPoints in the filters instead of reloading each time + var x, y, i, z, obj, point; - var ids = Array.isArray(id) ? id : [id]; + var dataPoints = []; - // get the specified item(s) - var itemsData = this.itemsData.getDataSet().get(ids, { - type: { - start: "Date", - end: "Date" - } - }); + 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 - // 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(); + // create two lists with all present x and y values + var dataX = []; + var dataY = []; + for (i = 0; i < this.getNumberOfRows(data); i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; - if (start === null || s < start) { - start = s; + if (dataX.indexOf(x) === -1) { + dataX.push(x); + } + if (dataY.indexOf(y) === -1) { + dataY.push(y); + } } - if (end === null || e > end) { - end = e; - } - }); + var sortNumber = function (a, b) { + return a - b; + }; + dataX.sort(sortNumber); + dataY.sort(sortNumber); - 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); + // create a grid, a 2d matrix, with all values. + var dataMatrix = []; // temporary data matrix + for (i = 0; i < data.length; i++) { + x = data[i][this.colX] || 0; + y = data[i][this.colY] || 0; + z = data[i][this.colZ] || 0; - var animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(middle - interval / 2, middle + interval / 2, animate); - } - }; + var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer + var yIndex = dataY.indexOf(y); - /** - * 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 (dataMatrix[xIndex] === undefined) { + dataMatrix[xIndex] = []; + } - 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 point3d = new Point3d(); + point3d.x = x; + point3d.y = y; + point3d.z = z; - // calculate maximum value of fields 'start' and 'end' - var maxStartItem = dataset.max("start"); - if (maxStartItem) { - max = util.convert(maxStartItem.start, "Date").valueOf(); + obj = {}; + obj.point = point3d; + obj.trans = undefined; + obj.screen = undefined; + obj.bottom = new Point3d(x, y, this.zMin); + + dataMatrix[xIndex][yIndex] = obj; + + dataPoints.push(obj); } - 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()); + + // 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; + } } } - } - - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; - }; - + } 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; - module.exports = Timeline; + if (this.colValue !== undefined) { + point.value = data[i][this.colValue] || 0; + } -/***/ }, -/* 14 */ -/***/ function(module, exports, __webpack_require__) { + obj = {}; + obj.point = point; + obj.bottom = new Point3d(point.x, point.y, this.zMin); + obj.trans = undefined; + obj.screen = undefined; - "use strict"; + dataPoints.push(obj); + } + } - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(17); - var Core = __webpack_require__(42); - var TimeAxis = __webpack_require__(35); - var CurrentTime = __webpack_require__(26); - var CustomTime = __webpack_require__(27); - var LineGraph = __webpack_require__(34); + return dataPoints; + }; /** - * 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 + * 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. */ - 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; + Graph3d.prototype.create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); } - 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); + this.frame = document.createElement("div"); + this.frame.style.position = "relative"; + this.frame.style.overflow = "hidden"; - // Create the DOM, props, and emitter - this._create(container); + // 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); + } - // all components listed here will be repainted automatically - this.components = []; + 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); - 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) - } + // 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' - // range - this.range = new Range(this.body); - this.components.push(this.range); - this.body.range = this.range; - - // time axis - this.timeAxis = new TimeAxis(this.body); - this.components.push(this.timeAxis); - //this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - - // current time bar - this.currentTime = new CurrentTime(this.body); - this.components.push(this.currentTime); + 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); - // 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); + // add the new graph to the container element + this.containerElement.appendChild(this.frame); + }; - // item set - this.linegraph = new LineGraph(this.body); - this.components.push(this.linegraph); - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + /** + * 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; - // apply options - if (options) { - this.setOptions(options); - } + this._resizeCanvas(); + }; - // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! - if (groups) { - this.setGroups(groups); - } + /** + * 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%"; - // create itemset - if (items) { - this.setItems(items); - } else { - this._redraw(); - } - } + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; - // Extend the functionality from Core - Graph2d.prototype = new Core(); + // adjust with for margin + this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + "px"; + }; /** - * Set items - * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + * Start animation */ - Graph2d.prototype.setItems = function (items) { - var initialLoad = this.itemsData == null; + Graph3d.prototype.animationStart = function () { + if (!this.frame.filter || !this.frame.filter.slider) throw "No animation available"; - // 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.filter.slider.play(); + }; - // set items - this.itemsData = newDataSet; - this.linegraph && this.linegraph.setItems(newDataSet); - if (initialLoad) { - if (this.options.start != undefined || this.options.end != undefined) { - var start = this.options.start != undefined ? this.options.start : null; - var end = this.options.end != undefined ? this.options.end : null; + /** + * Stop animation + */ + Graph3d.prototype.animationStop = function () { + if (!this.frame.filter || !this.frame.filter.slider) return; - this.setWindow(start, end, { animate: false }); - } else { - this.fit({ animate: false }); - } - } + this.frame.filter.slider.stop(); }; + /** - * Set groups - * @param {vis.DataSet | Array | google.visualization.DataTable} groups + * 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 */ - 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; + 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 { - // turn an array into a dataset - newDataSet = new DataSet(groups); + this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px } - this.groupsData = newDataSet; - this.linegraph.setGroups(newDataSet); + // 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 + } }; /** - * 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 + * 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. */ - Graph2d.prototype.getLegend = function (groupId, width, height) { - if (width === undefined) { - width = 15; + Graph3d.prototype.setCameraPosition = function (pos) { + if (pos === undefined) { + return; } - if (height === undefined) { - height = 15; + + if (pos.horizontal !== undefined && pos.vertical !== undefined) { + this.camera.setArmRotation(pos.horizontal, pos.vertical); } - if (this.linegraph.groups[groupId] !== undefined) { - return this.linegraph.groups[groupId].getLegend(width, height); - } else { - return "cannot find group:" + groupId; + + if (pos.distance !== undefined) { + this.camera.setArmLength(pos.distance); } + + this.redraw(); }; + /** - * This checks if the visible option of the supplied group (by ID) is true or false. - * @param groupId - * @returns {*} + * Retrieve the current camera rotation + * @return {object} An object with parameters horizontal, vertical, and + * distance */ - 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.getCameraPosition = function () { + var pos = this.camera.getArmRotation(); + pos.distance = this.camera.getArmLength(); + return pos; }; - /** - * 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 + * Load data into the 3D Graph */ - Graph2d.prototype.getItemRange = function () { - var min = null; - var max = null; + Graph3d.prototype._readData = function (data) { + // read the data + this._dataInitialize(data, this.style); - // 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; - } - } - } + + if (this.dataFilter) { + // apply filtering + this.dataPoints = this.dataFilter._getDataPoints(); + } else { + // no filtering. load all data + this.dataPoints = this._getDataPoints(this.dataTable); } - return { - min: min != null ? new Date(min) : null, - max: max != null ? new Date(max) : null - }; + // draw the filter + this._redrawFilter(); }; - - - module.exports = Graph2d; - -/***/ }, -/* 15 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - /** - * Created by Alex on 10/3/2014. + * Replace the dataset of the Graph3d + * @param {Array | DataSet | DataView} data */ - var moment = __webpack_require__(40); - + Graph3d.prototype.setData = function (data) { + this._readData(data); + this.redraw(); - /** - * used in Core to convert the options into a volatile variable - * - * @param Core - */ - exports.convertHiddenOptions = function (body, hiddenDates) { - body.hiddenDates = []; - if (hiddenDates) { - if (Array.isArray(hiddenDates) == true) { - for (var i = 0; i < hiddenDates.length; i++) { - if (hiddenDates[i].repeat === undefined) { - var dateItem = {}; - dateItem.start = moment(hiddenDates[i].start).toDate().valueOf(); - dateItem.end = moment(hiddenDates[i].end).toDate().valueOf(); - body.hiddenDates.push(dateItem); - } - } - body.hiddenDates.sort(function (a, b) { - return a.start - b.start; - }); // sort by start time - } + // 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); } - }; + 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(); + } - ///** - // * 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(); - // } - //}; - - /** - * 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; + + ctx.beginPath(); + ctx.moveTo(left - gridLineLen, y); + ctx.lineTo(left, y); + ctx.stroke(); + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y); - var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); - time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); + step.next(); + } - var conversion = Core.range.conversion(width, duration); - return (time.valueOf() - conversion.offset) * conversion.scale; + 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); - - var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); - return newTime; - } - }; + Graph3d.prototype._redrawFilter = function () { + this.frame.filter.innerHTML = ""; + if (this.dataFilter) { + var options = { + visible: this.showAnimationControls + }; + var slider = new Slider(this.frame.filter, options); + this.frame.filter.slider = slider; - /** - * Support function - * - * @param hiddenDates - * @param range - * @returns {number} - */ - exports.getHiddenDurationBetween = function (hiddenDates, start, end) { - var duration = 0; - for (var i = 0; i < hiddenDates.length; i++) { - var startDate = hiddenDates[i].start; - var endDate = hiddenDates[i].end; - // if time after the cutout, and the - if (startDate >= start && endDate < end) { - duration += endDate - startDate; - } - } - return duration; - }; + // 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); - /** - * Support function - * @param hiddenDates - * @param range - * @param time - * @returns {{duration: number, time: *, offset: number}} - */ - exports.correctTimeForHidden = function (hiddenDates, range, time) { - time = moment(time).toDate().valueOf(); - time -= exports.getHiddenDurationBefore(hiddenDates, range, time); - return time; - }; + // create an event handler + var me = this; + var onchange = function () { + var index = slider.getIndex(); - exports.getHiddenDurationBefore = function (hiddenDates, range, time) { - var timeOffset = 0; - time = moment(time).toDate().valueOf(); + me.dataFilter.selectValue(index); + me.dataPoints = me.dataFilter._getDataPoints(); - 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; - } - } + me.redraw(); + }; + slider.setOnChangeCallback(onchange); + } else { + this.frame.filter.slider = undefined; } - 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 slider */ - 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._redrawSlider = function () { + if (this.frame.filter.slider !== undefined) { + this.frame.filter.slider.redraw(); } - - return hiddenDuration; }; - /** - * 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 {*} + * Redraw common information */ - exports.snapAwayFromHidden = function (hiddenDates, time, direction, correctionEnabled) { - var isHidden = exports.isHidden(time, hiddenDates); - if (isHidden.hidden == true) { - if (direction < 0) { - if (correctionEnabled == true) { - return isHidden.startDate - (isHidden.endDate - time) - 1; - } else { - return isHidden.startDate - 1; - } - } else { - if (correctionEnabled == true) { - return isHidden.endDate + (time - isHidden.startDate) + 1; - } else { - return isHidden.endDate + 1; - } - } - } else { - return time; - } - }; - + Graph3d.prototype._redrawInfo = function () { + if (this.dataFilter) { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); - /** - * 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; + ctx.font = "14px arial"; // TODO: put in options + ctx.lineStyle = "gray"; + ctx.fillStyle = "gray"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; - if (time >= startDate && time < endDate) { - // if the start is entering a hidden zone - return { hidden: true, startDate: startDate, endDate: endDate }; - break; - } + var x = this.margin; + var y = this.margin; + ctx.fillText(this.dataFilter.getLabel() + ": " + this.dataFilter.getSelectedValue(), x, y); } - return { hidden: false, startDate: startDate, endDate: endDate }; }; -/***/ }, -/* 16 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; /** - * @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 + * Redraw the axis */ - function DataStep(start, end, minimumStep, containerHeight, customRange, alignZeros) { - // variables - this.current = 0; - - this.autoScale = true; - this.stepIndex = 0; - this.step = 1; - this.scale = 1; - - this.marginStart; - this.marginEnd; - this.deadSpace = 0; + 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; - this.majorSteps = [1, 2, 5, 10]; - this.minorSteps = [0.25, 0.5, 1, 2]; + // TODO: get the actual rendered style of the containerElement + //ctx.font = this.containerElement.style.font; + ctx.font = 24 / this.camera.getArmLength() + "px arial"; - this.alignZeros = alignZeros; + // 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; - this.setRange(start, end, minimumStep, containerHeight, customRange); - } + // draw x-grid lines + ctx.lineWidth = 1; + prettyStep = this.defaultXStep === undefined; + step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep); + step.start(); + if (step.getCurrent() < this.xMin) { + step.next(); + } + while (!step.end()) { + var x = step.getCurrent(); + if (this.showGrid) { + from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } else { + from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin)); + to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); + } - /** - * 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; + 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); - if (this._start == this._end) { - this._start -= 0.75; - this._end += 1; + step.next(); } - if (this.autoScale == true) { - this.setMinimumStep(minimumStep, containerHeight); + // 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(); - this.setFirst(customRange); - }; - - /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds - */ - DataStep.prototype.setMinimumStep = function (minimumStep, containerHeight) { - // round to floor - var size = this._end - this._start; - var safeSize = size * 1.2; - var minimumStepValue = minimumStep * (safeSize / containerHeight); - var orderOfMagnitude = Math.round(Math.log(safeSize) / Math.LN10); + 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(); + } - var minorStepIdx = -1; - var magnitudefactor = Math.pow(10, orderOfMagnitude); + 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); - var start = 0; - if (orderOfMagnitude < 0) { - start = orderOfMagnitude; + step.next(); } - 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; - } + // 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(); } - this.stepIndex = minorStepIdx; - this.scale = magnitudefactor; - this.step = magnitudefactor * this.minorSteps[minorStepIdx]; - }; - + xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax; + yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax; + while (!step.end()) { + // TODO: make z-grid lines really 3d? + from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent())); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(from.x - textMargin, from.y); + ctx.stroke(); + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.colorAxis; + ctx.fillText(this.zValueLabel(step.getCurrent()) + " ", from.x - 5, from.y); - /** - * 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 = {}; + step.next(); } + ctx.lineWidth = 1; + from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin)); + to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax)); + ctx.strokeStyle = this.colorAxis; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(to.x, to.y); + ctx.stroke(); - var 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; + // 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.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; - this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; + // 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(); - // 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; + // 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); } - this.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; - this.marginRange = this.marginEnd - this.marginStart; - - - 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; + // 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); } - }; - - - /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date - */ - DataStep.prototype.hasNext = function () { - return this.current >= this.marginStart; - }; - - /** - * Do the next step - */ - DataStep.prototype.next = function () { - var prev = this.current; - this.current -= this.step; - // safety mechanism: if current time is still unchanged, move to the end - if (this.current == prev) { - this.current = this._end; + // 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); } }; /** - * Do the next step + * 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.previous = function () { - this.current += this.step; - this.marginEnd += this.step; - this.marginRange = this.marginEnd - this.marginStart; - }; - + Graph3d.prototype._hsv2rgb = function (H, S, V) { + var R, G, B, C, Hi, X; + C = V * S; + Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5 + X = C * (1 - Math.abs(H / 60 % 2 - 1)); - /** - * 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); + switch (Hi) { + case 0: + R = C;G = X;B = 0;break; + case 1: + R = X;G = C;B = 0;break; + case 2: + R = 0;G = C;B = X;break; + case 3: + R = 0;G = X;B = C;break; + case 4: + R = X;G = 0;B = C;break; + case 5: + R = C;G = 0;B = X;break; - // If 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; - } - } - } + default: + R = 0;G = 0;B = 0;break; } - return toPrecision; - }; - - /** - * 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; + return "RGB(" + parseInt(R * 255) + "," + parseInt(G * 255) + "," + parseInt(B * 255) + ")"; }; - module.exports = DataStep; - -/***/ }, -/* 17 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var util = __webpack_require__(1); - var hammerUtil = __webpack_require__(43); - var moment = __webpack_require__(40); - var Component = __webpack_require__(25); - var DateUtil = __webpack_require__(15); /** - * @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 + * Draw all datapoints as a grid + * This function can be used when the style is 'grid' */ - function Range(body, options) { - var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); - this.start = now.clone().add(-3, "days").valueOf(); // Number - this.end = now.clone().add(4, "days").valueOf(); // Number - - this.body = body; - this.deltaDifference = 0; - this.scaleOffset = 0; - this.startToFront = false; - this.endToFront = true; - - // default options - this.defaultOptions = { - start: null, - end: null, - direction: "horizontal", // 'horizontal' or 'vertical' - moveable: true, - zoomable: true, - min: null, - max: null, - zoomMin: 10, // milliseconds - zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds - }; - this.options = util.extend({}, this.defaultOptions); - - this.props = { - touch: {} - }; - this.animateTimer = null; - - // drag listeners for dragging - this.body.emitter.on("panstart", this._onDragStart.bind(this)); - this.body.emitter.on("panmove", this._onDrag.bind(this)); - this.body.emitter.on("panend", this._onDragEnd.bind(this)); - - // ignore dragging when holding - this.body.emitter.on("press", this._onHold.bind(this)); - - // mouse wheel for zooming - this.body.emitter.on("mousewheel", this._onMouseWheel.bind(this)); - - // pinch to zoom - this.body.emitter.on("touch", this._onTouch.bind(this)); - this.body.emitter.on("pinch", this._onPinch.bind(this)); + 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.setOptions(options); - } - Range.prototype = new Component(); + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - /** - * 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); + // 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); - if ("start" in options || "end" in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); - } - } - }; + this.dataPoints[i].trans = trans; + this.dataPoints[i].screen = screen; - /** - * 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\"."); + // 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; } - } - /** - * 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(); + // sort the points on depth of their (x,y) position (not on z) + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - if (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.style === Graph3d.STYLE.SURFACE) { + for (i = 0; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + right = this.dataPoints[i].pointRight; + top = this.dataPoints[i].pointTop; + cross = this.dataPoints[i].pointCross; - var 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); + 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) - 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 }); + topSideVisible = crossproduct.z > 0; + } else { + topSideVisible = true; } - if (done) { - if (anyChanged) { - me.body.emitter.emit("rangechanged", { start: new Date(me.start), end: new Date(me.end), byUser: byUser }); + if (topSideVisible) { + // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0 + zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4; + h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240; + s = 1; // saturation + + if (this.showShadow) { + v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = fillStyle; + } else { + v = 1; + fillStyle = this._hsv2rgb(h, s, v); + strokeStyle = this.colorAxis; } } else { - // 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); + fillStyle = "gray"; + strokeStyle = this.colorAxis; } - } - }; + lineWidth = 0.5; - 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); + 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(); + } } - } - }; - - /** - * Stop an animation - * @private - */ - Range.prototype._cancelAnimation = function () { - if (this.animateTimer) { - clearTimeout(this.animateTimer); - this.animateTimer = null; - } - }; - - /** - * 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 - */ - Range.prototype._applyRange = function (start, end) { - var newStart = start != null ? util.convert(start, "Date").valueOf() : this.start, - newEnd = end != null ? util.convert(end, "Date").valueOf() : this.end, - max = this.options.max != null ? util.convert(this.options.max, "Date").valueOf() : null, - min = this.options.min != null ? util.convert(this.options.min, "Date").valueOf() : null, - diff; - - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error("Invalid start \"" + start + "\""); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error("Invalid end \"" + end + "\""); - } - - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } - - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = min - newStart; - newStart += diff; - newEnd += diff; + } 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; - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; + if (point !== undefined) { + if (this.showPerspective) { + lineWidth = 2 / -point.trans.z; + } else { + lineWidth = 2 * -(this.eye.z / this.camera.getArmLength()); } } - } - } - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = newEnd - max; - newStart -= diff; - newEnd -= diff; + 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; - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } + 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(); } - } - } - // 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 (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(); } } } + }; - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } - if (newEnd - newStart > zoomMax) { - if (this.end - this.start === zoomMax && newStart < this.start && newEnd > this.end) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } else { - // zoom to the maximum - diff = newEnd - newStart - zoomMax; - newStart += diff / 2; - newEnd -= diff / 2; - } - } - } + /** + * 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; - var changed = this.start != newStart || this.end != newEnd; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - // 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"); - } + // 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; - this.start = newStart; - this.end = newEnd; - return changed; - }; + // 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; + } - /** - * Retrieve the current range. - * @return {Object} An object with start and end properties - */ - Range.prototype.getRange = function () { - return { - start: this.start, - end: this.end + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; }; - }; + this.dataPoints.sort(sortDepth); - /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.prototype.conversion = function (width, totalHidden) { - return Range.conversion(this.start, this.end, width, totalHidden); - }; + // 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]; - /** - * Static method to calculate the conversion offset and scale for a range, - * based on the provided start, end, and width - * @param {Number} start - * @param {Number} end - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion - */ - Range.conversion = function (start, end, width, totalHidden) { - if (totalHidden === undefined) { - totalHidden = 0; - } - if (width != 0 && end - start != 0) { - return { - offset: start, - scale: width / (end - start - totalHidden) - }; - } else { - return { - offset: 0, - scale: 1 - }; + if (this.style === Graph3d.STYLE.DOTLINE) { + // draw a vertical line from the bottom to the graph value + //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin)); + var from = this._convert3Dto2D(point.bottom); + ctx.lineWidth = 1; + ctx.strokeStyle = this.colorGrid; + ctx.beginPath(); + ctx.moveTo(from.x, from.y); + ctx.lineTo(point.screen.x, point.screen.y); + ctx.stroke(); + } + + // calculate radius for the circle + var size; + if (this.style === Graph3d.STYLE.DOTSIZE) { + size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin); + } else { + size = dotSize; + } + + var radius; + if (this.showPerspective) { + radius = size / -point.trans.z; + } else { + radius = size * -(this.eye.z / this.camera.getArmLength()); + } + if (radius < 0) { + radius = 0; + } + + 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(); } }; /** - * Start dragging horizontally or vertically - * @param {Event} event - * @private + * Draw all datapoints as bars. + * This function can be used when the style is 'bar', 'bar-color', or 'bar-size' */ - Range.prototype._onDragStart = function (event) { - this.deltaDifference = 0; - this.previousDelta = 0; - // only allow dragging when configured as movable - if (!this.options.moveable) return; + Graph3d.prototype._redrawDataBar = function () { + var canvas = this.frame.canvas; + var ctx = canvas.getContext("2d"); + var i, j, surface, corners; - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.dragging = true; + // 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.body.dom.root) { - this.body.dom.root.style.cursor = "move"; + // 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; } - event.preventDefault(); - }; + // order the translated points by depth + var sortDepth = function (a, b) { + return b.dist - a.dist; + }; + this.dataPoints.sort(sortDepth); - /** - * Perform dragging operation - * @param {Event} event - * @private - */ - Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + // 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]; - // 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; + // 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); + } - var direction = this.options.direction; - validateDirection(direction); - var delta = direction == "horizontal" ? event.deltaX : event.deltaY; - delta -= this.deltaDifference; - var interval = this.props.touch.end - this.props.touch.start; + // calculate 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); + } - // normalize dragging speed if cutout is in between. - var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - interval -= duration; + // 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) }]; - 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; + // 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); + }); - // 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; - } + // 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; - this.previousDelta = delta; - this._applyRange(newStart, newEnd); + // 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}) + } - // fire a rangechange event - this.body.emitter.emit("rangechange", { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + // order the surfaces by their (translated) depth + surfaces.sort(function (a, b) { + var diff = b.dist - a.dist; + if (diff) return diff; - event.preventDefault(); + // if equal depth, sort the top surface last + if (a.corners === top) return 1; + if (b.corners === top) return -1; + + // both are equal + return 0; + }); + + // draw the ordered surfaces + ctx.lineWidth = 1; + ctx.strokeStyle = borderColor; + ctx.fillStyle = color; + // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside + for (j = 2; j < surfaces.length; j++) { + surface = surfaces[j]; + corners = surface.corners; + ctx.beginPath(); + ctx.moveTo(corners[3].screen.x, corners[3].screen.y); + ctx.lineTo(corners[0].screen.x, corners[0].screen.y); + ctx.lineTo(corners[1].screen.x, corners[1].screen.y); + ctx.lineTo(corners[2].screen.x, corners[2].screen.y); + ctx.lineTo(corners[3].screen.x, corners[3].screen.y); + ctx.fill(); + ctx.stroke(); + } + } }; + /** - * Stop dragging operation - * @param {event} event - * @private + * Draw a line through all datapoints. + * This function can be used when the style is 'line' */ - Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + Graph3d.prototype._redrawDataLine = function () { + var canvas = this.frame.canvas, + ctx = canvas.getContext("2d"), + point, + i; - // TODO: this may be redundant in hammerjs2 - // refuse to drag when we where pinching to prevent the timeline make a jump - // when releasing the fingers in opposite order from the touch screen - if (!this.props.touch.allowDragging) return; + if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception? - this.props.touch.dragging = false; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = "auto"; + // 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; } - // fire a rangechanged event - this.body.emitter.emit("rangechanged", { - start: new Date(this.start), - end: new Date(this.end), - byUser: true - }); + // start the line + if (this.dataPoints.length > 0) { + point = this.dataPoints[0]; + + ctx.lineWidth = 1; // TODO: make customizable + ctx.strokeStyle = "blue"; // TODO: make customizable + ctx.beginPath(); + ctx.moveTo(point.screen.x, point.screen.y); + } + + // draw the datapoints as colored circles + for (i = 1; i < this.dataPoints.length; i++) { + point = this.dataPoints[i]; + ctx.lineTo(point.screen.x, point.screen.y); + } + + // finish the line + if (this.dataPoints.length > 0) { + ctx.stroke(); + } }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event - * @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._onMouseWheel = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; + Graph3d.prototype._onMouseDown = function (event) { + event = event || window.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; + // check if mouse is still down (may be up when focus is lost for example + // in an iframe) + if (this.leftButtonDown) { + this._onMouseUp(event); } - // If 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); - } + // only react on left mouse button down + this.leftButtonDown = event.which ? event.which === 1 : event.button === 1; + if (!this.leftButtonDown && !this.touchDown) return; - // 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); + // get mouse position (different code for IE and all other browsers) + this.startMouseX = getMouseX(event); + this.startMouseY = getMouseY(event); - this.zoom(scale, pointerDate, delta); - } + this.startStart = new Date(this.start); + this.startEnd = new Date(this.end); + this.startArmRotation = this.camera.getArmRotation(); - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); - }; + this.frame.style.cursor = "move"; - /** - * Start of a touch gesture - * @private - */ - Range.prototype._onTouch = function (event) { - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.allowDragging = true; - this.props.touch.center = null; - this.scaleOffset = 0; - this.deltaDifference = 0; + // 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); }; - /** - * On start of a hold gesture - * @private - */ - Range.prototype._onHold = function () { - this.props.touch.allowDragging = false; - }; /** - * Handle pinch event - * @param {Event} event - * @private + * Perform moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {Event} event Well, eehh, the event */ - Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; - - this.props.touch.allowDragging = false; - - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.center, this.body.dom.center); - } - - var scale = 1 / (event.scale + this.scaleOffset); - var centerDate = this._pointerToDate(this.props.touch.center); + Graph3d.prototype._onMouseMove = function (event) { + event = event || window.event; - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + // calculate change in mouse position + var diffX = parseFloat(getMouseX(event)) - this.startMouseX; + var diffY = parseFloat(getMouseY(event)) - this.startMouseY; - // calculate new start and end - var newStart = centerDate - hiddenDurationBefore + (this.props.touch.start - (centerDate - hiddenDurationBefore)) * scale; - var newEnd = centerDate + hiddenDurationAfter + (this.props.touch.end - (centerDate + hiddenDurationAfter)) * scale; + var horizontalNew = this.startArmRotation.horizontal + diffX / 200; + var verticalNew = this.startArmRotation.vertical + diffY / 200; - // snapping times away from hidden zones - this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times - this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times + var snapAngle = 4; // degrees + var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI); - 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; + // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc... + // the -0.001 is to take care that the vertical axis is always drawn at the left front corner + if (Math.abs(Math.sin(horizontalNew)) < snapValue) { + horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001; + } + if (Math.abs(Math.cos(horizontalNew)) < snapValue) { + horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001; + } + + // snap vertically to nice angles + if (Math.abs(Math.sin(verticalNew)) < snapValue) { + verticalNew = Math.round(verticalNew / Math.PI) * Math.PI; + } + if (Math.abs(Math.cos(verticalNew)) < snapValue) { + verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI; } - this.setRange(newStart, newEnd, false, true); + this.camera.setArmRotation(horizontalNew, verticalNew); + this.redraw(); - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + // fire a cameraPositionChange event + var parameters = this.getCameraPosition(); + this.emit("cameraPositionChange", parameters); - event.preventDefault(); + util.preventDefault(event); }; + /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date - * @private + * Stop moving operating. + * This function activated from within the funcion Graph.mouseDown(). + * @param {event} event The event */ - Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; - - validateDirection(direction); + Graph3d.prototype._onMouseUp = function (event) { + this.frame.style.cursor = "auto"; + this.leftButtonDown = false; - 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; - } + // remove event listeners here + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); + util.preventDefault(event); }; /** - * Get the pointer location relative to the location of the dom element - * @param {{x: Number, y: Number}} touch - * @param {Element} element HTML DOM element - * @return {{x: Number, y: Number}} pointer - * @private + * 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 */ - function getPointer(touch, element) { - return { - x: touch.x - util.getAbsoluteLeft(element), - y: touch.y - util.getAbsoluteTop(element) - }; - } + 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; - /** - * 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; + if (!this.showTooltip) { + return; } - var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); - var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); - var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - - // calculate new start and end - var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; - var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); + } - // 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; + // (delayed) display of a tooltip only if no mouse button is down + if (this.leftButtonDown) { + this._hideTooltip(); + return; } - this.setRange(newStart, newEnd, false, true); + if (this.tooltip && this.tooltip.dataPoint) { + // tooltip is currently visible + var dataPoint = this._dataPointFromXY(mouseX, mouseY); + if (dataPoint !== this.tooltip.dataPoint) { + // datapoint changed + if (dataPoint) { + this._showTooltip(dataPoint); + } else { + this._hideTooltip(); + } + } + } else { + // tooltip is currently not visible + var me = this; + this.tooltipTimeout = setTimeout(function () { + me.tooltipTimeout = null; - this.startToFront = false; // revert to default - this.endToFront = true; // revert to default + // show a tooltip if we have a data point + var dataPoint = me._dataPointFromXY(mouseX, mouseY); + if (dataPoint) { + me._showTooltip(dataPoint); + } + }, delay); + } }; - - /** - * 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 + * Event handler for touchstart event on mobile devices */ - 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; + Graph3d.prototype._onTouchStart = function (event) { + this.touchDown = true; - // TODO: reckon with min and max range + 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.start = newStart; - this.end = newEnd; + this._onMouseDown(event); }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * Event handler for touchmove event on mobile devices */ - Range.prototype.moveTo = function (moveTo) { - var center = (this.start + this.end) / 2; + Graph3d.prototype._onTouchMove = function (event) { + this._onMouseMove(event); + }; - var diff = center - moveTo; + /** + * Event handler for touchend event on mobile devices + */ + Graph3d.prototype._onTouchEnd = function (event) { + this.touchDown = false; - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + util.removeEventListener(document, "touchmove", this.ontouchmove); + util.removeEventListener(document, "touchend", this.ontouchend); - this.setRange(newStart, newEnd); + this._onMouseUp(event); }; - module.exports = Range; -/***/ }, -/* 18 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Event handler for mouse wheel event, used to zoom the graph + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {event} event The event + */ + Graph3d.prototype._onWheel = function (event) { + if (!event) /* For IE. */ + event = window.event; - "use strict"; + // 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; + } - // Utility functions for ordering and stacking of items - var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors + // 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); - /** - * Order items by their start data - * @param {Item[]} items - */ - exports.orderByStart = function (items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); - }; + this.camera.setArmLength(newLength); + this.redraw(); - /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items - */ - exports.orderByEnd = function (items) { - items.sort(function (a, b) { - var aTime = "end" in a.data ? a.data.end : a.data.start, - bTime = "end" in b.data ? b.data.end : b.data.start; + this._hideTooltip(); + } - return aTime - bTime; - }); + // 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); }; /** - * 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 + * Test whether a point lies inside given 2D triangle + * @param {Point2d} point + * @param {Point2d[]} triangle + * @return {boolean} Returns true if given point lies inside or on the edge of the triangle + * @private */ - exports.stack = function (items, margin, force) { - var i, iMax; + Graph3d.prototype._insideTriangle = function (point, triangle) { + var a = triangle[0], + b = triangle[1], + c = triangle[2]; - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } + function sign(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; } - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.stack && item.top === null) { - // initialize top position - item.top = margin.axis; - - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - var collidingItem = null; - for (var j = 0, jj = items.length; j < jj; j++) { - var other = items[j]; - if (other.top !== null && other !== item && other.stack && exports.collision(item, other, margin.item)) { - collidingItem = other; - break; - } - } + 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)); - 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); - } - } + // 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); }; - /** - * 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. + * Find a data point close to given screen position (x, y) + * @param {Number} x + * @param {Number} y + * @return {Object | null} The closest data point or null if not close to any data point + * @private */ - exports.nostack = function (items, margin, subgroups) { - var i, iMax, newTop; + Graph3d.prototype._dataPointFromXY = function (x, y) { + var i, + distMax = 100, + // px + dataPoint = null, + closestDataPoint = null, + closestDist = null, + center = new Point2d(x, y); - // 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; + 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; } } } - items[i].top = newTop; - } else { - items[i].top = margin.axis; } - } - }; - - /** - * Test if the two provided items collide - * The items must have parameters left, width, top, and height. - * @param {Item} a The first item - * @param {Item} b The second item - * @param {{horizontal: number, vertical: number}} margin - * An object containing a horizontal and vertical - * minimum required margin. - * @return {boolean} true if a and b collide, else false - */ - exports.collision = function (a, b, margin) { - return a.left - margin.horizontal + EPSILON < b.left + b.width && a.left + a.width + margin.horizontal - EPSILON > b.left && a.top - margin.vertical + EPSILON < b.top + b.height && a.top + a.height + margin.vertical - EPSILON > b.top; - }; + } 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); -/***/ }, -/* 19 */ -/***/ function(module, exports, __webpack_require__) { + if ((closestDist === null || dist < closestDist) && dist < distMax) { + closestDist = dist; + closestDataPoint = dataPoint; + } + } + } + } - "use strict"; - var moment = __webpack_require__(40); - var DateUtil = __webpack_require__(15); - var util = __webpack_require__(1); + return closestDataPoint; + }; /** - * @constructor TimeStep - * The class TimeStep is an iterator for dates. You provide a start date and an - * end date. The class itself determines the best scale (step size) based on the - * provided start Date, end Date, and minimumStep. - * - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * - * Alternatively, you can set a scale by hand. - * After creation, you can initialize the class by executing first(). Then you - * can iterate from the start date to the end date via next(). You can check if - * the end date is reached with the function hasNext(). After each step, you can - * retrieve the current date via getCurrent(). - * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, - * days, to years. - * - * Version: 1.2 - * - * @param {Date} [start] The start date, for example new Date(2010, 9, 21) - * or new Date(2010, 9, 21, 23, 45, 00) - * @param {Date} [end] The end date - * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds + * Display a tooltip for given data point + * @param {Object} dataPoint + * @private */ - 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; + Graph3d.prototype._showTooltip = function (dataPoint) { + var content, line, dot; - // initialize the range - this.setRange(start, end, minimumStep); + 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)"; - // hidden Dates options - this.switchedDay = false; - this.switchedMonth = false; - this.switchedYear = false; - this.hiddenDates = hiddenDates; - if (hiddenDates === undefined) { - this.hiddenDates = []; - } + line = document.createElement("div"); + line.style.position = "absolute"; + line.style.height = "40px"; + line.style.width = "0"; + line.style.borderLeft = "1px solid #4d4d4d"; - this.format = TimeStep.FORMAT; // default formatting - } + 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"; - // 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.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; } - }; - /** - * 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); - }; + this._hideTooltip(); - /** - * Set a new range - * If minimumStep is provided, the step size is chosen as close as possible - * to the minimumStep but larger than minimumStep. If minimumStep is not - * provided, the scale is set to 1 DAY. - * The minimumStep should correspond with the onscreen size of about 6 characters - * @param {Date} [start] The start date and time. - * @param {Date} [end] The end date and time. - * @param {int} [minimumStep] Optional. Minimum step size in milliseconds - */ - TimeStep.prototype.setRange = function (start, end, minimumStep) { - if (!(start instanceof Date) || !(end instanceof Date)) { - throw "No legal start or end date in method setRange"; + 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 + "
"; } - this._start = start != undefined ? new Date(start.valueOf()) : new Date(); - this._end = end != undefined ? new Date(end.valueOf()) : new Date(); + content.style.left = "0"; + content.style.top = "0"; + this.frame.appendChild(content); + this.frame.appendChild(line); + this.frame.appendChild(dot); - if (this.autoScale) { - this.setMinimumStep(minimumStep); - } - }; + // calculate sizes + var contentWidth = content.offsetWidth; + var contentHeight = content.offsetHeight; + var lineHeight = line.offsetHeight; + var dotWidth = dot.offsetWidth; + var dotHeight = dot.offsetHeight; - /** - * Set the range iterator to the start date. - */ - TimeStep.prototype.first = function () { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); + 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"; }; /** - * Round the current date to the first minor date value - * This must be executed once when the current date is set to start Date + * Hide the tooltip when displayed + * @private */ - 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 - } + Graph3d.prototype._hideTooltip = function () { + if (this.tooltip) { + this.tooltip.dataPoint = null; - 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; + 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); + } + } } } }; + /**--------------------------------------------------------------------------**/ + + /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date + * Get the horizontal mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse x */ - TimeStep.prototype.hasNext = function () { - return this.current.valueOf() <= this._end.valueOf(); - }; + function getMouseX(event) { + if ("clientX" in event) { + return event.clientX; + }return event.targetTouches[0] && event.targetTouches[0].clientX || 0; + } /** - * Do the next step + * Get the vertical mouse position from a mouse event + * @param {Event} event + * @return {Number} mouse y */ - 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; - } - } + function getMouseY(event) { + if ("clientY" in event) { + return event.clientY; + }return event.targetTouches[0] && event.targetTouches[0].clientY || 0; + } - 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; - } - } + module.exports = Graph3d; + // use use defaults - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); - } +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { - DateUtil.stepOverHiddenDates(this, prev); - }; + + /** + * Expose `Emitter`. + */ + module.exports = Emitter; /** - * Get the current datetime - * @return {Date} current The current date + * Initialize a new `Emitter`. + * + * @api public */ - TimeStep.prototype.getCurrent = function () { - return this.current; + + function Emitter(obj) { + if (obj) return mixin(obj); }; /** - * 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. + * Mixin the emitter properties. * - * @param {{scale: string, step: number}} params - * An object containing two properties: - * - A string 'scale'. Choose from 'millisecond', 'second', - * 'minute', 'hour', 'weekday, 'day, 'month, 'year'. - * - A number 'step'. A step size, by default 1. - * Choose for example 1, 2, 5, or 10. + * @param {Object} obj + * @return {Object} + * @api private */ - TimeStep.prototype.setScale = function (params) { - if (params && typeof params.scale == "string") { - this.scale = params.scale; - this.step = params.step > 0 ? params.step : 1; - this.autoScale = false; + + function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; } - }; + return obj; + } /** - * Enable or disable autoscaling - * @param {boolean} enable If true, autoascaling is set true + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; - }; + Emitter.prototype.on = + Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; + }; /** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - TimeStep.prototype.setMinimumStep = function (minimumStep) { - if (minimumStep == undefined) { - return; - } - - //var b = asc + ds; - var stepYear = 1000 * 60 * 60 * 24 * 30 * 12; - var stepMonth = 1000 * 60 * 60 * 24 * 30; - var stepDay = 1000 * 60 * 60 * 24; - var stepHour = 1000 * 60 * 60; - var stepMinute = 1000 * 60; - var stepSecond = 1000; - var stepMillisecond = 1; + Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; - // find the smallest step that is larger than the provided minimumStep - if (stepYear * 1000 > minimumStep) { - this.scale = "year";this.step = 1000; - } - if (stepYear * 500 > minimumStep) { - this.scale = "year";this.step = 500; - } - if (stepYear * 100 > minimumStep) { - this.scale = "year";this.step = 100; - } - if (stepYear * 50 > minimumStep) { - this.scale = "year";this.step = 50; - } - if (stepYear * 10 > minimumStep) { - this.scale = "year";this.step = 10; - } - if (stepYear * 5 > minimumStep) { - this.scale = "year";this.step = 5; - } - if (stepYear > minimumStep) { - this.scale = "year";this.step = 1; - } - if (stepMonth * 3 > minimumStep) { - this.scale = "month";this.step = 3; - } - if (stepMonth > minimumStep) { - this.scale = "month";this.step = 1; - } - if (stepDay * 5 > minimumStep) { - this.scale = "day";this.step = 5; - } - if (stepDay * 2 > minimumStep) { - this.scale = "day";this.step = 2; - } - if (stepDay > minimumStep) { - this.scale = "day";this.step = 1; - } - if (stepDay / 2 > minimumStep) { - this.scale = "weekday";this.step = 1; - } - if (stepHour * 4 > minimumStep) { - this.scale = "hour";this.step = 4; - } - if (stepHour > minimumStep) { - this.scale = "hour";this.step = 1; - } - if (stepMinute * 15 > minimumStep) { - this.scale = "minute";this.step = 15; - } - if (stepMinute * 10 > minimumStep) { - this.scale = "minute";this.step = 10; - } - if (stepMinute * 5 > minimumStep) { - this.scale = "minute";this.step = 5; - } - if (stepMinute > minimumStep) { - this.scale = "minute";this.step = 1; - } - if (stepSecond * 15 > minimumStep) { - this.scale = "second";this.step = 15; - } - if (stepSecond * 10 > minimumStep) { - this.scale = "second";this.step = 10; - } - if (stepSecond * 5 > minimumStep) { - this.scale = "second";this.step = 5; - } - if (stepSecond > minimumStep) { - this.scale = "second";this.step = 1; - } - if (stepMillisecond * 200 > minimumStep) { - this.scale = "millisecond";this.step = 200; - } - if (stepMillisecond * 100 > minimumStep) { - this.scale = "millisecond";this.step = 100; - } - if (stepMillisecond * 50 > minimumStep) { - this.scale = "millisecond";this.step = 50; - } - if (stepMillisecond * 10 > minimumStep) { - this.scale = "millisecond";this.step = 10; - } - if (stepMillisecond * 5 > minimumStep) { - this.scale = "millisecond";this.step = 5; - } - if (stepMillisecond > minimumStep) { - this.scale = "millisecond";this.step = 1; + function on() { + self.off(event, on); + fn.apply(this, arguments); } + + on.fn = fn; + this.on(event, on); + return this; }; /** - * 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 + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ - TimeStep.snap = function (date, scale, step) { - var clone = new Date(date.valueOf()); - 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); - } + Emitter.prototype.off = + Emitter.prototype.removeListener = + Emitter.prototype.removeAllListeners = + Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; - 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); + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; } - return clone; + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; + + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } + + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; }; /** - * Check if 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. + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} */ - 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; + + Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; + + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); } } - 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; - } + return this; }; - /** - * 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 array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public */ - TimeStep.prototype.getLabelMinor = function (date) { - if (date == undefined) { - date = this.current; - } - var format = this.format.minorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ""; + Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; }; /** - * 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 + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public */ - TimeStep.prototype.getLabelMajor = function (date) { - if (date == undefined) { - date = this.current; - } - var format = this.format.majorLabels[this.scale]; - return format && format.length > 0 ? moment(date).format(format) : ""; + Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; }; - 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"; - } +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { - 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 ""; - } + "use strict"; - function currentWeek(date) { - return date.isSame(new Date(), "week") ? " current-week" : ""; - } + /** + * @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; + }; - function currentMonth(date) { - return date.isSame(new Date(), "month") ? " current-month" : ""; - } + /** + * Subtract the two provided points, returns a-b + * @param {Point3d} a + * @param {Point3d} b + * @return {Point3d} a-b + */ + Point3d.subtract = function (a, b) { + var sub = new Point3d(); + sub.x = a.x - b.x; + sub.y = a.y - b.y; + sub.z = a.z - b.z; + return sub; + }; - function currentYear(date) { - return date.isSame(new Date(), "year") ? " current-year" : ""; - } + /** + * 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; + }; - switch (this.scale) { - case "millisecond": - return even(date.milliseconds()).trim(); + /** + * 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); + }; - case "second": - return even(date.seconds()).trim(); + /** + * 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(); - case "minute": - return even(date.minutes()).trim(); + 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; - case "hour": - var hours = date.hours(); - if (this.step == 4) { - hours = hours + "-h" + (hours + 4); - } - return "h" + hours + today(date) + even(date.hours()); + return crossproduct; + }; - 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); + /** + * Rtrieve the length of the vector (or the distance from this point to the origin + * @return {Number} length + */ + Point3d.prototype.length = function () { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); + }; - case "month": - return date.format("MMMM").toLowerCase() + currentMonth(date) + even(date.month()); + module.exports = Point3d; - case "year": - var year = date.year(); - return "year" + year + currentYear(date) + even(year); +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { - default: - return ""; - } - }; + "use strict"; - module.exports = TimeStep; + /** + * @prototype Point2d + * @param {Number} [x] + * @param {Number} [y] + */ + function Point2d(x, y) { + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : 0; + } + + module.exports = Point2d; /***/ }, -/* 20 */ +/* 14 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); + var Point3d = __webpack_require__(12); /** - * @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 + * @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 */ - function Item(data, conversion, options) { - this.id = null; - this.parent = null; - this.data = data; - this.dom = null; - this.conversion = conversion || {}; - this.options = options || {}; + function Camera() { + this.armLocation = new Point3d(); + this.armRotation = {}; + this.armRotation.horizontal = 0; + this.armRotation.vertical = 0; + this.armLength = 1.7; - this.selected = false; - this.displayed = false; - this.dirty = true; + this.cameraLocation = new Point3d(); + this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0); - this.top = null; - this.left = null; - this.width = null; - this.height = null; + this.calculateCameraOrientation(); } - Item.prototype.stack = true; + /** + * Set the location (origin) of the arm + * @param {Number} x Normalized value of x + * @param {Number} y Normalized value of y + * @param {Number} z Normalized value of z + */ + Camera.prototype.setArmLocation = function (x, y, z) { + this.armLocation.x = x; + this.armLocation.y = y; + this.armLocation.z = z; + + this.calculateCameraOrientation(); + }; /** - * Select current item + * 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. */ - Item.prototype.select = function () { - this.selected = true; - this.dirty = true; - if (this.displayed) this.redraw(); + Camera.prototype.setArmRotation = function (horizontal, vertical) { + if (horizontal !== undefined) { + this.armRotation.horizontal = horizontal; + } + + 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 (horizontal !== undefined || vertical !== undefined) { + this.calculateCameraOrientation(); + } }; /** - * Unselect current item + * Retrieve the current arm rotation + * @return {object} An object with parameters horizontal and vertical */ - Item.prototype.unselect = function () { - this.selected = false; - this.dirty = true; - if (this.displayed) this.redraw(); + Camera.prototype.getArmRotation = function () { + var rot = {}; + rot.horizontal = this.armRotation.horizontal; + rot.vertical = this.armRotation.vertical; + + return rot; }; /** - * 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 + * Set the (normalized) length of the camera arm. + * @param {Number} length A length between 0.71 and 5.0 */ - Item.prototype.setData = function (data) { - this.data = data; - this.dirty = true; - if (this.displayed) this.redraw(); + Camera.prototype.setArmLength = function (length) { + if (length === undefined) return; + + this.armLength = length; + + // 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(); }; /** - * Set a parent for the item - * @param {ItemSet | Group} parent + * Retrieve the arm length + * @return {Number} length */ - Item.prototype.setParent = function (parent) { - if (this.displayed) { - this.hide(); - this.parent = parent; - if (this.parent) { - this.show(); - } + Camera.prototype.getArmLength = function () { + return this.armLength; + }; + + /** + * Retrieve the camera location + * @return {Point3d} cameraLocation + */ + Camera.prototype.getCameraLocation = function () { + return this.cameraLocation; + }; + + /** + * Retrieve the camera rotation + * @return {Point3d} cameraRotation + */ + Camera.prototype.getCameraRotation = function () { + return this.cameraRotation; + }; + + /** + * Calculate the location and rotation of the camera based on the + * position and orientation of the camera arm + */ + Camera.prototype.calculateCameraOrientation = function () { + // calculate location of the camera + this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); + this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); + + // 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; + +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var DataView = __webpack_require__(9); + + /** + * @class Filter + * + * @param {DataSet} data The google data table + * @param {Number} column The index of the column to be filtered + * @param {Graph} graph The graph + */ + function Filter(data, column, graph) { + this.data = data; + this.column = column; + this.graph = graph; // the parent graph + + this.index = undefined; + this.value = undefined; + + // read all distinct values and select the first one + this.values = graph.getDistinctValues(data.get(), this.column); + + // sort both numeric and string values correctly + this.values.sort(function (a, b) { + return a > b ? 1 : a < b ? -1 : 0; + }); + + if (this.values.length > 0) { + this.selectValue(0); + } + + // create an array with the filtered datapoints. this will be loaded afterwards + this.dataPoints = []; + + this.loaded = false; + this.onLoadCallback = undefined; + + if (graph.animationPreload) { + this.loaded = false; + this.loadInBackground(); } else { - this.parent = parent; + this.loaded = true; } }; + /** - * 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 + * Return the label + * @return {string} label */ - Item.prototype.isVisible = function (range) { - // Should be implemented by Item implementations - return false; + Filter.prototype.isLoaded = function () { + return this.loaded; + }; + + + /** + * 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 Math.round(i / len * 100); }; + /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed + * Return the label + * @return {string} label */ - Item.prototype.show = function () { - return false; + Filter.prototype.getLabel = function () { + return this.graph.filterLabel; }; + /** - * Hide the Item from the DOM (when visible) - * @return {Boolean} changed + * Return the columnIndex of the filter + * @return {Number} columnIndex */ - Item.prototype.hide = function () { - return false; + Filter.prototype.getColumn = function () { + return this.column; }; /** - * Repaint the item + * Return the currently selected value. Returns undefined if there is no selection + * @return {*} value */ - Item.prototype.redraw = function () {}; + Filter.prototype.getSelectedValue = function () { + if (this.index === undefined) return undefined; + + return this.values[this.index]; + }; /** - * Reposition the Item horizontally + * Retrieve all values of the filter + * @return {Array} values */ - Item.prototype.repositionX = function () {}; + Filter.prototype.getValues = function () { + return this.values; + }; /** - * Reposition the Item vertically + * Retrieve one value of the filter + * @param {Number} index + * @return {*} value */ - Item.prototype.repositionY = function () {}; + Filter.prototype.getValue = function (index) { + if (index >= this.values.length) throw "Error: index out of range"; + + return this.values[index]; + }; + /** - * Repaint a delete button on the top right of the item when the item is selected - * @param {HTMLElement} anchor - * @protected + * Retrieve the (filtered) dataPoints for the currently selected filter index + * @param {Number} [index] (optional) + * @return {Array} dataPoints */ - Item.prototype._repaintDeleteButton = function (anchor) { - if (this.selected && this.options.editable.remove && !this.dom.deleteButton) { - // create and show button - var me = this; + Filter.prototype._getDataPoints = function (index) { + if (index === undefined) index = this.index; - var deleteButton = document.createElement("div"); - deleteButton.className = "delete"; - deleteButton.title = "Delete this item"; + if (index === undefined) return []; - // TODO: be able to destroy the delete button - new Hammer(deleteButton).on("tap", function (event) { - me.parent.removeFromDataSet(me); - event.stopPropagation(); - event.preventDefault(); - }); + var dataPoints; + if (this.dataPoints[index]) { + dataPoints = this.dataPoints[index]; + } else { + var f = {}; + f.column = this.column; + f.value = this.values[index]; - anchor.appendChild(deleteButton); - this.dom.deleteButton = deleteButton; - } else if (!this.selected && this.dom.deleteButton) { - // remove button - if (this.dom.deleteButton.parentNode) { - this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); - } - this.dom.deleteButton = null; + var dataView = new DataView(this.data, { filter: function (item) { + return item[f.column] == f.value; + } }).get(); + dataPoints = this.graph._getDataPoints(dataView); + + this.dataPoints[index] = dataPoints; } + + return dataPoints; }; - /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private - */ - Item.prototype._updateContents = function (element) { - var content; - if (this.options.template) { - var itemData = this.parent.itemSet.itemsData.get(this.id); // get a clone of the data from the dataset - content = this.options.template(itemData); - } else { - content = this.data.content; - } - if (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.content = content; - } + /** + * Set a callback function when the filter is fully loaded. + */ + Filter.prototype.setOnLoadCallback = function (callback) { + this.onLoadCallback = callback; }; + /** - * Set HTML contents for the item - * @param {Element} element HTML element to fill with the contents - * @private + * Add a value to the list with available values for this filter + * No double entries will be created. + * @param {Number} index */ - Item.prototype._updateTitle = function (element) { - if (this.data.title != null) { - element.title = this.data.title || ""; - } else { - element.removeAttribute("title"); - } + Filter.prototype.selectValue = function (index) { + if (index >= this.values.length) throw "Error: index out of range"; + + this.index = index; + this.value = this.values[index]; }; /** - * 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 + * Load all filtered rows in the background one by one + * Start this method without providing an index! */ - Item.prototype._updateDataAttributes = function (element) { - if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { - var attributes = []; + Filter.prototype.loadInBackground = function (index) { + if (index === undefined) index = 0; - if (Array.isArray(this.options.dataAttributes)) { - attributes = this.options.dataAttributes; - } else if (this.options.dataAttributes == "all") { - attributes = Object.keys(this.data); - } else { - return; - } + var frame = this.graph.frame; - for (var i = 0; i < attributes.length; i++) { - var name = attributes[i]; - var value = this.data[name]; + if (index < this.values.length) { + var dataPointsTemp = this._getDataPoints(index); + //this.graph.redrawInfo(); // TODO: not neat - if (value != null) { - element.setAttribute("data-" + name, value); - } else { - element.removeAttribute("data-" + name); - } + // create a progress box + if (frame.progress === undefined) { + frame.progress = document.createElement("DIV"); + frame.progress.style.position = "absolute"; + frame.progress.style.color = "gray"; + frame.appendChild(frame.progress); } - } - }; + var progress = this.getLoadedProgress(); + frame.progress.innerHTML = "Loading animation... " + progress + "%"; + // TODO: this is no nice solution... + frame.progress.style.bottom = 60 + "px"; // TODO: use height of slider + frame.progress.style.left = 10 + "px"; - /** - * Update 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 me = this; + setTimeout(function () { + me.loadInBackground(index + 1); + }, 10); + this.loaded = false; + } else { + this.loaded = true; - // append new styles - if (this.data.style) { - util.addCssText(element, this.data.style); - this.style = this.data.style; + // remove the progress box + if (frame.progress !== undefined) { + frame.removeChild(frame.progress); + frame.progress = undefined; + } + + if (this.onLoadCallback) this.onLoadCallback(); } }; - module.exports = Item; - // should be implemented by the item - // should be implemented by the item - // should be implemented by the item + module.exports = Filter; /***/ }, -/* 21 */ +/* 16 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Hammer = __webpack_require__(41); - var Item = __webpack_require__(20); - var BackgroundGroup = __webpack_require__(31); - var RangeItem = __webpack_require__(24); + var util = __webpack_require__(1); /** - * @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 + * @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. */ - // 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); - } + 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; - Item.call(this, data, conversion, options); + 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.emptyContent = false; - } + this.frame.prev = document.createElement("INPUT"); + this.frame.prev.type = "BUTTON"; + this.frame.prev.value = "Prev"; + this.frame.appendChild(this.frame.prev); - BackgroundItem.prototype = new Item(null, null, null); + this.frame.play = document.createElement("INPUT"); + this.frame.play.type = "BUTTON"; + this.frame.play.value = "Play"; + this.frame.appendChild(this.frame.play); - BackgroundItem.prototype.baseClassName = "item background"; - BackgroundItem.prototype.stack = false; + this.frame.next = document.createElement("INPUT"); + this.frame.next.type = "BUTTON"; + this.frame.next.value = "Next"; + this.frame.appendChild(this.frame.next); - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - BackgroundItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; - }; + this.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); - /** - * Repaint the item - */ - BackgroundItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + 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); - // background box - dom.box = document.createElement("div"); - // className is updated in redraw() + // 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); + }; + } - // contents box - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); + this.onChangeCallback = undefined; - // 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.values = []; + this.index = undefined; - this.dirty = true; - } + this.playTimeout = undefined; + this.playInterval = 1000; // milliseconds + this.playLoop = true; + } - // append DOM to parent DOM - if (!this.parent) { - throw new Error("Cannot redraw item: no parent attached"); + /** + * Select the previous index + */ + Slider.prototype.prev = function () { + var index = this.getIndex(); + if (index > 0) { + index--; + this.setIndex(index); } - 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); + }; + + /** + * Select the next index + */ + Slider.prototype.next = function () { + var index = this.getIndex(); + if (index < this.values.length - 1) { + index++; + this.setIndex(index); } - 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); + /** + * Select the next index + */ + Slider.prototype.playNext = function () { + var start = new Date(); - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = this.baseClassName + className; + 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); + } - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; + var end = new Date(); + var diff = end - start; - // 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 + // 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 - this.dirty = false; - } + var me = this; + this.playTimeout = setTimeout(function () { + me.playNext(); + }, interval); }; /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * Toggle start or stop playing */ - BackgroundItem.prototype.show = RangeItem.prototype.show; + Slider.prototype.togglePlay = function () { + if (this.playTimeout === undefined) { + this.play(); + } else { + this.stop(); + } + }; /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed + * Start playing */ - BackgroundItem.prototype.hide = RangeItem.prototype.hide; + Slider.prototype.play = function () { + // Test whether already playing + if (this.playTimeout) return; + + this.playNext(); + + if (this.frame) { + this.frame.play.value = "Stop"; + } + }; /** - * Reposition the item horizontally - * @Override + * Stop playing */ - BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; + Slider.prototype.stop = function () { + clearInterval(this.playTimeout); + this.playTimeout = undefined; + + if (this.frame) { + this.frame.play.value = "Play"; + } + }; /** - * Reposition the item vertically - * @Override + * Set a callback function which will be triggered when the value of the + * slider bar has changed. */ - 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; - } - } - } + Slider.prototype.setOnChangeCallback = function (callback) { + this.onChangeCallback = callback; + }; - // 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"; + /** + * Set the interval for playing the list + * @param {Number} interval The interval in milliseconds + */ + Slider.prototype.setPlayInterval = function (interval) { + this.playInterval = interval; }; - module.exports = BackgroundItem; + /** + * Retrieve the current play interval + * @return {Number} interval The interval in milliseconds + */ + Slider.prototype.getPlayInterval = function (interval) { + return this.playInterval; + }; -/***/ }, -/* 22 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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; + }; - "use strict"; - var Item = __webpack_require__(20); - var util = __webpack_require__(1); + /** + * Execute the onchange callback function + */ + Slider.prototype.onChange = function () { + if (this.onChangeCallback !== undefined) { + this.onChangeCallback(); + } + }; /** - * @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 + * redraw the slider on the correct place */ - function BoxItem(data, conversion, options) { - this.props = { - dot: { - width: 0, - height: 0 - }, - line: { - width: 0, - height: 0 - } - }; + 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"; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data); - } + // position the slider button + var left = this.indexToLeft(this.index); + this.frame.slide.style.left = left + "px"; } + }; - Item.call(this, data, conversion, options); - } - BoxItem.prototype = new Item(null, null, null); + /** + * Set the list with values for the slider + * @param {Array} values A javascript array with values (any type) + */ + Slider.prototype.setValues = function (values) { + this.values = values; + + if (this.values.length > 0) this.setIndex(0);else this.index = undefined; + }; /** - * 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 a value by its index + * @param {Number} index */ - 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; + Slider.prototype.setIndex = function (index) { + if (index < this.values.length) { + this.index = index; + + this.redraw(); + this.onChange(); + } else { + throw "Error: index out of range"; + } }; /** - * Repaint the item + * retrieve the index of the currently selected vaue + * @return {Number} index */ - BoxItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; + Slider.prototype.getIndex = function () { + return this.index; + }; - // create main box - dom.box = document.createElement("DIV"); - // contents box (inside the background box). used for making margins - dom.content = document.createElement("DIV"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); + /** + * retrieve the currently selected value + * @return {*} value + */ + Slider.prototype.get = function () { + return this.values[this.index]; + }; - // line to axis - dom.line = document.createElement("DIV"); - dom.line.className = "line"; - // dot on axis - dom.dot = document.createElement("DIV"); - dom.dot.className = "dot"; + 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; - // attach this item as attribute - dom.box["timeline-item"] = this; + this.startClientX = event.clientX; + this.startSlideX = parseFloat(this.frame.slide.style.left); - this.dirty = true; - } + this.frame.style.cursor = "move"; - // 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; + // 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); + }; - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = "item box" + className; - dom.line.className = "item line" + className; - dom.dot.className = "item dot" + className; + Slider.prototype.leftToIndex = function (left) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + var x = left - 3; - // 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; + 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; - this.dirty = false; - } + return index; + }; - this._repaintDeleteButton(dom.box); + Slider.prototype.indexToLeft = function (index) { + var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10; + + var x = index / (this.values.length - 1) * width; + var left = x + 3; + + return left; + }; + + + + Slider.prototype._onMouseMove = function (event) { + var diff = event.clientX - this.startClientX; + var x = this.startSlideX + diff; + + var index = this.leftToIndex(x); + + this.setIndex(index); + + util.preventDefault(); + }; + + + Slider.prototype._onMouseUp = function (event) { + this.frame.style.cursor = "auto"; + + // remove event listeners + util.removeEventListener(document, "mousemove", this.onmousemove); + util.removeEventListener(document, "mouseup", this.onmouseup); + + util.preventDefault(); }; + module.exports = Slider; + +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + /** - * 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 + function Timeline(container, items, groups, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError("Constructor must be called with the new operator"); + } + + // if the third element is options, the forth is groups (optionally); + if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) { + var forthArgument = options; + options = groups; + groups = forthArgument; + } + + var me = this; + this.defaultOptions = { + start: null, + end: null, + + autoResize: true, + + orientation: "bottom", + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); + + // Create the DOM, props, and emitter + this._create(container); + + // all components listed here will be repainted automatically + this.components = []; + + this.body = { + dom: this.dom, + domProps: this.props, + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this) }, - content: { - height: 0, - marginLeft: 0 + 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) } }; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data); - } + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; + + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); + + // custom time bar + // Note: time bar will be attached in this.setOptions when selected + this.customTime = new CustomTime(this.body); + this.components.push(this.customTime); + + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // apply options + if (options) { + this.setOptions(options); } - Item.call(this, data, conversion, options); + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } + + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } } - PointItem.prototype = new Item(null, null, null); + // Extend the functionality from Core + Timeline.prototype = new Core(); /** - * 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 + * 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. */ - 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; + Timeline.prototype.redraw = function () { + this.itemSet && this.itemSet.markDirty({ refreshItems: true }); + this._redraw(); }; /** - * Repaint the item + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items */ - PointItem.prototype.redraw = function () { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; - - // background box - dom.point = document.createElement("div"); - // className is updated in redraw() - - // contents box, right from the dot - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.point.appendChild(dom.content); - - // dot at start - dom.dot = document.createElement("div"); - dom.point.appendChild(dom.dot); - - // attach this item as attribute - dom.point["timeline-item"] = this; - - this.dirty = true; - } + Timeline.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; - // 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); + // 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.displayed = true; - - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.point); - this._updateDataAttributes(this.dom.point); - this._updateStyle(this.dom.point); - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.point.className = "item point" + className; - dom.dot.className = "item dot" + className; - - // recalculate size - this.width = dom.point.offsetWidth; - this.height = dom.point.offsetHeight; - this.props.dot.width = dom.dot.offsetWidth; - this.props.dot.height = dom.dot.offsetHeight; - this.props.content.height = dom.content.offsetHeight; + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + "px"; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + if (this.options.start == undefined || this.options.end == undefined) { + var dataRange = this._getDataRange(); + } - dom.dot.style.top = (this.height - this.props.dot.height) / 2 + "px"; - dom.dot.style.left = this.props.dot.width / 2 + "px"; + var start = this.options.start != undefined ? this.options.start : dataRange.start; + var end = this.options.end != undefined ? this.options.end : dataRange.end; - this.dirty = false; + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); + } } - - this._repaintDeleteButton(dom.point); }; /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ - PointItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); + 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); }; /** - * Hide the item from the DOM (when visible) + * 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. */ - 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; + Timeline.prototype.setSelection = function (ids, options) { + this.itemSet && this.itemSet.setSelection(ids); - this.displayed = false; + if (options && options.focus) { + this.focus(ids, options); } }; /** - * Reposition the item horizontally - * @Override + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ - 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"; + Timeline.prototype.getSelection = function () { + return this.itemSet && this.itemSet.getSelection() || []; }; /** - * Reposition the item vertically - * @Override + * 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 */ - PointItem.prototype.repositionY = function () { - var orientation = this.options.orientation, - point = this.dom.point; + Timeline.prototype.focus = function (id, options) { + if (!this.itemsData || id == undefined) return; - if (orientation == "top") { - point.style.top = this.top + "px"; - } else { - point.style.top = this.parent.height - this.top - this.height + "px"; - } - }; + var ids = Array.isArray(id) ? id : [id]; - module.exports = PointItem; + // get the specified item(s) + var itemsData = this.itemsData.getDataSet().get(ids, { + type: { + start: "Date", + end: "Date" + } + }); -/***/ }, -/* 24 */ -/***/ function(module, exports, __webpack_require__) { + // 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(); - "use strict"; + if (start === null || s < start) { + start = s; + } + + if (end === null || e > end) { + end = e; + } + }); - var Hammer = __webpack_require__(41); - var Item = __webpack_require__(20); + 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); + } + }; /** - * @constructor RangeItem - * @extends Item - * @param {Object} data Object containing parameters start, end - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe options + * Get the data range of the item set. + * @returns {{min: Date, max: Date}} range A range with a start and end Date. + * When no minimum is found, min==null + * When no maximum is found, max==null */ - function RangeItem(data, conversion, options) { - this.props = { - content: { - width: 0 - } - }; - this.overflow = false; // if contents can overflow (css styling), this flag is set to true + Timeline.prototype.getItemRange = function () { + // calculate min from start filed + var dataset = this.itemsData.getDataSet(), + min = null, + max = null; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error("Property \"start\" missing in item " + data.id); + 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(); } - if (data.end == undefined) { - throw new Error("Property \"end\" missing in item " + data.id); + 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()); + } } } - Item.call(this, data, conversion, options); - } - - RangeItem.prototype = new Item(null, null, null); - - RangeItem.prototype.baseClassName = "item range"; - - /** - * Check whether this item is visible inside given range - * @returns {{start: Number, end: Number}} range with a timestamp for start and end - * @returns {boolean} True if visible - */ - RangeItem.prototype.isVisible = function (range) { - // determine visibility - return this.data.start < range.end && this.data.end > range.start; + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; }; - /** - * Repaint the item - */ - 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() + module.exports = Timeline; - // contents box - dom.content = document.createElement("div"); - dom.content.className = "content"; - dom.box.appendChild(dom.content); +/***/ }, +/* 19 */ +/***/ function(module, exports, __webpack_require__) { - // attach this item as attribute - dom.box["timeline-item"] = this; + "use strict"; - this.dirty = true; - } + // 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."); + }; + } - // 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; +/***/ }, +/* 20 */ +/***/ function(module, exports, __webpack_require__) { - // Update DOM when item is marked dirty. An item is marked dirty when: - // - the item is not yet rendered - // - the item's data is changed - // - the item is selected/deselected - if (this.dirty) { - this._updateContents(this.dom.content); - this._updateTitle(this.dom.box); - this._updateDataAttributes(this.dom.box); - this._updateStyle(this.dom.box); + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; - // update class - var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); - dom.box.className = this.baseClassName + className; + (function (factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + window.propagating = factory(); + } + }(function () { + // will contain the target element where the gesture started + var _firstTarget = null; // singleton - // determine from css whether this box has overflow - this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; + /** + * 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; - // 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 = ""; + 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)); + }; - this.dirty = false; - } + return PropagatingHammer; + } - this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); - }; + // attach to DOM element + var element = hammer.element; + element.hammer = hammer; - /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - */ - RangeItem.prototype.show = function () { - if (!this.displayed) { - this.redraw(); - } - }; + // move the original functions that we will wrap + hammer._on = hammer.on; + hammer._off = hammer.off; + hammer._emit = hammer.emit; + hammer._destroy = hammer.destroy; - /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed - */ - RangeItem.prototype.hide = function () { - if (this.displayed) { - var box = this.dom.box; + /** @type {Object.>} */ + hammer._handlers = {}; - if (box.parentNode) { - box.parentNode.removeChild(box); - } + // 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; + } + }); - this.top = null; - this.left = 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 = []; - this.displayed = false; - } - }; + // register the static, propagated handler + hammer._on(event, propagatedHandler); + } + _handlers.push(handler); + }); - /** - * 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; + return hammer; + }; - // 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); + /** + * 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 (this.overflow) { - this.left = start; - this.width = boxWidth + this.props.content.width; - contentWidth = this.props.content.width; + if (_handlers.length > 0) { + hammer._handlers[event] = _handlers; + } + else { + // remove static, propagated handler + hammer._off(event, propagatedHandler); + delete hammer._handlers[event]; + } + } + }); - // 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); - } + return hammer; + }; - this.dom.box.style.left = this.left + "px"; - this.dom.box.style.width = boxWidth + "px"; + /** + * Emit to the event listeners + * @param {string} eventType + * @param {Event} event + */ + hammer.emit = function(eventType, event) { + _firstTarget = event.target; + hammer._emit(eventType, event); + }; - switch (this.options.align) { - case "left": - this.dom.content.style.left = "0"; - break; + hammer.destroy = function () { + // Detach from DOM element + var element = hammer.element; + delete element.hammer; - case "right": - this.dom.content.style.left = Math.max(boxWidth - contentWidth - 2 * this.options.padding, 0) + "px"; - break; + // clear all handlers + hammer._handlers = {}; - case "center": - this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + "px"; - break; + // call original hammer destroy + hammer._destroy(); + }; - 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 + // split a string with space separated words + function split(events) { + return events.match(/[^ ]+/g); + } + + /** + * A static event handler, applying event propagation. + * @param {Object} event + */ + function propagatedHandler(event) { + // let only a single hammer instance handle this event + if (event.type !== 'hammer.input') { + if (event.srcEvent._handled && event.srcEvent._handled[event.type]) { + return; } - } else { - 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; + else { + // it is possible that the same srcEvent is used with multiple hammer events + event.srcEvent._handled = {}; + event.srcEvent._handled[event.type] = true; } } - this.dom.content.style.left = contentLeft + "px"; - } - }; - - /** - * Reposition the item vertically - * @Override - */ - 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"; - } - }; + // attach a stopPropagation function to the event + var stopped = false; + event.stopPropagation = function () { + stopped = true; + }; - /** - * 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; + // attach firstTarget property to the event + event.firstTarget = _firstTarget; - //// TODO: this should be redundant? - //Hammer(dragLeft, { - // preventDefault: true - //}).on('drag', function () { - // //console.log('drag left') - // }); + // 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.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); + elem = elem.parentNode; + } } - this.dom.dragLeft = null; - } - }; - - /** - * 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; - - //// 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; - } - }; + return hammer; + }; + })); - module.exports = RangeItem; /***/ }, -/* 25 */ +/* 21 */ /***/ function(module, exports, __webpack_require__) { - "use strict"; + var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 + * http://hammerjs.github.io/ + * + * Copyright (c) 2014 Jorik Tangelder; + * Licensed under the MIT license */ + (function(window, document, exportName, undefined) { + 'use strict'; + + var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; + var TEST_ELEMENT = document.createElement('div'); + + var TYPE_FUNCTION = 'function'; + + var round = Math.round; + var abs = Math.abs; + var now = Date.now; /** - * Prototype for visual components - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] - * @param {Object} [options] + * set a timeout with a given scope + * @param {Function} fn + * @param {Number} timeout + * @param {Object} context + * @returns {number} */ - function Component(body, options) { - this.options = null; - this.props = null; + function setTimeoutContext(fn, timeout, context) { + return setTimeout(bindFn(fn, context), timeout); } /** - * Set options for the component. The new options will be merged into the - * current options. - * @param {Object} options + * 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} */ - Component.prototype.setOptions = function (options) { - if (options) { - util.extend(this.options, options); - } - }; + function invokeArrayArg(arg, fn, context) { + if (Array.isArray(arg)) { + each(arg, context[fn], context); + return true; + } + return false; + } /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * walk objects and arrays + * @param {Object} obj + * @param {Function} iterator + * @param {Object} context */ - Component.prototype.redraw = function () { - // should be implemented by the component - return false; - }; + function each(obj, iterator, context) { + var i; + + if (!obj) { + return; + } + + if (obj.forEach) { + obj.forEach(iterator, context); + } else if (obj.length !== undefined) { + i = 0; + while (i < obj.length) { + iterator.call(context, obj[i], i, obj); + i++; + } + } else { + for (i in obj) { + obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj); + } + } + } /** - * Destroy the component. Cleanup DOM and event listeners + * 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 */ - Component.prototype.destroy = function () {}; + 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; + } /** - * Test whether the component is resized since the last time _isResized() was - * called. - * @return {Boolean} Returns true if the component is resized - * @protected + * 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 */ - Component.prototype._isResized = function () { - var resized = this.props._previousWidth !== this.props.width || this.props._previousHeight !== this.props.height; - - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; - - return resized; - }; - - module.exports = Component; - // should be implemented by the component - -/***/ }, -/* 26 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var moment = __webpack_require__(40); - var locales = __webpack_require__(44); + function merge(dest, src) { + return extend(dest, src, true); + } /** - * A current time bar - * @param {{range: Range, dom: Object, domProps: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCurrentTime] - * @constructor CurrentTime - * @extends Component + * simple class inheritance + * @param {Function} child + * @param {Function} base + * @param {Object} [properties] */ - function CurrentTime(body, options) { - this.body = body; - - // default options - this.defaultOptions = { - showCurrentTime: true, - - locales: locales, - locale: "en" - }; - this.options = util.extend({}, this.defaultOptions); - this.offset = 0; + function inherit(child, base, properties) { + var baseP = base.prototype, + childP; - this._create(); + childP = child.prototype = Object.create(baseP); + childP.constructor = child; + childP._super = baseP; - this.setOptions(options); + if (properties) { + extend(childP, properties); + } } - CurrentTime.prototype = new Component(); - /** - * Create the HTML DOM for the current time bar - * @private + * simple function bind + * @param {Function} fn + * @param {Object} context + * @returns {Function} */ - 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 bindFn(fn, context) { + return function boundFn() { + return fn.apply(context, arguments); + }; + } /** - * Destroy the CurrentTime 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.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing - - this.body = null; - }; + function boolOrFn(val, args) { + if (typeof val == TYPE_FUNCTION) { + return val.apply(args ? args[0] || undefined : undefined, args); + } + return val; + } /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] + * use the val2 when val1 is undefined + * @param {*} val1 + * @param {*} val2 + * @returns {*} */ - CurrentTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(["showCurrentTime", "locale", "locales"], this.options, options); - } - }; + function ifUndefined(val1, val2) { + return (val1 === undefined) ? val2 : val1; + } /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * addEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - CurrentTime.prototype.redraw = function () { - if (this.options.showCurrentTime) { - var parent = this.body.dom.backgroundVertical; - if (this.bar.parentNode != parent) { - // attach to the dom - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - parent.appendChild(this.bar); - - this.start(); - } - - var now = new Date(new Date().valueOf() + this.offset); - var x = this.body.util.toScreen(now); - - var locale = this.options.locales[this.options.locale]; - var title = locale.current + " " + locale.time + ": " + moment(now).format("dddd, MMMM Do YYYY, H:mm:ss"); - title = title.charAt(0).toUpperCase() + title.substring(1); - - this.bar.style.left = x + "px"; - this.bar.title = title; - } else { - // remove the line from the DOM - if (this.bar.parentNode) { - this.bar.parentNode.removeChild(this.bar); - } - this.stop(); - } - - return false; - }; + function addEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.addEventListener(type, handler, false); + }); + } /** - * Start auto refreshing the current time bar + * removeEventListener with multiple events at once + * @param {EventTarget} target + * @param {String} types + * @param {Function} handler */ - 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 removeEventListeners(target, types, handler) { + each(splitStr(types), function(type) { + target.removeEventListener(type, handler, false); + }); + } /** - * Stop auto refreshing the current time bar + * find if a node is in the given parent + * @method hasParent + * @param {HTMLElement} node + * @param {HTMLElement} parent + * @return {Boolean} found */ - CurrentTime.prototype.stop = function () { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; - } - }; + function hasParent(node, parent) { + while (node) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; + } /** - * 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. + * small indexOf wrapper + * @param {String} str + * @param {String} find + * @returns {Boolean} found */ - CurrentTime.prototype.setCurrentTime = function (time) { - var t = util.convert(time, "Date").valueOf(); - var now = new Date().valueOf(); - this.offset = t - now; - this.redraw(); - }; + function inStr(str, find) { + return str.indexOf(find) > -1; + } /** - * Get the current time. - * @return {Date} Returns the current time. + * split string on whitespace + * @param {String} str + * @returns {Array} words */ - 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__(41); - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var moment = __webpack_require__(40); - var locales = __webpack_require__(44); + function splitStr(str) { + return str.trim().split(/\s+/g); + } /** - * A custom time bar - * @param {{range: Range, dom: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCustomTime] - * @constructor CustomTime - * @extends Component + * find if a array contains the object using indexOf or a simple polyFill + * @param {Array} src + * @param {String} find + * @param {String} [findByKey] + * @return {Boolean|Number} false when not found, or the index */ + function inArray(src, find, findByKey) { + if (src.indexOf && !findByKey) { + return src.indexOf(find); + } else { + var i = 0; + while (i < src.length) { + if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { + return i; + } + i++; + } + return -1; + } + } - 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); + /** + * convert array-like objects to real arrays + * @param {Object} obj + * @returns {Array} + */ + function toArray(obj) { + return Array.prototype.slice.call(obj, 0); + } - if (options && options.time) { - this.customTime = options.time; - } else { - this.customTime = new Date(); - } + /** + * 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; - this.eventParams = {}; // stores state parameters while dragging the bar + 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++; + } - // create the DOM - this._create(); + if (sort) { + if (!key) { + results = results.sort(); + } else { + results = results.sort(function sortUniqueArray(a, b) { + return a[key] > b[key]; + }); + } + } - this.setOptions(options); + return results; } - CustomTime.prototype = new Component(); - /** - * Set options for the component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCustomTime] + * get the prefixed property + * @param {Object} obj + * @param {String} property + * @returns {String|Undefined} prefixed */ - CustomTime.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(["showCustomTime", "locale", "locales", "id"], this.options, options); + function prefixed(obj, property) { + var prefix, prop; + var camelProp = property[0].toUpperCase() + property.slice(1); - // Triggered by addCustomTimeBar, redraw to add new bar - if (this.options.id) { - this.redraw(); + 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; + } /** - * Create the DOM for the custom time - * @private + * get a unique id + * @returns {number} uniqueId */ - CustomTime.prototype._create = function () { - var bar = document.createElement("div"); - bar.className = "customtime"; - bar.style.position = "absolute"; - bar.style.top = "0px"; - bar.style.height = "100%"; - this.bar = bar; - - var drag = document.createElement("div"); - drag.style.position = "relative"; - drag.style.top = "0px"; - drag.style.left = "-10px"; - drag.style.height = "100%"; - drag.style.width = "20px"; - bar.appendChild(drag); - - // attach event listeners - this.hammer = new Hammer(drag); - this.hammer.on("panstart", this._onDragStart.bind(this)); - this.hammer.on("panmove", this._onDrag.bind(this)); - this.hammer.on("panend", this._onDragEnd.bind(this)); - this.hammer.on("pan", function (event) { - event.preventDefault(); - }); - }; + var _uniqueId = 1; + function uniqueId() { + return _uniqueId++; + } /** - * Destroy the CustomTime bar + * get the window object of an element + * @param {HTMLElement} element + * @returns {DocumentView|Window} */ - CustomTime.prototype.destroy = function () { - this.options.showCustomTime = false; - this.redraw(); // will remove the bar from the DOM - - this.hammer.enable(false); - this.hammer = null; + function getWindowForElement(element) { + var doc = element.ownerDocument; + return (doc.defaultView || doc.parentWindow); + } - this.body = null; - }; + var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; - /** - * 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); - } + 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.customTime); + var INPUT_TYPE_TOUCH = 'touch'; + var INPUT_TYPE_PEN = 'pen'; + var INPUT_TYPE_MOUSE = 'mouse'; + var INPUT_TYPE_KINECT = 'kinect'; - 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); + var COMPUTE_INTERVAL = 25; - 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); - } - } + var INPUT_START = 1; + var INPUT_MOVE = 2; + var INPUT_END = 4; + var INPUT_CANCEL = 8; - return false; - }; + var DIRECTION_NONE = 1; + var DIRECTION_LEFT = 2; + var DIRECTION_RIGHT = 4; + var DIRECTION_UP = 8; + var DIRECTION_DOWN = 16; - /** - * Set custom time. - * @param {Date | number | string} time - */ - CustomTime.prototype.setCustomTime = function (time) { - this.customTime = util.convert(time, "Date"); - this.redraw(); - }; + var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; + var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; + var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; - /** - * Retrieve the current custom time. - * @return {Date} customTime - */ - CustomTime.prototype.getCustomTime = function () { - return new Date(this.customTime.valueOf()); - }; + var PROPS_XY = ['x', 'y']; + var PROPS_CLIENT_XY = ['clientX', 'clientY']; /** - * Start moving horizontally - * @param {Event} event - * @private + * create new input type manager + * @param {Manager} manager + * @param {Function} callback + * @returns {Input} + * @constructor */ - CustomTime.prototype._onDragStart = function (event) { - this.eventParams.dragging = true; - this.eventParams.customTime = this.customTime; + function Input(manager, callback) { + var self = this; + this.manager = manager; + this.callback = callback; + this.element = manager.element; + this.target = manager.options.inputTarget; - event.stopPropagation(); - event.preventDefault(); - }; + // 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); + } + }; - /** - * Perform moving operating. - * @param {Event} event - * @private - */ - CustomTime.prototype._onDrag = function (event) { - if (!this.eventParams.dragging) return; + this.init(); - var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; - var time = this.body.util.toTime(x); + } - this.setCustomTime(time); + Input.prototype = { + /** + * should handle the inputEvent data and trigger the callback + * @virtual + */ + handler: function() { }, - // fire a timechange event - this.body.emitter.emit("timechange", { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + /** + * 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); + }, - event.stopPropagation(); - event.preventDefault(); + /** + * 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); + } }; /** - * Stop moving operating. - * @param {Event} event - * @private + * create new input type manager + * called by the Manager constructor + * @param {Hammer} manager + * @returns {Input} */ - CustomTime.prototype._onDragEnd = function (event) { - if (!this.eventParams.dragging) return; + function createInputInstance(manager) { + var Type; + var inputClass = manager.options.inputClass; - // fire a timechanged event - this.body.emitter.emit("timechanged", { - id: this.options.id, - time: new Date(this.customTime.valueOf()) - }); + if (inputClass) { + Type = inputClass; + } else if (SUPPORT_POINTER_EVENTS) { + Type = PointerEventInput; + } else if (SUPPORT_ONLY_TOUCH) { + Type = TouchInput; + } else if (!SUPPORT_TOUCH) { + Type = MouseInput; + } else { + Type = TouchMouseInput; + } + return new (Type)(manager, inputHandler); + } - event.stopPropagation(); - event.preventDefault(); - }; + /** + * 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)); - module.exports = CustomTime; + input.isFirst = !!isFirst; + input.isFinal = !!isFinal; -/***/ }, -/* 28 */ -/***/ function(module, exports, __webpack_require__) { + if (isFirst) { + manager.session = {}; + } - "use strict"; + // source event is the normalized value of the domEvents + // like 'touchstart, mouseup, pointerdown' + input.eventType = eventType; - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var Component = __webpack_require__(25); - var DataStep = __webpack_require__(16); + // compute scale, rotation etc + computeInputData(manager, input); + + // emit secret event + manager.emit('hammer.input', input); + + manager.recognize(input); + manager.session.prevInput = input; + } /** - * A horizontal time axis - * @param {Object} [options] See DataAxis.setOptions for the available - * options. - * @constructor DataAxis - * @extends Component - * @param body + * extend the data with some usable properties like scale, rotate, velocity etc + * @param {Object} manager + * @param {Object} input */ - function DataAxis(body, options, svg, linegraphOptions) { - this.id = util.randomUUID(); - this.body = body; + function computeInputData(manager, input) { + var session = manager.session; + var pointers = input.pointers; + var pointersLength = pointers.length; - 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 } + // store the first input to calculate the distance and direction + if (!session.firstInput) { + session.firstInput = simpleCloneInputData(input); } - }; - - this.linegraphOptions = linegraphOptions; - this.linegraphSVG = svg; - this.props = {}; - this.DOMelements = { // dynamic elements - lines: {}, - labels: {}, - title: {} - }; - - this.dom = {}; - - this.range = { start: 0, end: 0 }; - this.options = util.extend({}, this.defaultOptions); - this.conversionFactor = 1; + // 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; + } - this.setOptions(options); - this.width = Number(("" + this.options.width).replace("px", "")); - this.minWidth = this.width; - this.height = this.linegraphSVG.offsetHeight; - this.hidden = false; + var firstInput = session.firstInput; + var firstMultiple = session.firstMultiple; + var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - this.stepPixels = 25; - this.stepPixelsForced = 25; - this.zeroCrossing = -1; + var center = input.center = getCenter(pointers); + input.timeStamp = now(); + input.deltaTime = input.timeStamp - firstInput.timeStamp; - this.lineOffset = 0; - this.master = true; - this.svgElements = {}; - this.iconsRemoved = false; + input.angle = getAngle(offsetCenter, center); + input.distance = getDistance(offsetCenter, center); + computeDeltaXY(session, input); + input.offsetDirection = getDirection(input.deltaX, input.deltaY); - this.groups = {}; - this.amountOfGroups = 0; + input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; + input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; - // create the HTML DOM - this._create(); + computeIntervalInputData(session, input); - var me = this; - this.body.emitter.on("verticalDrag", function () { - me.dom.lineContainer.style.top = me.body.domProps.scrollTop + "px"; - }); + // find the correct target + var target = manager.element; + if (hasParent(input.srcEvent.target, target)) { + target = input.srcEvent.target; + } + input.target = target; } - DataAxis.prototype = new Component(); - + function computeDeltaXY(session, input) { + var center = input.center; + var offset = session.offsetDelta || {}; + var prevDelta = session.prevDelta || {}; + var prevInput = session.prevInput || {}; - DataAxis.prototype.addGroup = function (label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; - } - this.amountOfGroups += 1; - }; + if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { + prevDelta = session.prevDelta = { + x: prevInput.deltaX || 0, + y: prevInput.deltaY || 0 + }; - DataAxis.prototype.updateGroup = function (label, graphOptions) { - this.groups[label] = graphOptions; - }; + offset = session.offsetDelta = { + x: center.x, + y: center.y + }; + } - DataAxis.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; - } - }; + input.deltaX = prevDelta.x + (center.x - offset.x); + input.deltaY = prevDelta.y + (center.y - offset.y); + } + /** + * velocity is calculated every x ms + * @param {Object} session + * @param {Object} input + */ + function computeIntervalInputData(session, input) { + var last = session.lastInterval || input, + deltaTime = input.timeStamp - last.timeStamp, + velocity, velocityX, velocityY, direction; - 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); + if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { + var deltaX = last.deltaX - input.deltaX; + var deltaY = last.deltaY - input.deltaY; - this.minWidth = Number(("" + this.options.width).replace("px", "")); + 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); - if (redraw == true && this.dom.frame) { - this.hide(); - this.show(); + 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 the HTML DOM for the DataAxis + * create a simple clone from the input used for storage of firstInput and firstMultiple + * @param {Object} input + * @returns {Object} clonedInputData */ - 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); - }; + 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; - - var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation], this.master == false && this.options.alignZeros // doess the step have to align zeros? only if not master and the options is on - ); - - this.step = step; - // get the distance in pixels for a step - // dead space is space that is "left over" after a step - var stepPixels = (this.dom.frame.offsetHeight - step.deadSpace * (this.dom.frame.offsetHeight / step.marginRange)) / ((step.marginRange - step.deadSpace) / step.step); - - this.stepPixels = stepPixels; - - var amountOfSteps = this.height / stepPixels; - var stepDifference = 0; + // 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 + }; - // 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; + var POINTER_ELEMENT_EVENTS = 'pointerdown'; + var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; - 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; - } + // IE10 has prefixed support, and case-sensitive + if (window.MSPointerEvent) { + POINTER_ELEMENT_EVENTS = 'MSPointerDown'; + POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; + } + /** + * Pointer events input + * @constructor + * @extends Input + */ + function PointerEventInput() { + this.evEl = POINTER_ELEMENT_EVENTS; + this.evWin = POINTER_WINDOW_EVENTS; - this.valueAtZero = step.marginEnd; - var marginStartPos = 0; + Input.apply(this, arguments); - // do not draw the first label - var max = 1; + this.store = (this.manager.session.pointerEvents = []); + } - // Get the number of decimal places - var decimals; - if (this.options.format[orientation] !== undefined) { - decimals = this.options.format[orientation].decimals; - } + inherit(PointerEventInput, Input, { + /** + * handle mouse events + * @param {Object} ev + */ + handler: function PEhandler(ev) { + var store = this.store; + var removePointer = false; - 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(); + var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); + var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; + var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; - 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); - } + var isTouch = (pointerType == INPUT_TYPE_TOUCH); - 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); - } + // get index of the event in the store + var storeIndex = inArray(store, ev.pointerId, 'pointerId'); - if (this.master == true && step.current == 0) { - this.zeroCrossing = max; - } + // 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; + } - max++; - } + // it not found, so the pointer hasn't been down (so it's probably a hover) + if (storeIndex < 0) { + return; + } - if (this.master == false) { - this.conversionFactor = y / (this.valueAtZero - step.current); - } else { - this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; - } + // update the event in the store + store[storeIndex] = ev; - // 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; + this.callback(this.manager, eventType, { + pointers: store, + changedPointers: [ev], + pointerType: pointerType, + srcEvent: ev + }); - // 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; - } + if (removePointer) { + // remove from the store + store.splice(storeIndex, 1); + } + } + }); - return resized; + var SINGLE_TOUCH_INPUT_MAP = { + touchstart: INPUT_START, + touchmove: INPUT_MOVE, + touchend: INPUT_END, + touchcancel: INPUT_CANCEL }; - DataAxis.prototype.convertValue = function (value) { - var invertedValue = this.valueAtZero - value; - var convertedValue = invertedValue * this.conversionFactor; - return convertedValue; - }; + var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; + var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; /** - * Create a label for the axis at position x - * @private - * @param y - * @param text - * @param orientation - * @param className - * @param characterHeight + * Touch events input + * @constructor + * @extends Input */ - 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"; - } + function SingleTouchInput() { + this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; + this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; + this.started = false; - label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + "px"; + Input.apply(this, arguments); + } - text += ""; + inherit(SingleTouchInput, Input, { + handler: function TEhandler(ev) { + var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; - var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); - if (this.maxLabelSize < text.length * largestWidth) { - this.maxLabelSize = text.length * largestWidth; - } - }; + // should we handle the touch events? + if (type === INPUT_START) { + this.started = true; + } + + if (!this.started) { + return; + } + + var touches = normalizeSingleTouches.call(this, ev, type); + + // when done, reset the started state + if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { + this.started = false; + } + + this.callback(this.manager, type, { + pointers: touches[0], + changedPointers: touches[1], + pointerType: INPUT_TYPE_TOUCH, + srcEvent: ev + }); + } + }); /** - * 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__(45); - var Bar = __webpack_require__(46); - var Points = __webpack_require__(47); + // 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(); + } - /** - * 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 (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 is used for plotting barcharts, this way, we only have to calculate it once. - * @param pos - */ - GraphGroup.prototype.setZeroPosition = function (pos) { - this.zeroPosition = pos; + /** + * 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(); + } }; - /** - * set the options of the graph group over the default options. - * @param options + * when the touchActions are collected they are not a valid value, so we need to clean things up. * + * @param {String} actions + * @returns {*} */ - GraphGroup.prototype.setOptions = function (options) { - if (options !== undefined) { - var fields = ["sampling", "style", "sort", "yAxisOrientation", "barChart"]; - util.selectiveDeepExtend(fields, this.options, options); + function cleanTouchActions(actions) { + // none + if (inStr(actions, TOUCH_ACTION_NONE)) { + return TOUCH_ACTION_NONE; + } - util.mergeOptions(this.options, options, "catmullRom"); - util.mergeOptions(this.options, options, "drawPoints"); - util.mergeOptions(this.options, options, "shaded"); + var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); + var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); - 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; - } - } - } + // pan-x and pan-y can be combined + if (hasPanX && hasPanY) { + return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; } - } - 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); - } - }; + // pan-x OR pan-y + if (hasPanX || hasPanY) { + return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; + } + + // manipulation + if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { + return TOUCH_ACTION_MANIPULATION; + } + return TOUCH_ACTION_AUTO; + } /** - * this updates the current group class with the latest group dataset entree, used in _updateGroup in linegraph - * @param group + * 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.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); - }; - + 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; /** - * draw the icon for the legend. - * - * @param x - * @param y - * @param JSONcontainer - * @param SVGcontainer - * @param iconWidth - * @param iconHeight + * Recognizer + * Every recognizer needs to extend from this class. + * @constructor + * @param {Object} options */ - GraphGroup.prototype.drawIcon = function (x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) { - var fillHeight = iconHeight * 0.5; - var path, fillPath; + function Recognizer(options) { + this.id = uniqueId(); - var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer); - outline.setAttributeNS(null, "x", x); - outline.setAttributeNS(null, "y", y - fillHeight); - outline.setAttributeNS(null, "width", iconWidth); - outline.setAttributeNS(null, "height", 2 * fillHeight); - outline.setAttributeNS(null, "class", "outline"); + this.manager = null; + this.options = merge(options || {}, this.defaults); - 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); - } + // default is enable true + this.options.enable = ifUndefined(this.options.enable, true); - path.setAttributeNS(null, "d", "M" + x + "," + y + " L" + (x + iconWidth) + "," + y + ""); - if (this.options.shaded.enabled == true) { - fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer); - if (this.options.shaded.orientation == "top") { - fillPath.setAttributeNS(null, "d", "M" + x + ", " + (y - fillHeight) + "L" + x + "," + y + " L" + (x + iconWidth) + "," + y + " L" + (x + iconWidth) + "," + (y - fillHeight)); - } else { - fillPath.setAttributeNS(null, "d", "M" + x + "," + y + " " + "L" + x + "," + (y + fillHeight) + " " + "L" + (x + iconWidth) + "," + (y + fillHeight) + "L" + (x + iconWidth) + "," + y); - } - fillPath.setAttributeNS(null, "class", this.className + " iconFill"); - } + this.state = STATE_POSSIBLE; - 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); + this.simultaneous = {}; + this.requireFail = []; + } - var offset = Math.round((iconWidth - 2 * barWidth) / 3); + Recognizer.prototype = { + /** + * @virtual + * @type {Object} + */ + defaults: {}, - 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); - } - }; + /** + * 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; + }, - /** - * 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 }; - }; + /** + * recognize simultaneous with an other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + recognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { + return this; + } - GraphGroup.prototype.getYRange = function (groupData) { - return this.type.getYRange(groupData); - }; + var simultaneous = this.simultaneous; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (!simultaneous[otherRecognizer.id]) { + simultaneous[otherRecognizer.id] = otherRecognizer; + otherRecognizer.recognizeWith(this); + } + return this; + }, - GraphGroup.prototype.draw = function (dataset, group, framework) { - this.type.draw(dataset, group, framework); - }; + /** + * drop the simultaneous link. it doesnt remove the link on the other recognizer. + * @param {Recognizer} otherRecognizer + * @returns {Recognizer} this + */ + dropRecognizeWith: function(otherRecognizer) { + if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) { + return this; + } + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + delete this.simultaneous[otherRecognizer.id]; + return this; + }, - module.exports = GraphGroup; + /** + * 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; + } -/***/ }, -/* 30 */ -/***/ function(module, exports, __webpack_require__) { + var requireFail = this.requireFail; + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + if (inArray(requireFail, otherRecognizer) === -1) { + requireFail.push(otherRecognizer); + otherRecognizer.requireFailure(this); + } + return this; + }, - "use strict"; + /** + * 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; + } - var util = __webpack_require__(1); - var stack = __webpack_require__(18); - var RangeItem = __webpack_require__(24); + otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); + var index = inArray(this.requireFail, otherRecognizer); + if (index > -1) { + this.requireFail.splice(index, 1); + } + return this; + }, - /** - * @constructor Group - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet - */ - function Group(groupId, data, itemSet) { - this.groupId = groupId; - this.subgroups = {}; - this.subgroupIndex = 0; - this.subgroupOrderer = data && data.subgroupOrder; - this.itemSet = itemSet; + /** + * has require failures boolean + * @returns {boolean} + */ + hasRequireFailures: function() { + return this.requireFail.length > 0; + }, - this.dom = {}; - this.props = { - label: { - width: 0, - height: 0 - } - }; - this.className = null; + /** + * if the recognizer can recognize simultaneous with an other recognizer + * @param {Recognizer} otherRecognizer + * @returns {Boolean} + */ + canRecognizeWith: function(otherRecognizer) { + return !!this.simultaneous[otherRecognizer.id]; + }, - 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; - }); + /** + * 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; - this._create(); + function emit(withState) { + self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); + } - this.setData(data); - } + // 'panstart' and 'panmove' + if (state < STATE_ENDED) { + emit(true); + } - /** - * Create DOM elements for the group - * @private - */ - Group.prototype._create = function () { - var label = document.createElement("div"); - label.className = "vlabel"; - this.dom.label = label; + emit(); // simple 'eventName' events - var inner = document.createElement("div"); - inner.className = "inner"; - label.appendChild(inner); - this.dom.inner = inner; + // panend and pancancel + if (state >= STATE_ENDED) { + emit(true); + } + }, - var foreground = document.createElement("div"); - foreground.className = "group"; - foreground["timeline-group"] = this; - this.dom.foreground = foreground; + /** + * 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; + }, - this.dom.background = document.createElement("div"); - this.dom.background.className = "group"; + /** + * 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; + }, - this.dom.axis = document.createElement("div"); - this.dom.axis.className = "group"; + /** + * 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); - // 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); - }; + // is is enabled and allow recognizing? + if (!boolOrFn(this.options.enable, [this, inputDataClone])) { + this.reset(); + this.state = STATE_FAILED; + return; + } - /** - * Set the group data for this group - * @param {Object} data Group data, can contain properties content and className - */ - Group.prototype.setData = function (data) { - // update contents - var content = data && data.content; - if (content instanceof Element) { - this.dom.inner.appendChild(content); - } else if (content !== undefined && content !== null) { - this.dom.inner.innerHTML = content; - } else { - this.dom.inner.innerHTML = this.groupId || ""; // groupId can be null - } + // reset when we've reached the end + if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { + this.state = STATE_POSSIBLE; + } - // update title - this.dom.label.title = data && data.title || ""; + this.state = this.process(inputDataClone); - if (!this.dom.inner.firstChild) { - util.addClassName(this.dom.inner, "hidden"); - } else { - util.removeClassName(this.dom.inner, "hidden"); - } + // the recognizer has recognized a gesture + // so trigger an event + if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { + this.tryEmit(inputDataClone); + } + }, - // update className - var className = data && data.className || null; - if (className != this.className) { - if (this.className) { - util.removeClassName(this.dom.label, this.className); - util.removeClassName(this.dom.foreground, this.className); - util.removeClassName(this.dom.background, this.className); - util.removeClassName(this.dom.axis, this.className); - } - util.addClassName(this.dom.label, className); - util.addClassName(this.dom.foreground, className); - util.addClassName(this.dom.background, className); - util.addClassName(this.dom.axis, className); - this.className = className; - } + /** + * 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 - // 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 the preferred touch-action + * @virtual + * @returns {Array} + */ + getTouchAction: function() { }, + + /** + * called when the gesture isn't allowed to recognize + * like when another is being recognized or it is disabled + * @virtual + */ + reset: function() { } }; /** - * Get the width of the group label - * @return {number} width + * get a usable string, used as event postfix + * @param {Const} state + * @returns {String} state + */ + function stateStr(state) { + if (state & STATE_CANCELLED) { + return 'cancel'; + } else if (state & STATE_ENDED) { + return 'end'; + } else if (state & STATE_CHANGED) { + return 'move'; + } else if (state & STATE_BEGAN) { + return 'start'; + } + return ''; + } + + /** + * direction cons to string + * @param {Const} direction + * @returns {String} + */ + function directionStr(direction) { + if (direction == DIRECTION_DOWN) { + return 'down'; + } else if (direction == DIRECTION_UP) { + return 'up'; + } else if (direction == DIRECTION_LEFT) { + return 'left'; + } else if (direction == DIRECTION_RIGHT) { + return 'right'; + } + return ''; + } + + /** + * get a recognizer by name if it is bound to a manager + * @param {Recognizer|String} otherRecognizer + * @param {Recognizer} recognizer + * @returns {Recognizer} */ - Group.prototype.getLabelWidth = function () { - return this.props.label.width; - }; - + function getRecognizerByNameIfManager(otherRecognizer, recognizer) { + var manager = recognizer.manager; + if (manager) { + return manager.get(otherRecognizer); + } + return otherRecognizer; + } /** - * 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 + * This recognizer is just used as a base for the simple attribute recognizers. + * @constructor + * @extends Recognizer */ - Group.prototype.redraw = function (range, margin, restack) { - var resized = false; + function AttrRecognizer() { + Recognizer.apply(this, arguments); + } - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + inherit(AttrRecognizer, Recognizer, { + /** + * @namespace + * @memberof AttrRecognizer + */ + defaults: { + /** + * @type {Number} + * @default 1 + */ + pointers: 1 + }, - // 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; + /** + * 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; + }, - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); + /** + * 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; - restack = true; - } + var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); + var isValid = this.attrTest(input); - // 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); - } + // 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; + } + }); - // recalculate the height of the group - var height = this._calculateHeight(margin); + /** + * Pan + * Recognized when the pointer is down and moved in the allowed direction. + * @constructor + * @extends AttrRecognizer + */ + function PanRecognizer() { + AttrRecognizer.apply(this, arguments); - // calculate actual size and position - var foreground = this.dom.foreground; - this.top = foreground.offsetTop; - this.left = foreground.offsetLeft; - this.width = foreground.offsetWidth; - resized = util.updateProperty(this, "height", height) || resized; + this.pX = null; + this.pY = null; + } - // 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; + inherit(PanRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof PanRecognizer + */ + defaults: { + event: 'pan', + threshold: 10, + pointers: 1, + direction: DIRECTION_ALL + }, - // apply new height - this.dom.background.style.height = height + "px"; - this.dom.foreground.style.height = height + "px"; - this.dom.label.style.height = height + "px"; + 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; + }, - // 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); - } + 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; - return 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; + }, - /** - * recalculate the height of the group - * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin - * @returns {number} Returns the height - * @private - */ - Group.prototype._calculateHeight = function (margin) { - // recalculate the height of the group - var height; - var visibleItems = this.visibleItems; - //var visibleSubgroups = []; - //this.visibleSubgroups = 0; - this.resetSubgroups(); - var me = this; - if (visibleItems.length > 0) { - var min = visibleItems[0].top; - var max = visibleItems[0].top + visibleItems[0].height; - util.forEach(visibleItems, function (item) { - min = Math.min(min, item.top); - max = Math.max(max, item.top + item.height); - if (item.data.subgroup !== undefined) { - me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); - me.subgroups[item.data.subgroup].visible = true; - } - }); - if (min > margin.axis) { - // there is an empty gap between the lowest item and the axis - var offset = min - margin.axis; - max -= offset; - util.forEach(visibleItems, function (item) { - item.top -= offset; - }); - } - height = max + margin.item.vertical / 2; - } else { - height = margin.axis + margin.item.vertical; - } - height = Math.max(height, this.props.label.height); + attrTest: function(input) { + return AttrRecognizer.prototype.attrTest.call(this, input) && + (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); + }, - return height; - }; + emit: function(input) { + this.pX = input.deltaX; + this.pY = input.deltaY; + + var direction = directionStr(input.direction); + if (direction) { + this.manager.emit(this.options.event + direction, input); + } + + this._super.emit.call(this, input); + } + }); /** - * 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); + } + inherit(SwipeRecognizer, AttrRecognizer, { + /** + * @namespace + * @memberof SwipeRecognizer + */ + defaults: { + event: 'swipe', + threshold: 10, + velocity: 0.65, + direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, + pointers: 1 + }, - /** - * Reorder the items - */ - Group.prototype.order = function () { - var array = util.toArray(this.items); - var startArray = []; - var endArray = []; + getTouchAction: function() { + return PanRecognizer.prototype.getTouchAction.call(this); + }, - 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 - }; + attrTest: function(input) { + var direction = this.options.direction; + var velocity; - stack.orderByStart(this.orderedItems.byStart); - stack.orderByEnd(this.orderedItems.byEnd); - }; + 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); + } + this.manager.emit(this.options.event, input); + } + }); /** - * 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 + * 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 */ - 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; + function TapRecognizer() { + Recognizer.apply(this, arguments); - // 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; - } - }; + // previous time and center, + // used for tap counting + this.pTime = false; + this.pCenter = false; - // 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._timer = null; + this._input = null; + this.count = 0; + } - // we do a binary search for the items that have only start values. - var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, "data", "start"); + 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 + }, - // 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; - }); + getTouchAction: function() { + return [TOUCH_ACTION_MANIPULATION]; + }, - // 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"); + process: function(input) { + var options = this.options; - // 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; - }); - } + var validPointers = input.pointers.length === options.pointers; + var validMovement = input.distance < options.threshold; + var validTouchTime = input.deltaTime < options.time; + this.reset(); - // 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(); - } + if ((input.eventType & INPUT_START) && (this.count === 0)) { + return this.failTimeout(); + } - // 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" : "") - // } - //} + // 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(); + } - return visibleItems; - }; + var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; + var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; - Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { - var item; - var i; + this.pTime = input.timeStamp; + this.pCenter = input.center; - 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 (!validMultiTap || !validInterval) { + this.count = 1; + } else { + this.count += 1; + } + + this._input = input; + + // if tap count matches we have recognized it, + // else it has began recognizing... + var tapCount = this.count % options.taps; + if (tapCount === 0) { + // no failing requirements, immediately trigger the tap event + // or wait as long as the multitap interval to trigger + if (!this.hasRequireFailures()) { + return STATE_RECOGNIZED; + } else { + this._timer = setTimeoutContext(function() { + this.state = STATE_RECOGNIZED; + this.tryEmit(); + }, options.interval, this); + return STATE_BEGAN; + } + } } - } - } + return STATE_FAILED; + }, - 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); + failTimeout: function() { + this._timer = setTimeoutContext(function() { + this.state = STATE_FAILED; + }, this.options.interval, this); + return STATE_FAILED; + }, + + reset: function() { + clearTimeout(this._timer); + }, + + emit: function() { + if (this.state == STATE_RECOGNIZED ) { + this._input.tapCount = this.count; + this.manager.emit(this.options.event, this._input); } - } } - } - }; - + }); /** - * 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 + * Simple way to create an manager with a default set of recognizers. + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor */ - Group.prototype._checkIfVisible = function (item, visibleItems, range) { - if (item.isVisible(range)) { - if (!item.displayed) item.show(); - // reposition item horizontally - item.repositionX(); - visibleItems.push(item); - } else { - if (item.displayed) item.hide(); - } - }; + function 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'; /** - * 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 + * default settings + * @namespace */ - 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(); - } - }; + Hammer.defaults = { + /** + * set if DOM events are being triggered. + * But this is slower and unused by simple implementations, so disabled by default. + * @type {Boolean} + * @default false + */ + domEvents: false, + /** + * The value for the touchAction property/fallback. + * When set to `compute` it will magically set the correct value based on the added recognizers. + * @type {String} + * @default compute + */ + touchAction: TOUCH_ACTION_COMPUTE, + /** + * @type {Boolean} + * @default true + */ + enable: true, - module.exports = Group; + /** + * 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, -/***/ }, -/* 31 */ -/***/ function(module, exports, __webpack_require__) { + /** + * force an input class + * @type {Null|Function} + * @default null + */ + inputClass: null, - "use strict"; + /** + * 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] + ], - var util = __webpack_require__(1); - var Group = __webpack_require__(30); + /** + * 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', - /** - * @constructor BackgroundGroup - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet - */ - function BackgroundGroup(groupId, data, itemSet) { - Group.call(this, groupId, data, itemSet); + /** + * Disable the Windows Phone grippers when pressing an element. + * @type {String} + * @default 'none' + */ + touchSelect: 'none', - this.width = 0; - this.height = 0; - this.top = 0; - this.left = 0; - } + /** + * 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', - BackgroundGroup.prototype = Object.create(Group.prototype); + /** + * Specifies whether zooming is enabled. Used by IE10> + * @type {String} + * @default 'none' + */ + contentZooming: 'none', + + /** + * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. + * @type {String} + * @default 'none' + */ + userDrag: 'none', + + /** + * Overrides the highlight color shown when the user taps a link or a JavaScript + * clickable element in iOS. This property obeys the alpha value, if specified. + * @type {String} + * @default 'rgba(0,0,0,0)' + */ + tapHighlightColor: 'rgba(0,0,0,0)' + } + }; + + var STOP = 1; + var FORCED_STOP = 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 + * Manager + * @param {HTMLElement} element + * @param {Object} [options] + * @constructor */ - BackgroundGroup.prototype.redraw = function (range, margin, restack) { - var resized = false; + function Manager(element, options) { + options = options || {}; - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); + this.options = merge(options, Hammer.defaults); + this.options.inputTarget = this.options.inputTarget || element; - // calculate actual size - this.width = this.dom.background.offsetWidth; + this.handlers = {}; + this.session = {}; + this.recognizers = []; - // apply new height (just always zero for BackgroundGroup - this.dom.background.style.height = "0"; + this.element = element; + this.input = createInputInstance(this); + this.touchAction = new TouchAction(this, this.options.touchAction); - // 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); - } + toggleCssProps(this, true); - return resized; - }; + each(options.recognizers, function(item) { + var recognizer = this.add(new (item[0])(item[1])); + item[2] && recognizer.recognizeWith(item[2]); + item[3] && recognizer.requireFailure(item[3]); + }, this); + } - /** - * Show this group: attach to the DOM - */ - BackgroundGroup.prototype.show = function () { - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); - } - }; + Manager.prototype = { + /** + * set options + * @param {Object} options + * @returns {Manager} + */ + set: function(options) { + extend(this.options, options); - module.exports = BackgroundGroup; + // 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; + }, -/***/ }, -/* 32 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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; + }, - "use strict"; + /** + * run the recognizers! + * called by the inputHandler function on every movement of the pointers (touches) + * it walks through all the recognizers and tries to detect the gesture that is being made + * @param {Object} inputData + */ + recognize: function(inputData) { + var session = this.session; + if (session.stopped) { + return; + } - var Hammer = __webpack_require__(41); - 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); + // run the touch-action polyfill + this.touchAction.preventDefaults(inputData); + var recognizer; + var recognizers = this.recognizers; - var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items - var BACKGROUND = "__background__"; // reserved group id for background items without group + // 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; - /** - * 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 - */ - function ItemSet(body, options) { - this.body = body; + // 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; + } - this.defaultOptions = { - type: null, // 'box', 'point', 'range', 'background' - orientation: "bottom", // 'top' or 'bottom' - align: "auto", // alignment of box items - stack: true, - groupOrder: null, + var i = 0; + while (i < recognizers.length) { + recognizer = recognizers[i]; + + // find out if we are allowed try to recognize the input for this one. + // 1. allow if the session is NOT forced stopped (see the .stop() method) + // 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one + // that is being recognized. + // 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer. + // this can be setup with the `recognizeWith()` method on the recognizer. + if (session.stopped !== FORCED_STOP && ( // 1 + !curRecognizer || recognizer == curRecognizer || // 2 + recognizer.canRecognizeWith(curRecognizer))) { // 3 + recognizer.recognize(inputData); + } else { + recognizer.reset(); + } - selectable: true, - editable: { - updateTime: false, - updateGroup: false, - add: false, - remove: false + // 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++; + } }, - snap: TimeStep.snap, + /** + * get a recognizer by its event name. + * @param {Recognizer|String} recognizer + * @returns {Recognizer|Null} + */ + get: function(recognizer) { + if (recognizer instanceof Recognizer) { + return recognizer; + } - 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); + var recognizers = this.recognizers; + for (var i = 0; i < recognizers.length; i++) { + if (recognizers[i].options.event == recognizer) { + return recognizers[i]; + } + } + return null; }, - margin: { - item: { - horizontal: 10, - vertical: 10 - }, - axis: 20 - }, - padding: 5 - }; + /** + * 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; + } - // options is shared by this ItemSet and all its items - this.options = util.extend({}, this.defaultOptions); + // remove existing + var existing = this.get(recognizer.options.event); + if (existing) { + this.remove(existing); + } - // options for getting items from the DataSet with the correct type - this.itemOptions = { - type: { start: "Date", end: "Date" } - }; + this.recognizers.push(recognizer); + recognizer.manager = this; - this.conversion = { - toScreen: body.util.toScreen, - toTime: body.util.toTime - }; - this.dom = {}; - this.props = {}; - this.hammer = null; + this.touchAction.update(); + return recognizer; + }, - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + /** + * remove a recognizer by name or instance + * @param {Recognizer|String} recognizer + * @returns {Manager} + */ + remove: function(recognizer) { + if (invokeArrayArg(recognizer, 'remove', this)) { + return this; + } - // 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); - } - }; + var recognizers = this.recognizers; + recognizer = this.get(recognizer); + recognizers.splice(inArray(recognizers, recognizer), 1); - // listeners for the DataSet of the groups - this.groupListeners = { - add: function (event, params, senderId) { - me._onAddGroups(params.items); + this.touchAction.update(); + return this; }, - update: function (event, params, senderId) { - me._onUpdateGroups(params.items); + + /** + * 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; }, - 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 = []; + /** + * unbind event, leave emit blank to remove all handlers + * @param {String} events + * @param {Function} [handler] + * @returns {EventEmitter} this + */ + off: function(events, handler) { + var handlers = this.handlers; + each(splitStr(events), function(event) { + if (!handler) { + delete handlers[event]; + } else { + handlers[event].splice(inArray(handlers[event], handler), 1); + } + }); + return this; + }, - this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw + /** + * 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); + } - this.touchParams = {}; // stores properties while dragging - // create the HTML DOM + // no handlers, so skip it all + var handlers = this.handlers[event] && this.handlers[event].slice(); + if (!handlers || !handlers.length) { + return; + } - this._create(); + data.type = event; + data.preventDefault = function() { + data.srcEvent.preventDefault(); + }; - this.setOptions(options); - } + var i = 0; + while (i < handlers.length) { + handlers[i](data); + i++; + } + }, - ItemSet.prototype = new Component(); + /** + * 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); - // available item types will be registered here - ItemSet.types = { - background: BackgroundItem, - box: BoxItem, - range: RangeItem, - point: PointItem + this.handlers = {}; + this.session = {}; + this.input.destroy(); + this.element = null; + } }; /** - * Create the HTML DOM for the ItemSet + * add/remove the css properties as defined in manager.options.cssProps + * @param {Manager} manager + * @param {Boolean} add */ - ItemSet.prototype._create = function () { - var frame = document.createElement("div"); - frame.className = "itemset"; - frame["timeline-itemset"] = this; - this.dom.frame = frame; + function toggleCssProps(manager, add) { + var element = manager.element; + each(manager.options.cssProps, function(value, name) { + element.style[prefixed(element.style, name)] = add ? value : ''; + }); + } - // create background panel - var background = document.createElement("div"); - background.className = "background"; - frame.appendChild(background); - this.dom.background = background; + /** + * 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); + } - // create foreground panel - var foreground = document.createElement("div"); - foreground.className = "foreground"; - frame.appendChild(foreground); - this.dom.foreground = foreground; + extend(Hammer, { + INPUT_START: INPUT_START, + INPUT_MOVE: INPUT_MOVE, + INPUT_END: INPUT_END, + INPUT_CANCEL: INPUT_CANCEL, - // create axis panel - var axis = document.createElement("div"); - axis.className = "axis"; - this.dom.axis = axis; + 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, - // create labelset - var labelSet = document.createElement("div"); - labelSet.className = "labelset"; - this.dom.labelSet = labelSet; + 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, - // create ungrouped Group - this._updateUngrouped(); + Manager: Manager, + Input: Input, + TouchAction: TouchAction, - // create background Group - var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); - backgroundGroup.show(); - this.groups[BACKGROUND] = backgroundGroup; + TouchInput: TouchInput, + MouseInput: MouseInput, + PointerEventInput: PointerEventInput, + TouchMouseInput: TouchMouseInput, + SingleTouchInput: SingleTouchInput, - // 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); + Recognizer: Recognizer, + AttrRecognizer: AttrRecognizer, + Tap: TapRecognizer, + Pan: PanRecognizer, + Swipe: SwipeRecognizer, + Pinch: PinchRecognizer, + Rotate: RotateRecognizer, + Press: PressRecognizer, - // 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)); + on: addEventListeners, + off: removeEventListeners, + each: each, + merge: merge, + extend: extend, + inherit: inherit, + bindFn: bindFn, + prefixed: prefixed + }); - // single select (or unselect) when tapping an item - this.hammer.on("tap", this._onSelectItem.bind(this)); + 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; + } - // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on("press", this._onMultiSelectItem.bind(this)); + })(window, document, 'Hammer'); - // add item on doubletap - this.hammer.on("doubletap", this._onAddItem.bind(this)); - // attach to the DOM - this.show(); - }; +/***/ }, +/* 22 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Set options for the ItemSet. Existing options will be extended/overwritten. - * @param {Object} [options] The following options are available: - * {String} type - * Default type for the items. Choose from 'box' - * (default), 'point', 'range', or 'background'. - * The default style can be overwritten by - * individual items. - * {String} align - * Alignment for the items, only applicable for - * BoxItem. Choose 'center' (default), 'left', or - * 'right'. - * {String} orientation - * 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); + /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; - 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); - } - } - } - } + /* WEBPACK VAR INJECTION */}.call(exports, {})) - 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); - } - } +/***/ }, +/* 23 */ +/***/ function(module, exports, __webpack_require__) { - // callback functions - var addCallback = (function (name) { - var fn = options[name]; - if (fn) { - if (!(fn instanceof Function)) { - throw new Error("option " + name + " must be a function " + name + "(item, callback)"); - } - this.options[name] = fn; - } - }).bind(this); - ["onAdd", "onUpdate", "onRemove", "onMove", "onMoving"].forEach(addCallback); + "use strict"; - // force the itemSet to refresh: options like orientation and margins may be changed - this.markDirty(); - } - }; + 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); /** - * 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] + * @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.markDirty = function (options) { - this.groupIds = []; - this.stackDirty = true; + function Range(body, options) { + var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0); + this.start = now.clone().add(-3, "days").valueOf(); // Number + this.end = now.clone().add(4, "days").valueOf(); // Number + + this.body = body; + this.deltaDifference = 0; + this.scaleOffset = 0; + this.startToFront = false; + this.endToFront = true; + + // default options + this.defaultOptions = { + start: null, + end: null, + direction: "horizontal", // 'horizontal' or 'vertical' + moveable: true, + zoomable: true, + min: null, + max: null, + zoomMin: 10, // milliseconds + zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds + }; + this.options = util.extend({}, this.defaultOptions); + + this.props = { + touch: {} + }; + this.animateTimer = null; + + // drag listeners for dragging + this.body.emitter.on("panstart", this._onDragStart.bind(this)); + this.body.emitter.on("panmove", this._onDrag.bind(this)); + this.body.emitter.on("panend", this._onDragEnd.bind(this)); + + // ignore dragging when holding + this.body.emitter.on("press", this._onHold.bind(this)); - if (options && options.refreshItems) { - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); - } - }; + // mouse wheel for zooming + this.body.emitter.on("mousewheel", this._onMouseWheel.bind(this)); - /** - * Destroy the ItemSet - */ - ItemSet.prototype.destroy = function () { - this.hide(); - this.setItems(null); - this.setGroups(null); + // pinch to zoom + this.body.emitter.on("touch", this._onTouch.bind(this)); + this.body.emitter.on("pinch", this._onPinch.bind(this)); - this.hammer = null; + this.setOptions(options); + } - this.body = null; - this.conversion = null; - }; + Range.prototype = new Component(); /** - * Hide the component from the DOM + * 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.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } - - // remove the axis with dots - if (this.dom.axis.parentNode) { - this.dom.axis.parentNode.removeChild(this.dom.axis); - } + 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); - // remove the labelset containing all group labels - if (this.dom.labelSet.parentNode) { - this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + if ("start" in options || "end" in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); + } } }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ - ItemSet.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); + function validateDirection(direction) { + if (direction != "horizontal" && direction != "vertical") { + throw new TypeError("Unknown direction \"" + direction + "\". " + "Choose \"horizontal\" or \"vertical\"."); } + } - // show axis with dots - if (!this.dom.axis.parentNode) { - this.body.dom.backgroundVertical.appendChild(this.dom.axis); + /** + * 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(); - // show labelset containing labels - if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); - } - }; + 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; - /** - * 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; + 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); - if (ids == undefined) ids = []; - if (!Array.isArray(ids)) ids = [ids]; + 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 }); + } - // 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 (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); + } + } + }; - // 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(); + 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 selected items by their id - * @return {Array} ids The ids of the selected items + * Stop an animation + * @private */ - ItemSet.prototype.getSelection = function () { - return this.selection.concat([]); + Range.prototype._cancelAnimation = function () { + if (this.animateTimer) { + clearTimeout(this.animateTimer); + this.animateTimer = null; + } }; /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible 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.getVisibleItems = function () { - var range = this.body.range.getRange(); - var left = this.body.util.toScreen(range.start); - var right = this.body.util.toScreen(range.end); + Range.prototype._applyRange = function (start, end) { + var newStart = start != null ? util.convert(start, "Date").valueOf() : this.start, + newEnd = end != null ? util.convert(end, "Date").valueOf() : this.end, + max = this.options.max != null ? util.convert(this.options.max, "Date").valueOf() : null, + min = this.options.min != null ? util.convert(this.options.min, "Date").valueOf() : null, + diff; - var ids = []; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - var group = this.groups[groupId]; - var rawVisibleItems = group.visibleItems; + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error("Invalid start \"" + start + "\""); + } + if (isNaN(newEnd) || newEnd === null) { + throw new Error("Invalid end \"" + end + "\""); + } - // 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); + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } + + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = min - newStart; + newStart += diff; + newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; } } } } - return ids; - }; + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = newEnd - max; + newStart -= diff; + newEnd -= diff; - /** - * 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; + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } } } - }; - /** - * 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; + // 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; + } + } + } - // 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; + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } - // update class name - frame.className = "itemset" + (editable ? " editable" : ""); + 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; + } + } + } - // reorder the groups (if needed) - resized = this._orderGroups() || resized; + var changed = this.start != newStart || this.end != newEnd; - // 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; + // 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"); + } - 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 + this.start = newStart; + this.end = newEnd; + return changed; + }; + + /** + * Retrieve the current range. + * @return {Object} An object with start and end properties + */ + Range.prototype.getRange = function () { + return { + start: this.start, + end: this.end }; - var height = 0; - var minHeight = margin.axis + margin.item.vertical; + }; - // redraw the background group - this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); + /** + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ + Range.prototype.conversion = function (width, totalHidden) { + return Range.conversion(this.start, this.end, width, totalHidden); + }; - // 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; + /** + * 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 + }; + } + }; - // update frame height - frame.style.height = asSize(height); + /** + * Start dragging horizontally or vertically + * @param {Event} event + * @private + */ + Range.prototype._onDragStart = function (event) { + this.deltaDifference = 0; + this.previousDelta = 0; + // only allow dragging when configured as movable + if (!this.options.moveable) return; - // calculate actual size - this.props.width = frame.offsetWidth; - this.props.height = height; + // 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; - // reposition axis - this.dom.axis.style.top = asSize(orientation == "top" ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); - this.dom.axis.style.left = "0"; + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.dragging = true; - // check if this component is resized - resized = this._isResized() || resized; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = "move"; + } - return resized; + event.preventDefault(); }; /** - * Get the first group, aligned with the axis - * @return {Group | null} firstGroup + * Perform dragging operation + * @param {Event} event * @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]; + Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - return firstGroup || null; - }; + // 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; - /** - * 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; + 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; - if (this.groupsData) { - // remove the group holding all ungrouped items - if (ungrouped) { - ungrouped.hide(); - delete this.groups[UNGROUPED]; + // normalize dragging speed if cutout is in between. + var duration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + interval -= duration; - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - item.parent && item.parent.remove(item); - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - group && group.add(item) || item.hide(); - } - } - } - } else { - // create a group holding all (unfiltered) items - if (!ungrouped) { - var id = null; - var data = null; - ungrouped = new Group(id, data, this); - this.groups[UNGROUPED] = ungrouped; + 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; - for (itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - item = this.items[itemId]; - ungrouped.add(item); - } - } + // 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 + }); - ungrouped.show(); - } - } + event.preventDefault(); }; /** - * Get the element for the labelset - * @return {HTMLElement} labelSet + * Stop dragging operation + * @param {event} event + * @private */ - ItemSet.prototype.getLabelSet = function () { - return this.dom.labelSet; + 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; + + this.props.touch.dragging = false; + if (this.body.dom.root) { + this.body.dom.root.style.cursor = "auto"; + } + + // fire a rangechanged event + this.body.emitter.emit("rangechanged", { + start: new Date(this.start), + end: new Date(this.end), + byUser: true + }); }; /** - * Set items - * @param {vis.DataSet | null} items + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event + * @private */ - ItemSet.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; + Range.prototype._onMouseWheel = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - // 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"); + // 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 (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 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 - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); - }); + // 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); + } - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + // 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); - // update the group holding all ungrouped items - this._updateUngrouped(); + this.zoom(scale, pointerDate, delta); } + + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); }; /** - * Get the current items - * @returns {vis.DataSet | null} + * Start of a touch gesture + * @private */ - ItemSet.prototype.getItems = function () { - return this.itemsData; + 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; }; /** - * Set groups - * @param {vis.DataSet} groups + * On start of a hold gesture + * @private */ - ItemSet.prototype.setGroups = function (groups) { - var me = this, - ids; + Range.prototype._onHold = function () { + this.props.touch.allowDragging = false; + }; - // unsubscribe from current dataset - if (this.groupsData) { - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.unsubscribe(event, callback); - }); + /** + * Handle pinch event + * @param {Event} event + * @private + */ + Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + this.props.touch.allowDragging = false; - // 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.props.touch.center) { + this.props.touch.center = getPointer(event.center, this.body.dom.center); } - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + var scale = 1 / (event.scale + this.scaleOffset); + var centerDate = this._pointerToDate(this.props.touch.center); - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, centerDate); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; + + // 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; + + // snapping times away from hidden zones + this.startToFront = 1 - scale <= 0; // used to do the right auto correction with periodic hidden times + this.endToFront = scale - 1 <= 0; // used to do the right auto correction with periodic hidden times + + var 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; } - // update the group holding all ungrouped items - this._updateUngrouped(); + this.setRange(newStart, newEnd, false, true); - // update the order of all items in each group - this._order(); + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default - this.body.emitter.emit("change", { queue: true }); + event.preventDefault(); }; /** - * Get the current groups - * @returns {vis.DataSet | null} groups + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date + * @private */ - ItemSet.prototype.getGroups = function () { - return this.groupsData; - }; + Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - /** - * 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(); + validateDirection(direction); - 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 (direction == "horizontal") { + return this.body.util.toTime(pointer.x).valueOf(); + } else { + var height = this.body.domProps.center.height; + conversion = this.conversion(height); + return pointer.y / conversion.scale + conversion.offset; } }; /** - * Get the time of an item based on it's data and options.type - * @param {Object} itemData - * @returns {string} Returns the type + * 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._getType = function (itemData) { - return itemData.type || this.options.type || (itemData.end ? "range" : "box"); - }; - + function getPointer(touch, element) { + return { + x: touch.x - util.getAbsoluteLeft(element), + y: touch.y - util.getAbsoluteTop(element) + }; + } /** - * Get the group id for an item - * @param {Object} itemData - * @returns {string} Returns the groupId - * @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._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.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; } - }; - - /** - * Handle updated items - * @param {Number[]} ids - * @protected - */ - 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); + var hiddenDuration = DateUtil.getHiddenDurationBetween(this.body.hiddenDates, this.start, this.end); + var hiddenDurationBefore = DateUtil.getHiddenDurationBefore(this.body.hiddenDates, this, center); + var hiddenDurationAfter = hiddenDuration - hiddenDurationBefore; - var constructor = ItemSet.types[type]; + // calculate new start and end + var newStart = center - hiddenDurationBefore + (this.start - (center - hiddenDurationBefore)) * scale; + var newEnd = center + hiddenDurationAfter + (this.end - (center + hiddenDurationAfter)) * scale; - 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); - } - } + // 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; + } - 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.setRange(newStart, newEnd, false, true); - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit("change", { queue: true }); + this.startToFront = false; // revert to default + this.endToFront = true; // revert to default }; - /** - * Handle added items - * @param {Number[]} ids - * @protected - */ - ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; + /** - * Handle removed items - * @param {Number[]} ids - * @protected + * 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._onRemove = function (ids) { - var count = 0; - var me = this; - ids.forEach(function (id) { - var item = me.items[id]; - if (item) { - count++; - me._removeItem(item); - } - }); + Range.prototype.move = function (delta) { + // zoom start Date and end Date relative to the centerDate + var diff = this.end - this.start; - if (count) { - // update order - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit("change", { queue: true }); - } - }; + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; - /** - * Update the order of item in all groups - * @private - */ - ItemSet.prototype._order = function () { - // reorder the items in all groups - // TODO: optimization: only reorder groups affected by the changed items - util.forEach(this.groups, function (group) { - group.order(); - }); - }; + // TODO: reckon with min and max range - /** - * Handle updated groups - * @param {Number[]} ids - * @private - */ - ItemSet.prototype._onUpdateGroups = function (ids) { - this._onAddGroups(ids); + this.start = newStart; + this.end = newEnd; }; /** - * Handle changed groups (added or updated) - * @param {Number[]} ids - * @private + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ - ItemSet.prototype._onAddGroups = function (ids) { - var me = this; + Range.prototype.moveTo = function (moveTo) { + var center = (this.start + this.end) / 2; - ids.forEach(function (id) { - var groupData = me.groupsData.get(id); - var group = me.groups[id]; + var diff = center - moveTo; - if (!group) { - // check for reserved ids - if (id == UNGROUPED || id == BACKGROUND) { - throw new Error("Illegal group id. " + id + " is a reserved id."); - } + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null - }); + this.setRange(newStart, newEnd); + }; - group = new Group(id, groupData, me); - me.groups[id] = group; + module.exports = Range; - // 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); - } - } - } +/***/ }, +/* 24 */ +/***/ function(module, exports, __webpack_require__) { - group.order(); - group.show(); - } else { - // update group - group.setData(groupData); + "use strict"; + + var Hammer = __webpack_require__(19); + + /** + * 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.body.emitter.emit("change", { queue: true }); + hammer.on("hammer.input", callback.inputHandler); }; /** - * Handle removed groups - * @param {Number[]} ids - * @private + * Register a release event, taking place after a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - ItemSet.prototype._onRemoveGroups = function (ids) { - var groups = this.groups; - ids.forEach(function (id) { - var group = groups[id]; - - if (group) { - group.hide(); - delete groups[id]; + exports.onRelease = function (hammer, callback) { + callback.inputHandler = function (event) { + if (event.isFinal) { + callback(event); } - }); + }; - this.markDirty(); + return hammer.on("hammer.input", callback.inputHandler); + }; - this.body.emitter.emit("change", { queue: true }); + /** + * 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); }; /** - * Reorder the groups if needed - * @return {boolean} changed - * @private + * Unregister a release event, taking place before a gesture + * @param {Hammer} hammer A hammer instance + * @param {function} callback Callback, called as callback(event) */ - ItemSet.prototype._orderGroups = function () { - if (this.groupsData) { - // reorder the groups - var groupIds = this.groupsData.getIds({ - order: this.options.groupOrder - }); + exports.offRelease = exports.offTouch; - 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(); - }); +/***/ }, +/* 25 */ +/***/ function(module, exports, __webpack_require__) { - // show the groups again, attach them to the DOM in correct order - groupIds.forEach(function (groupId) { - groups[groupId].show(); - }); + "use strict"; - this.groupIds = groupIds; - } + /** + * 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; + } - return changed; - } else { - return false; + /** + * 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); } }; /** - * Add a new item - * @param {Item} item - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - 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); + Component.prototype.redraw = function () { + // should be implemented by the component + return false; }; /** - * Update an existing item - * @param {Item} item - * @param {Object} itemData - * @private + * Destroy the component. Cleanup DOM and event listeners */ - ItemSet.prototype._updateItem = function (item, itemData) { - var oldGroupId = item.data.group; + Component.prototype.destroy = function () {}; - // update the items data (will redraw the item when displayed) - item.setData(itemData); + /** + * 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; - // update group - if (oldGroupId != item.data.group) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - var groupId = this._getGroupId(item.data); - var group = this.groups[groupId]; - if (group) group.add(item); - } + return resized; }; - /** - * Delete an item from the ItemSet: remove it from the DOM, from the map - * with items, and from the map with visible items, and from the selection - * @param {Item} item - * @private - */ - ItemSet.prototype._removeItem = function (item) { - // remove from DOM - item.hide(); - - // remove from items - delete this.items[item.id]; + module.exports = Component; + // should be implemented by the component - // remove from selection - var index = this.selection.indexOf(item.id); - if (index != -1) this.selection.splice(index, 1); +/***/ }, +/* 26 */ +/***/ function(module, exports, __webpack_require__) { - // remove from group - item.parent && item.parent.remove(item); - }; + "use strict"; /** - * Create an array containing all items being a range (having an end date) - * @param array - * @returns {Array} - * @private + * Created by Alex on 10/3/2014. */ - ItemSet.prototype._constructByEndArray = function (array) { - var endArray = []; + var moment = __webpack_require__(2); - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof RangeItem) { - endArray.push(array[i]); - } - } - return endArray; - }; /** - * Register the clicked item on touch, before dragStart is initiated. - * - * dragStart is initiated from a mousemove event, AFTER the mouse/touch is - * already moving. Therefore, the mouse/touch can sometimes be above an other - * DOM element than the item itself. - * - * @param {Event} event - * @private + * used in Core to convert the options into a volatile variable + * + * @param Core */ - 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; + 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 + } + } }; + /** - * Start dragging the selected events - * @param {Event} event - * @private + * create new entrees for the repeating hidden dates + * @param body + * @param hiddenDates */ - ItemSet.prototype._onDragStart = function (event) { - if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { - return; - } + exports.updateHiddenDates = function (body, hiddenDates) { + if (hiddenDates && body.domProps.centerContainer.width !== undefined) { + exports.convertHiddenOptions(body, hiddenDates); - var item = this.touchParams.item || null; - var me = this; - var props; + var start = moment(body.range.start); + var end = moment(body.range.end); - if (item && item.selected) { - var dragLeftItem = this.touchParams.dragLeftItem; - var dragRightItem = this.touchParams.dragRightItem; + var totalRange = body.range.end - body.range.start; + var pixelTime = totalRange / body.domProps.centerContainer.width; - if (dragLeftItem) { - props = { - item: dragLeftItem, - initialX: event.center.x - }; + 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 (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; - } + if (startDate._d == "Invalid Date") { + throw new Error("Supplied start date is not valid: " + hiddenDates[i].start); + } + if (endDate._d == "Invalid Date") { + throw new Error("Supplied end date is not valid: " + hiddenDates[i].end); + } - this.touchParams.itemProps = [props]; - } else if (dragRightItem) { - props = { - item: dragRightItem, - initialX: event.center.x - }; + 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 (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; - } + endDate.dayOfYear(start.dayOfYear()); + endDate.year(start.year()); + endDate.subtract(7 - offset, "days"); - 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 - }; + runUntil.add(1, "weeks"); + break; + case "weekly": + var dayOffset = endDate.diff(startDate, "days"); + var day = startDate.day(); - if (me.options.editable.updateTime) { - if ("start" in item.data) { - props.start = item.data.start.valueOf(); + // set the start date to the range.start + startDate.date(start.date()); + startDate.month(start.month()); + startDate.year(start.year()); + endDate = startDate.clone(); - 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; + // 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() }); } - if (me.options.editable.updateGroup) { - if ("group" in item.data) props.group = item.data.group; + } + } + // 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); + } + } + }; + + + /** + * remove duplicates from the hidden dates list. Duplicates are evil. They mess everything up. + * Scales with N^2 + * @param body + */ + exports.removeDuplicates = function (body) { + var hiddenDates = body.hiddenDates; + var safeDates = []; + for (var i = 0; i < hiddenDates.length; i++) { + for (var j = 0; j < hiddenDates.length; j++) { + if (i != j && hiddenDates[j].remove != true && hiddenDates[i].remove != true) { + // j inside i + if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[j].remove = true; + } + // j start inside i + else if (hiddenDates[j].start >= hiddenDates[i].start && hiddenDates[j].start <= hiddenDates[i].end) { + hiddenDates[i].end = hiddenDates[j].end; + hiddenDates[j].remove = true; + } + // j end inside i + else if (hiddenDates[j].end >= hiddenDates[i].start && hiddenDates[j].end <= hiddenDates[i].end) { + hiddenDates[i].start = hiddenDates[j].start; + hiddenDates[j].remove = true; } + } + } + } - return props; - }); + for (var i = 0; i < hiddenDates.length; i++) { + if (hiddenDates[i].remove !== true) { + safeDates.push(hiddenDates[i]); } + } - event.stopPropagation(); - event.preventDefault(); + body.hiddenDates = safeDates; + body.hiddenDates.sort(function (a, b) { + return a.start - b.start; + }); // sort by start time + }; + + exports.printDates = function (dates) { + for (var i = 0; i < dates.length; i++) { + console.log(i, new Date(dates[i].start), new Date(dates[i].end), dates[i].start, dates[i].end, dates[i].remove); } }; /** - * Drag selected items - * @param {Event} event - * @private + * Used in TimeStep to avoid the hidden times. + * @param timeStep + * @param previousTime */ - ItemSet.prototype._onDrag = function (event) { - event.preventDefault(); + exports.stepOverHiddenDates = function (timeStep, previousTime) { + var stepInHidden = false; + var currentValue = timeStep.current.valueOf(); + for (var i = 0; i < timeStep.hiddenDates.length; i++) { + var startDate = timeStep.hiddenDates[i].start; + var endDate = timeStep.hiddenDates[i].end; + if (currentValue >= startDate && currentValue < endDate) { + stepInHidden = true; + break; + } + } - if (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(); + 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; + } - // 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; + timeStep.current = newValue.toDate(); + } + }; - 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); - } + ///** + // * 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 ("group" in props) { - // drag from one group to another - var group = me.groupFromTarget(event); - newProps.group = group && group.groupId; - } + /** + * 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; + } - // 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); - } - }); - }); + var duration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + time = exports.correctTimeForHidden(Core.body.hiddenDates, Core.range, time); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit("change"); + var conversion = Core.range.conversion(width, duration); + return (time.valueOf() - conversion.offset) * conversion.scale; + } + }; - event.stopPropagation(); + + /** + * Replaces the core toTime methods + * @param body + * @param range + * @param x + * @param width + * @returns {Date} + */ + exports.toTime = function (Core, x, width) { + if (Core.body.hiddenDates.length == 0) { + var conversion = Core.range.conversion(width); + return new Date(x / conversion.scale + conversion.offset); + } else { + var hiddenDuration = exports.getHiddenDurationBetween(Core.body.hiddenDates, Core.range.start, Core.range.end); + var totalDuration = Core.range.end - Core.range.start - hiddenDuration; + var partialDuration = totalDuration * x / width; + var accumulatedHiddenDuration = exports.getAccumulatedHiddenDuration(Core.body.hiddenDates, Core.range, partialDuration); + + var newTime = new Date(accumulatedHiddenDuration + partialDuration + Core.range.start); + return newTime; + } + }; + + + /** + * Support function + * + * @param hiddenDates + * @param range + * @returns {number} + */ + exports.getHiddenDurationBetween = function (hiddenDates, start, end) { + var duration = 0; + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= start && endDate < end) { + duration += endDate - startDate; + } } + return duration; }; + /** - * Update an items properties - * @param {Item} item - * @param {Object} props Can contain properties start, end, and group. - * @private + * Support function + * @param hiddenDates + * @param range + * @param time + * @returns {{duration: number, time: *, offset: number}} */ - 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); - } + exports.correctTimeForHidden = function (hiddenDates, range, time) { + time = moment(time).toDate().valueOf(); + time -= exports.getHiddenDurationBefore(hiddenDates, range, time); + return time; }; - /** - * Move an item to another group - * @param {Item} item - * @param {String | Number} groupId - * @private - */ - ItemSet.prototype._moveToGroup = function (item, groupId) { - var group = this.groups[groupId]; - if (group && group.groupId != item.data.group) { - var oldGroup = item.parent; - oldGroup.remove(item); - oldGroup.order(); - group.add(item); - group.order(); + exports.getHiddenDurationBefore = function (hiddenDates, range, time) { + var timeOffset = 0; + time = moment(time).toDate().valueOf(); - item.data.group = group.groupId; + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; + // if time after the cutout, and the + if (startDate >= range.start && endDate < range.end) { + if (time >= endDate) { + timeOffset += endDate - startDate; + } + } } + return timeOffset; }; /** - * End of dragging selected items - * @param {Event} event - * @private + * 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.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.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; + } + } + } - 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); + return hiddenDuration; + }; - 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; - } - // 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"); - } - }); + /** + * 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 (correctionEnabled == true) { + return isHidden.endDate + (time - isHidden.startDate) + 1; + } else { + return isHidden.endDate + 1; } - }); - - // apply the changes to the data (if there are changes) - if (changes.length) { - dataset.update(changes); } - - event.stopPropagation(); + } else { + return time; } }; + /** - * Handle selecting/deselecting an item when tapping it - * @param {Event} event - * @private + * Check if a time is hidden + * + * @param time + * @param hiddenDates + * @returns {{hidden: boolean, startDate: Window.start, endDate: *}} */ - ItemSet.prototype._onSelectItem = function (event) { - if (!this.options.selectable) return; + exports.isHidden = function (time, hiddenDates) { + for (var i = 0; i < hiddenDates.length; i++) { + var startDate = hiddenDates[i].start; + var endDate = hiddenDates[i].end; - var ctrlKey = event.srcEvent && event.srcEvent.ctrlKey; - var shiftKey = event.srcEvent && event.srcEvent.shiftKey; - if (ctrlKey || shiftKey) { - this._onMultiSelectItem(event); - return; + if (time >= startDate && time < endDate) { + // if the start is entering a hidden zone + return { hidden: true, startDate: startDate, endDate: endDate }; + break; + } } + return { hidden: false, startDate: startDate, endDate: endDate }; + }; - var oldSelection = this.getSelection(); +/***/ }, +/* 27 */ +/***/ function(module, exports, __webpack_require__) { - var item = ItemSet.itemFromTarget(event); - var selection = item ? [item.id] : []; - this.setSelection(selection); + "use strict"; - var newSelection = this.getSelection(); + var Emitter = __webpack_require__(11); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); + var Range = __webpack_require__(23); + var ItemSet = __webpack_require__(28); + var Activator = __webpack_require__(38); + var DateUtil = __webpack_require__(26); + var CustomTime = __webpack_require__(40); - // 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 - }); - } - }; + /** + * Create a timeline visualization + * @param {HTMLElement} container + * @param {vis.DataSet | Array | google.visualization.DataTable} [items] + * @param {Object} [options] See Core.setOptions for the available options. + * @constructor + */ + function Core() {} + + // turn Core into an event emitter + Emitter(Core.prototype); /** - * Handle creation and updates of an item on double tap - * @param event + * 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 */ - ItemSet.prototype._onAddItem = function (event) { - if (!this.options.selectable) return; - if (!this.options.editable.add) return; + Core.prototype._create = function (container) { + this.dom = {}; - var me = this, - snap = this.options.snap || null, - item = ItemSet.itemFromTarget(event); + this.dom.root = document.createElement("div"); + this.dom.background = document.createElement("div"); + this.dom.backgroundVertical = document.createElement("div"); + this.dom.backgroundHorizontal = document.createElement("div"); + this.dom.centerContainer = document.createElement("div"); + this.dom.leftContainer = document.createElement("div"); + this.dom.rightContainer = document.createElement("div"); + this.dom.center = document.createElement("div"); + this.dom.left = document.createElement("div"); + this.dom.right = document.createElement("div"); + this.dom.top = document.createElement("div"); + this.dom.bottom = document.createElement("div"); + this.dom.shadowTop = document.createElement("div"); + this.dom.shadowBottom = document.createElement("div"); + this.dom.shadowTopLeft = document.createElement("div"); + this.dom.shadowBottomLeft = document.createElement("div"); + this.dom.shadowTopRight = document.createElement("div"); + this.dom.shadowBottomRight = document.createElement("div"); - if (item) { - // update item + 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"; - // 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); + this.dom.root.appendChild(this.dom.background); + this.dom.root.appendChild(this.dom.backgroundVertical); + this.dom.root.appendChild(this.dom.backgroundHorizontal); + this.dom.root.appendChild(this.dom.centerContainer); + this.dom.root.appendChild(this.dom.leftContainer); + this.dom.root.appendChild(this.dom.rightContainer); + this.dom.root.appendChild(this.dom.top); + this.dom.root.appendChild(this.dom.bottom); + + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); + + this.dom.centerContainer.appendChild(this.dom.shadowTop); + this.dom.centerContainer.appendChild(this.dom.shadowBottom); + this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); + this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); + this.dom.rightContainer.appendChild(this.dom.shadowTopRight); + this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); + + this.on("rangechange", this.redraw.bind(this)); + + 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 { - // 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(); + } else { + // redraw immediately + me._redraw(); + } + }); - var newItem = { - start: snap ? snap(start, scale, step) : start, - content: "new item" + // create event listeners for all interesting events, these events will be + // emitted via emitter + this.hammer = new Hammer(this.dom.root, { touchAction: "pan-y" }); + this.hammer.get("pinch").set({ enable: true }); + this.listeners = {}; + + var events = ["tap", "doubletap", "press", "pinch", "pan", "panstart", "panmove", "panend" + // TODO: cleanup + //'touch', 'pinch', + //'tap', 'doubletap', 'hold', + //'dragstart', 'drag', 'dragend', + //'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (type) { + var listener = function (event) { + if (me.isActive()) { + me.emit(type, event); + } }; + me.hammer.on(type, listener); + me.listeners[type] = listener; + }); - // 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; + // 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.root.addEventListener("mousewheel", onMouseWheel); + this.dom.root.addEventListener("DOMMouseScroll", onMouseWheel); - newItem[this.itemsData._fieldId] = util.randomUUID(); + // size properties of each of the panels + this.props = { + root: {}, + background: {}, + centerContainer: {}, + leftContainer: {}, + rightContainer: {}, + center: {}, + left: {}, + right: {}, + top: {}, + bottom: {}, + border: {}, + scrollTop: 0, + scrollTopMin: 0 + }; - var group = this.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; + this.redrawCount = 0; + + // attach the root panel to the provided container + if (!container) throw new Error("No container provided"); + container.appendChild(this.dom.root); + }; + + /** + * Set options. Options will be passed to all components loaded in the Timeline. + * @param {Object} [options] + * {String} orientation + * Vertical orientation for the Timeline, + * can be 'bottom' (default) or 'top'. + * {String | Number} width + * Width for the timeline, a number in pixels or + * a css string like '1000px' or '75%'. '100%' by default. + * {String | Number} height + * Fixed height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. If undefined, + * The Timeline will automatically size such that + * its contents fit. + * {String | Number} minHeight + * Minimum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {String | Number} maxHeight + * Maximum height for the Timeline, a number in pixels or + * a css string like '400px' or '75%'. + * {Number | Date | String} start + * Start date for the visible window + * {Number | Date | String} end + * End date for the visible window + */ + Core.prototype.setOptions = function (options) { + if (options) { + // copy the known options + var fields = ["width", "height", "minHeight", "maxHeight", "autoResize", "start", "end", "orientation", "clickToUse", "dataAttributes", "hiddenDates"]; + util.selectiveExtend(fields, this.options, options); + + if ("hiddenDates" in this.options) { + DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); } - // 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? + 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; + } } - }); + } + + // enable/disable autoResize + this._initAutoResize(); + } + + // propagate options to all components + this.components.forEach(function (component) { + return component.setOptions(options); + }); + + // TODO: remove deprecation error one day (deprecated since version 0.8.0) + if (options && options.order) { + throw new Error("Option order is deprecated. There is no replacement for this feature."); } + + // redraw everything + this._redraw(); }; /** - * Handle selecting/deselecting multiple items when holding an item - * @param {Event} event - * @private + * Returns true when the Timeline is active. + * @returns {boolean} */ - ItemSet.prototype._onMultiSelectItem = function (event) { - if (!this.options.selectable) return; - - var selection, - item = ItemSet.itemFromTarget(event); + Core.prototype.isActive = function () { + return !this.activator || this.activator.active; + }; - if (item) { - // multi select items - selection = this.getSelection(); // current selection + /** + * Destroy the Core, clean up all DOM elements and event listeners. + */ + Core.prototype.destroy = function () { + // unbind datasets + this.clear(); - var shiftKey = event.srcEvent && event.srcEvent.shiftKey || false; - if (shiftKey) { - // select all items between the old selection and the tapped item + // remove all event listeners + this.off(); - // determine the selection range - selection.push(item.id); - var range = ItemSet._getItemRange(this.itemsData.get(selection, this.itemOptions)); + // stop checking for changed size + this._stopAutoResize(); - // 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; + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); + } + this.dom = null; - 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); - } + // remove Activator + if (this.activator) { + this.activator.destroy(); + delete this.activator; + } + + // cleanup hammer touch events + for (var event in this.listeners) { + if (this.listeners.hasOwnProperty(event)) { + delete this.listeners[event]; } + } + this.listeners = null; + this.hammer = null; - this.setSelection(selection); + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + return component.destroy(); + }); - this.body.emitter.emit("select", { - items: this.getSelection() - }); - } + this.body = null; }; + /** - * 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 + * Set a custom time bar + * @param {Date} time + * @param {int} id */ - ItemSet._getItemRange = function (itemsData) { - var max = null; - var min = null; + Core.prototype.setCustomTime = function (time, id) { + if (!this.customTime) { + throw new Error("Cannot get custom time: Custom time bar is not enabled"); + } - itemsData.forEach(function (data) { - if (min == null || data.start < min) { - min = data.start; - } + var barId = id || 0; - if (data.end != undefined) { - if (max == null || data.end > max) { - max = data.end; - } - } else { - if (max == null || data.start > max) { - max = data.start; - } + this.components.forEach(function (element, index, components) { + if (element instanceof CustomTime && element.options.id === barId) { + element.setCustomTime(time); } }); - - return { - min: min, - max: max - }; }; /** - * Find an item from an event target: - * searches for the attribute 'timeline-item' in the event target's element tree - * @param {Event} event - * @return {Item | null} item + * Retrieve the current custom time. + * @return {Date} customTime + * @param {int} id */ - ItemSet.itemFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty("timeline-item")) { - return target["timeline-item"]; - } - target = target.parentNode; + Core.prototype.getCustomTime = function (id) { + if (!this.customTime) { + throw new Error("Cannot get custom time: Custom time bar is not enabled"); } - return null; + 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; }; /** - * 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 + * 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 */ - 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; - //} - // + Core.prototype.addCustomTime = function (time, id) { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); + } - 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 (time === undefined) { + throw new Error("Time parameter for the custom bar must be provided"); + } - 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; + var ts = util.convert(time, "Date").valueOf(), + numIds, + customTime, + customBarId; + + // 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 the ID is not provided, generate one, otherwise just use it + if (id === undefined) { + numIds = this.customBarIds.filter(function (element) { + return util.isNumber(element); + }); + + 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"); } - } + }); + + customBarId = id; } - return null; + this.customBarIds.push(customBarId); + + customTime = new CustomTime(this.body, { + showCustomTime: true, + time: ts, + id: customBarId + }); + + this.components.push(customTime); + this.redraw(); + + return customBarId; }; /** - * 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 + * 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 */ - ItemSet.itemSetFromTarget = function (event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty("timeline-itemset")) { - return target["timeline-itemset"]; - } - target = target.parentNode; - } + Core.prototype.removeCustomTime = function (id) { + var me = this; - return null; + 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(); + } + } + }); }; - module.exports = ItemSet; -/***/ }, -/* 33 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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() || []; + }; - "use strict"; - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var Component = __webpack_require__(25); /** - * Legend for Graph2d + * 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} */ - 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); - } + Core.prototype.clear = function (what) { + // clear items + if (!what || what.items) { + this.setItems(null); + } - Legend.prototype = new Component(); + // clear groups + if (!what || what.groups) { + this.setGroups(null); + } - Legend.prototype.clear = function () { - this.groups = {}; - this.amountOfGroups = 0; - }; + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + return component.setOptions(component.defaultOptions); + }); - Legend.prototype.addGroup = function (label, graphOptions) { - if (!this.groups.hasOwnProperty(label)) { - this.groups[label] = graphOptions; + this.setOptions(this.defaultOptions); // this will also do a redraw } - this.amountOfGroups += 1; }; - Legend.prototype.updateGroup = function (label, graphOptions) { - this.groups[label] = graphOptions; - }; + /** + * Set Core window such that it fits all items + * @param {Object} [options] Available options: + * `animate: boolean | number` + * If true (default), the range is animated + * smoothly to the new window. + * If a number, the number is taken as duration + * for the animation. Default duration is 500 ms. + */ + Core.prototype.fit = function (options) { + var range = this._getDataRange(); - Legend.prototype.removeGroup = function (label) { - if (this.groups.hasOwnProperty(label)) { - delete this.groups[label]; - this.amountOfGroups -= 1; + // skip range set if there is no start and end date + if (range.start === null && range.end === null) { + return; } - }; - 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"; + var animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(range.start, range.end, animate); + }; - this.dom.textArea = document.createElement("div"); - this.dom.textArea.className = "legendText"; - this.dom.textArea.style.position = "relative"; - this.dom.textArea.style.top = "0px"; + /** + * 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(); - 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%"; + // 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.dom.frame.appendChild(this.svg); - this.dom.frame.appendChild(this.dom.textArea); + return { + start: start, + end: end + }; }; /** - * Hide the component from the DOM + * 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. */ - Legend.prototype.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); + Core.prototype.setWindow = function (start, end, options) { + var animate; + if (arguments.length == 1) { + var range = arguments[0]; + animate = range.animate !== undefined ? range.animate : true; + this.range.setRange(range.start, range.end, animate); + } else { + animate = options && options.animate !== undefined ? options.animate : true; + this.range.setRange(start, end, animate); } }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * 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. */ - Legend.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } + 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); }; - Legend.prototype.setOptions = function (options) { - var fields = ["enabled", "orientation", "icons", "left", "right"]; - util.selectiveDeepExtend(fields, this.options, options); + /** + * Get the visible window + * @return {{start: Date, end: Date}} Visible range + */ + Core.prototype.getWindow = function () { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; }; - 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++; - } - } - } + /** + * Force a redraw. Can be overridden by implementations of Core + */ + Core.prototype.redraw = function () { + this._redraw(); + }; - 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 = ""; - } + /** + * Redraw for internal use. Redraws all components. See also the public + * method redraw. + * @protected + */ + Core.prototype._redraw = function () { + var resized = false; + var options = this.options; + var props = this.props; + var dom = this.dom; - 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 = ""; - } + if (!dom) return; // when destroyed - 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(); - } + DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); - var content = ""; - for (var groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { - content += this.groups[groupId].content + "
"; - } - } - } - this.dom.textArea.innerHTML = content; - this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + "px"; + // 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"); } - }; - - Legend.prototype.drawLegendIcons = function () { - if (this.dom.frame.parentNode) { - DOMutil.prepareElements(this.svgElements); - var padding = window.getComputedStyle(this.dom.frame).paddingTop; - var iconOffset = Number(padding.replace("px", "")); - var x = iconOffset; - var iconWidth = this.options.iconSize; - var iconHeight = 0.75 * this.options.iconSize; - var y = iconOffset + 0.5 * iconHeight + 3; - this.svg.style.width = iconWidth + 5 + iconOffset + "px"; + // 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, ""); - 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; - } - } - } + // 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; - DOMutil.cleanupElements(this.svgElements); + // 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; } - }; - - module.exports = Legend; -/***/ }, -/* 34 */ -/***/ function(module, exports, __webpack_require__) { + // 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; - "use strict"; + // TODO: compensate borders when any of the panels is empty. - var util = __webpack_require__(1); - var DOMutil = __webpack_require__(2); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Component = __webpack_require__(25); - var DataAxis = __webpack_require__(28); - var GraphGroup = __webpack_require__(29); - var Legend = __webpack_require__(33); - var BarGraphFunctions = __webpack_require__(46); + // 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"); - var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items + // 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; - /** - * 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; + // 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; - 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: {} - } - }; + // 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"; - // 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; + 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"; - var me = this; - this.itemsData = null; // DataSet - this.groupsData = null; // DataSet + // 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"; - // 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); - } - }; + // 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(); - // 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); - } - }; + // 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"; - 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 + // show shadows when vertical scrolling is available + var visibilityTop = this.props.scrollTop == 0 ? "hidden" : ""; + var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? "hidden" : ""; + dom.shadowTop.style.visibility = visibilityTop; + dom.shadowBottom.style.visibility = visibilityBottom; + dom.shadowTopLeft.style.visibility = visibilityTop; + dom.shadowBottomLeft.style.visibility = visibilityBottom; + dom.shadowTopRight.style.visibility = visibilityTop; + dom.shadowBottomRight.style.visibility = visibilityBottom; - this.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); + // redraw all components + this.components.forEach(function (component) { + resized = component.redraw() || resized; }); + if (resized) { + // keep repainting until all sizes are settled + var MAX_REDRAWS = 3; // maximum number of consecutive redraws + if (this.redrawCount < MAX_REDRAWS) { + this.redrawCount++; + this._redraw(); + } else { + console.log("WARNING: infinite loop in redraw?"); + } + this.redrawCount = 0; + } - // create 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.emit("finishedRedraw"); + }; - LineGraph.prototype = new Component(); + // TODO: deprecated since version 1.1.0, remove some day + Core.prototype.repaint = function () { + throw new Error("Function repaint is deprecated. Use redraw instead."); + }; /** - * Create the HTML DOM for the ItemSet + * 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._create = function () { - var frame = document.createElement("div"); - frame.className = "LineGraph"; - this.dom.frame = frame; - - // create svg element for graph drawing. - this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - this.svg.style.position = "relative"; - this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; - this.svg.style.display = "block"; - frame.appendChild(this.svg); - - // data axis - this.options.dataAxis.orientation = "left"; - this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - - this.options.dataAxis.orientation = "right"; - this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - delete this.options.dataAxis.orientation; - - // legends - this.legendLeft = new Legend(this.body, this.options.legend, "left", this.options.groups); - this.legendRight = new Legend(this.body, this.options.legend, "right", this.options.groups); + Core.prototype.setCurrentTime = function (time) { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); + } - this.show(); + this.currentTime.setCurrentTime(time); }; /** - * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. - * @param {object} options + * Get the current time. + * Only applicable when option `showCurrentTime` is true. + * @return {Date} Returns the current time. */ - 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); - } + Core.prototype.getCurrentTime = function () { + if (!this.currentTime) { + throw new Error("Option showCurrentTime must be true"); } - // this is used to redraw the graph if the visibility of the groups is changed. - if (this.dom.frame) { - this.redraw(true); - } + return this.currentTime.getCurrentTime(); }; /** - * Hide the component from the DOM + * 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.hide = function () { - // remove the frame containing the items - if (this.dom.frame.parentNode) { - this.dom.frame.parentNode.removeChild(this.dom.frame); - } + // TODO: move this function to Range + Core.prototype._toTime = function (x) { + return DateUtil.toTime(this, x, this.props.center.width); }; - /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * 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 */ - LineGraph.prototype.show = function () { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } + // 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); }; - /** - * Set items - * @param {vis.DataSet | null} items + * 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 */ - LineGraph.prototype.setItems = function (items) { - var me = this, - ids, - oldItemsData = this.itemsData; + // TODO: move this function to Range + Core.prototype._toScreen = function (time) { + return DateUtil.toScreen(this, time, this.props.center.width); + }; - // 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); - }); - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + /** + * 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; + }; - 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); + /** + * Initialize watching when option autoResize is true + * @private + */ + Core.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } else { + this._stopAutoResize(); } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); }; - /** - * Set groups - * @param {vis.DataSet} groups + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ - LineGraph.prototype.setGroups = function (groups) { + Core.prototype._startAutoResize = function () { 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._stopAutoResize(); - // remove all drawn groups - ids = this.groupsData.getIds(); - this.groupsData = null; - this._onRemoveGroups(ids); // note: this will cause a redraw - } + this._onResize = function () { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; + } - // 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 (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 (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + me.emit("change"); + } + } + }; + + // add event listener to window resize + util.addEventListener(window, "resize", this._onResize); - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); - } - this._onUpdate(); + this.watchTimer = setInterval(this._onResize, 1000); }; - /** - * Update the data - * @param [ids] + * Stop watching for a resize of the frame. * @private */ - LineGraph.prototype._onUpdate = function (ids) { - this._updateUngrouped(); - this._updateAllGroupData(); - //this._updateGraph(); - this.redraw(true); - }; - LineGraph.prototype._onAdd = function (ids) { - this._onUpdate(ids); - }; - LineGraph.prototype._onRemove = function (ids) { - this._onUpdate(ids); - }; - LineGraph.prototype._onUpdateGroups = function (groupIds) { - for (var i = 0; i < groupIds.length; i++) { - var group = this.groupsData.get(groupIds[i]); - this._updateGroup(group, groupIds[i]); + Core.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; } - //this._updateGraph(); - this.redraw(true); - }; - LineGraph.prototype._onAddGroups = function (groupIds) { - this._onUpdateGroups(groupIds); + // remove event listener on window.resize + util.removeEventListener(window, "resize", this._onResize); + this._onResize = null; }; - /** - * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph - * @param {Array} groupIds + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ - LineGraph.prototype._onRemoveGroups = function (groupIds) { - for (var i = 0; i < groupIds.length; i++) { - if (this.groups.hasOwnProperty(groupIds[i])) { - if (this.groups[groupIds[i]].options.yAxisOrientation == "right") { - this.yAxisRight.removeGroup(groupIds[i]); - this.legendRight.removeGroup(groupIds[i]); - this.legendRight.redraw(); - } else { - this.yAxisLeft.removeGroup(groupIds[i]); - this.legendLeft.removeGroup(groupIds[i]); - this.legendLeft.redraw(); - } - delete this.groups[groupIds[i]]; - } - } - this._updateUngrouped(); - //this._updateGraph(); - this.redraw(true); + Core.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; }; - /** - * update a group object with the group dataset entree - * - * @param group - * @param groupId + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ - LineGraph.prototype._updateGroup = function (group, groupId) { - if (!this.groups.hasOwnProperty(groupId)) { - this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles); - if (this.groups[groupId].options.yAxisOrientation == "right") { - this.yAxisRight.addGroup(groupId, this.groups[groupId]); - this.legendRight.addGroup(groupId, this.groups[groupId]); - } else { - this.yAxisLeft.addGroup(groupId, this.groups[groupId]); - this.legendLeft.addGroup(groupId, this.groups[groupId]); - } - } else { - this.groups[groupId].update(group); - if (this.groups[groupId].options.yAxisOrientation == "right") { - this.yAxisRight.updateGroup(groupId, this.groups[groupId]); - this.legendRight.updateGroup(groupId, this.groups[groupId]); - } else { - this.yAxisLeft.updateGroup(groupId, this.groups[groupId]); - this.legendLeft.updateGroup(groupId, this.groups[groupId]); + 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; } - this.legendLeft.redraw(); - this.legendRight.redraw(); - }; + // 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 updates all groups, it is used when there is an update the the itemset. - * + * Get the current scrollTop + * @returns {number} scrollTop * @private */ - LineGraph.prototype._updateAllGroupData = function () { - if (this.itemsData != null) { - var groupsContent = {}; - var groupId; - for (groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - groupsContent[groupId] = []; - } - } - for (var itemId in this.itemsData._data) { - if (this.itemsData._data.hasOwnProperty(itemId)) { - var item = this.itemsData._data[itemId]; - if (groupsContent[item.group] === undefined) { - throw new Error("Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups."); - } - item.x = util.convert(item.x, "Date"); - groupsContent[item.group].push(item); - } - } - for (groupId in this.groups) { - if (this.groups.hasOwnProperty(groupId)) { - this.groups[groupId].setItems(groupsContent[groupId]); - } - } - } + Core.prototype._getScrollTop = function () { + return this.props.scrollTop; }; + module.exports = Core; + +/***/ }, +/* 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 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 /** - * 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 + * 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 */ - 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; - } - } + 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.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); + }, + update: function (event, params, senderId) { + me._onUpdate(params.items); + }, + remove: function (event, params, senderId) { + me._onRemove(params.items); } + }; - 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); + // 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); } - } else { - delete this.groups[UNGROUPED]; - this.legendLeft.removeGroup(UNGROUPED); - this.legendRight.removeGroup(UNGROUPED); - this.yAxisLeft.removeGroup(UNGROUPED); - this.yAxisRight.removeGroup(UNGROUPED); - } + }; - this.legendLeft.redraw(); - this.legendRight.redraw(); - }; + this.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); + } + + ItemSet.prototype = new Component(); + // available item types will be registered here + ItemSet.types = { + background: BackgroundItem, + box: BoxItem, + range: RangeItem, + point: PointItem + }; /** - * Redraw the component, mandatory function - * @return {boolean} Returns true if the component is resized + * Create the HTML DOM for the ItemSet */ - LineGraph.prototype.redraw = function (forceGraphUpdate) { - var resized = false; + ItemSet.prototype._create = function () { + var frame = document.createElement("div"); + frame.className = "itemset"; + frame["timeline-itemset"] = this; + this.dom.frame = frame; - // calculate actual size and position - this.props.width = this.dom.frame.offsetWidth; - this.props.height = this.body.domProps.centerContainer.height; + // create background panel + var background = document.createElement("div"); + background.className = "background"; + frame.appendChild(background); + this.dom.background = background; - // update the graph if there is no lastWidth or with, used for the initial draw - if (this.lastWidth === undefined && this.props.width) { - forceGraphUpdate = true; - } + // create foreground panel + var foreground = document.createElement("div"); + foreground.className = "foreground"; + frame.appendChild(foreground); + this.dom.foreground = foreground; - // check if this component is resized - resized = this._isResized() || resized; + // create axis panel + var axis = document.createElement("div"); + axis.className = "axis"; + this.dom.axis = axis; - // 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; + // create labelset + var labelSet = document.createElement("div"); + labelSet.className = "labelset"; + this.dom.labelSet = labelSet; + // create ungrouped Group + this._updateUngrouped(); - // 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); + // create background Group + var backgroundGroup = new BackgroundGroup(BACKGROUND, null, this); + backgroundGroup.show(); + this.groups[BACKGROUND] = backgroundGroup; - // 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; + // attach event listeners + // Note: we bind to the centerContainer for the case where the height + // of the center container is larger than of the ItemSet, so we + // can click in the empty area to create a new item or deselect an item. + this.hammer = new Hammer(this.body.dom.centerContainer); + + // drag items when selected + this.hammer.on("hammer.input", (function (event) { + if (event.isFirst) { + this._onTouch(event); } - } + }).bind(this)); + this.hammer.on("panstart", this._onDragStart.bind(this)); + this.hammer.on("panmove", this._onDrag.bind(this)); + this.hammer.on("panend", this._onDragEnd.bind(this)); - // 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"; + // single select (or unselect) when tapping an item + this.hammer.on("tap", this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.hammer.on("press", this._onMultiSelectItem.bind(this)); + + // add item on doubletap + this.hammer.on("doubletap", this._onAddItem.bind(this)); + + // attach to the DOM + this.show(); + }; + + /** + * Set options for the ItemSet. Existing options will be extended/overwritten. + * @param {Object} [options] The following options are available: + * {String} type + * Default type for the items. Choose from 'box' + * (default), 'point', 'range', or 'background'. + * The default style can be overwritten by + * individual items. + * {String} align + * Alignment for the items, only applicable for + * BoxItem. Choose 'center' (default), 'left', or + * 'right'. + * {String} orientation + * 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); + + 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); + } + } + } } - this.updateSVGheight = false; - } else { - this.svg.style.height = ("" + this.options.graphHeight).replace("px", "") + "px"; - } - // 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"; + 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); } } - } - this.legendLeft.redraw(); - this.legendRight.redraw(); - return resized; + // callback functions + var addCallback = (function (name) { + var fn = options[name]; + if (fn) { + if (!(fn instanceof Function)) { + throw new Error("option " + name + " must be a function " + name + "(item, callback)"); + } + this.options[name] = fn; + } + }).bind(this); + ["onAdd", "onUpdate", "onRemove", "onMove", "onMoving"].forEach(addCallback); + + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); + } }; + /** + * 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; + + if (options && options.refreshItems) { + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); + } + }; /** - * Update and redraw the graph. - * + * Destroy the ItemSet */ - 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; + ItemSet.prototype.destroy = function () { + this.hide(); + this.setItems(null); + this.setGroups(null); - // 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); + this.hammer = null; - // apply sampling, if disabled, it will pass through this function. - this._applySampling(groupIds, groupsData); + this.body = null; + this.conversion = null; + }; - // we transform the X coordinates to detect collisions - for (i = 0; i < groupIds.length; i++) { - preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); - } + /** + * Hide the component from the DOM + */ + ItemSet.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } - // now all needed data has been collected we start the processing. - this._getYRanges(groupIds, preprocessedGroupData, groupRanges); + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } - // 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; + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + } + }; - // 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 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); + } - // 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); - } - } + // show axis with dots + if (!this.dom.axis.parentNode) { + this.body.dom.backgroundVertical.appendChild(this.dom.axis); } - // cleanup unused svg elements - DOMutil.cleanupElements(this.svgElements); - return false; + // show labelset containing labels + if (!this.dom.labelSet.parentNode) { + this.body.dom.left.appendChild(this.dom.labelSet); + } }; - /** - * 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 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. */ - 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); - } - } - } - } + ItemSet.prototype.setSelection = function (ids) { + var i, ii, id, item; + + if (ids == undefined) ids = []; + if (!Array.isArray(ids)) ids = [ids]; + + // unselect currently selected items + for (i = 0, ii = this.selection.length; i < ii; i++) { + id = this.selection[i]; + item = this.items[id]; + if (item) item.unselect(); + } + + // select items + this.selection = []; + for (i = 0, ii = ids.length; i < ii; i++) { + id = ids[i]; + item = this.items[id]; + if (item) { + this.selection.push(id); + item.select(); } } }; + /** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ + ItemSet.prototype.getSelection = function () { + return this.selection.concat([]); + }; /** - * - * @param groupIds - * @param groupsData - * @private + * Get the id's of the currently visible items. + * @returns {Array} The ids of the visible items */ - 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; + 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); - // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop - // of width changing of the yAxis. - var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x); - var pointsPerPixel = amountOfPoints / xDistance; - increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel))); + var ids = []; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + var group = this.groups[groupId]; + var rawVisibleItems = group.visibleItems; - var sampledData = []; - for (var j = 0; j < amountOfPoints; j += increment) { - sampledData.push(dataContainer[j]); - } - groupsData[groupIds[i]] = sampledData; + // 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; + }; /** - * - * - * @param {array} groupIds - * @param {object} groupsData - * @param {object} groupRanges | this is being filled here + * Deselect a selected item + * @param {String | Number} id * @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]); - } - } + 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; } - - // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. - BarGraphFunctions.getStackedBarYRange(barCombinedDataLeft, groupRanges, groupIds, "__barchartLeft", "left"); - BarGraphFunctions.getStackedBarYRange(barCombinedDataRight, groupRanges, groupIds, "__barchartRight", "right"); } }; - /** - * this 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 + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - 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; - } - } + 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; - // 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; + // 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; - 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; - } - } - } - } + // update class name + frame.className = "itemset" + (editable ? " editable" : ""); - 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; + // reorder the groups (if needed) + resized = this._orderGroups() || 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; - } + // 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; - 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; - } + var restack = this.stackDirty; + var firstGroup = this._firstGroup(); + var firstMargin = { + item: margin.item, + axis: margin.axis + }; + var nonFirstMargin = { + item: margin.item, + axis: margin.item.vertical / 2 + }; + var height = 0; + var minHeight = margin.axis + margin.item.vertical; - // 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); - } + // redraw the background group + this.groups[BACKGROUND].redraw(range, nonFirstMargin, restack); + + // 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; + + // update frame height + frame.style.height = asSize(height); + + // calculate actual size + this.props.width = frame.offsetWidth; + this.props.height = height; + + // reposition axis + this.dom.axis.style.top = asSize(orientation == "top" ? this.body.domProps.top.height + this.body.domProps.border.top : this.body.domProps.top.height + this.body.domProps.centerContainer.height); + this.dom.axis.style.left = "0"; + + // check if this component is resized + resized = this._isResized() || resized; return resized; }; - /** - * 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} + * Get the first group, aligned with the axis + * @return {Group | null} firstGroup * @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; + ItemSet.prototype._firstGroup = function () { + var firstGroupIndex = this.options.orientation == "top" ? 0 : this.groupIds.length - 1; + var firstGroupId = this.groupIds[firstGroupIndex]; + var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED]; + + return firstGroup || null; + }; + + /** + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. + * @protected + */ + ItemSet.prototype._updateUngrouped = function () { + var ungrouped = this.groups[UNGROUPED]; + var background = this.groups[BACKGROUND]; + var item, itemId; + + if (this.groupsData) { + // remove the group holding all ungrouped items + if (ungrouped) { + ungrouped.hide(); + delete this.groups[UNGROUPED]; + + for (itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + item = this.items[itemId]; + item.parent && item.parent.remove(item); + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + group && group.add(item) || item.hide(); + } + } } } else { - if (!axis.dom.frame.parentNode && axis.hidden == true) { - axis.show(); - changed = true; + // 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(); } } - return changed; }; - /** - * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the - * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for - * the yAxis. - * - * @param datapoints - * @returns {Array} - * @private + * Get the element for the labelset + * @return {HTMLElement} labelSet */ - 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; + ItemSet.prototype.getLabelSet = function () { + return this.dom.labelSet; }; - /** - * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the - * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for - * the yAxis. - * - * @param datapoints - * @param group - * @returns {Array} - * @private + * Set items + * @param {vis.DataSet | null} items */ - 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; + ItemSet.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"); } - 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 }); + 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); } - group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - return extractedData; + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + + // update the group holding all ungrouped items + this._updateUngrouped(); + } }; + /** + * Get the current items + * @returns {vis.DataSet | null} + */ + ItemSet.prototype.getItems = function () { + return this.itemsData; + }; - module.exports = LineGraph; + /** + * Set groups + * @param {vis.DataSet} groups + */ + ItemSet.prototype.setGroups = function (groups) { + var me = this, + ids; -/***/ }, -/* 35 */ -/***/ function(module, exports, __webpack_require__) { + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - "use strict"; + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - var util = __webpack_require__(1); - var Component = __webpack_require__(25); - var TimeStep = __webpack_require__(19); - var DateUtil = __webpack_require__(15); - var moment = __webpack_require__(40); + // 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"); + } - /** - * A horizontal time axis - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body - * @param {Object} [options] See TimeAxis.setOptions for the available - * options. - * @constructor TimeAxis - * @extends Component - */ - function TimeAxis(body, options) { - this.dom = { - foreground: null, - lines: [], - majorTexts: [], - minorTexts: [], - redundant: { - lines: [], - majorTexts: [], - minorTexts: [] - } - }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 - }, - lineTop: 0 - }; + 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.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); + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } - this.body = body; + // update the group holding all ungrouped items + this._updateUngrouped(); - // create the HTML DOM - this._create(); + // update the order of all items in each group + this._order(); - this.setOptions(options); - } + this.body.emitter.emit("change", { queue: true }); + }; - TimeAxis.prototype = new Component(); + /** + * Get the current groups + * @returns {vis.DataSet | null} groups + */ + ItemSet.prototype.getGroups = function () { + return this.groupsData; + }; /** - * Set options for the TimeAxis. - * Parameters will be merged in current options. - * @param {Object} options Available options: - * {string} [orientation] - * {boolean} [showMinorLabels] - * {boolean} [showMajorLabels] + * Remove an item by its id + * @param {String | Number} id */ - TimeAxis.prototype.setOptions = function (options) { - if (options) { - // copy all options that we know - util.selectiveExtend(["orientation", "showMinorLabels", "showMajorLabels", "hiddenDates", "format", "timeAxis"], this.options, options); + ItemSet.prototype.removeItem = function (id) { + var item = this.itemsData.get(id), + dataset = this.itemsData.getDataSet(); - // 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); + if (item) { + // confirm deletion + this.options.onRemove(item, function (item) { + if (item) { + // remove by id here, it is possible that an item has no id defined + // itself, so better not delete by the item itself + dataset.remove(id); } - } + }); } }; /** - * Create the HTML DOM for the TimeAxis + * Get the time of an item based on it's data and options.type + * @param {Object} itemData + * @returns {string} Returns the type + * @private */ - TimeAxis.prototype._create = function () { - this.dom.foreground = document.createElement("div"); - this.dom.background = document.createElement("div"); - - this.dom.foreground.className = "timeaxis foreground"; - this.dom.background.className = "timeaxis background"; + ItemSet.prototype._getType = function (itemData) { + return itemData.type || this.options.type || (itemData.end ? "range" : "box"); }; + /** - * Destroy the TimeAxis + * Get the group id for an item + * @param {Object} itemData + * @returns {string} Returns the groupId + * @private */ - 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._getGroupId = function (itemData) { + var type = this._getType(itemData); + if (type == "background" && itemData.group == undefined) { + return BACKGROUND; + } else { + return this.groupsData ? itemData.group : UNGROUPED; } - - this.body = null; }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Handle updated items + * @param {Number[]} ids + * @protected */ - TimeAxis.prototype.redraw = function () { - var options = this.options; - var props = this.props; - var foreground = this.dom.foreground; - var background = this.dom.background; - - // determine the correct parent DOM element (depending on option orientation) - var parent = options.orientation == "top" ? this.body.dom.top : this.body.dom.bottom; - var parentChanged = foreground.parentNode !== parent; + ItemSet.prototype._onUpdate = function (ids) { + var me = this; - // calculate character width and height - this._calculateCharSize(); + ids.forEach(function (id) { + var itemData = me.itemsData.get(id, me.itemOptions); + var item = me.items[id]; + var type = me._getType(itemData); - // 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; + var constructor = ItemSet.types[type]; - // 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; + 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); + } + } - 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 + 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 + "\""); + } + } + }); - // 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); + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit("change", { queue: true }); + }; - foreground.style.height = this.props.height + "px"; + /** + * Handle added items + * @param {Number[]} ids + * @protected + */ + ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; - this._repaintLabels(); + /** + * 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); + } + }); - // 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); + if (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit("change", { queue: true }); } - - return this._isResized() || parentChanged; }; /** - * Repaint major and minor text labels and vertical grid lines + * Update the order of item in all groups * @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; + 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(); + }); + }; - // 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 = []; + /** + * Handle updated groups + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onUpdateGroups = function (ids) { + this._onAddGroups(ids); + }; - var cur; - var x = 0; - var isMajor; - var xPrev = 0; - var width = 0; - var prevLine; - var xFirstMajorLabel = undefined; - var max = 0; - var className; + /** + * Handle changed groups (added or updated) + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onAddGroups = function (ids) { + var me = this; - step.first(); - while (step.hasNext() && max < 1000) { - max++; + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; - cur = step.getCurrent(); - isMajor = step.isMajor(); - className = step.getClassName(); + if (!group) { + // check for reserved ids + if (id == UNGROUPED || id == BACKGROUND) { + throw new Error("Illegal group id. " + id + " is a reserved id."); + } - xPrev = x; - x = this.body.util.toScreen(cur); - width = x - xPrev; - if (prevLine) { - prevLine.style.width = width + "px"; - } + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); - if (this.options.showMinorLabels) { - this._repaintMinorText(x, step.getLabelMinor(), orientation, className); - } + group = new Group(id, groupData, me); + me.groups[id] = group; - if (isMajor && this.options.showMajorLabels) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; + // 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); + } } - this._repaintMajorText(x, step.getLabelMajor(), orientation, className); } - prevLine = this._repaintMajorLine(x, orientation, className); + + group.order(); + group.show(); } else { - prevLine = this._repaintMinorLine(x, orientation, className); + // update group + group.setData(groupData); } + }); - 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 + this.body.emitter.emit("change", { queue: true }); + }; - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText, orientation, className); - } - } + /** + * Handle removed groups + * @param {Number[]} ids + * @private + */ + ItemSet.prototype._onRemoveGroups = function (ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; - // Cleanup leftover DOM elements from the redundant list - util.forEach(this.dom.redundant, function (arr) { - while (arr.length) { - var elem = arr.pop(); - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } + if (group) { + group.hide(); + delete groups[id]; } }); + + this.markDirty(); + + this.body.emitter.emit("change", { queue: true }); }; /** - * 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 + * Reorder the groups if needed + * @return {boolean} changed * @private */ - TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); + ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder + }); - if (!label) { - // create new label - var content = document.createTextNode(""); - label = document.createElement("div"); - label.appendChild(content); - this.dom.foreground.appendChild(label); + 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(); + }); + + this.groupIds = groupIds; + } + + return changed; + } else { + return false; } - this.dom.minorTexts.push(label); + }; - label.childNodes[0].nodeValue = text; + /** + * Add a new item + * @param {Item} item + * @private + */ + ItemSet.prototype._addItem = function (item) { + this.items[item.id] = item; - 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 + // add to group + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); }; /** - * 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 + * Update an existing item + * @param {Item} item + * @param {Object} itemData * @private */ - TimeAxis.prototype._repaintMajorText = function (x, text, orientation, className) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); + ItemSet.prototype._updateItem = function (item, itemData) { + var oldGroupId = item.data.group; - 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); + // update the items data (will redraw the item when displayed) + item.setData(itemData); - label.childNodes[0].nodeValue = text; - label.className = "text major " + className; - //label.title = title; // TODO: this is a heavy operation + // update group + if (oldGroupId != item.data.group) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); - label.style.top = orientation == "top" ? "0" : this.props.minorLabelHeight + "px"; - label.style.left = x + "px"; + var groupId = this._getGroupId(item.data); + var group = this.groups[groupId]; + if (group) group.add(item); + } }; /** - * 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 + * 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 */ - TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) { - // reuse redundant line - var line = this.dom.redundant.lines.shift(); - if (!line) { - // create vertical line - line = document.createElement("div"); - this.dom.background.appendChild(line); - } - this.dom.lines.push(line); + ItemSet.prototype._removeItem = function (item) { + // remove from DOM + item.hide(); - 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"; + // remove from items + delete this.items[item.id]; - line.className = "grid vertical minor " + className; + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); - return line; + // remove from group + item.parent && item.parent.remove(item); }; /** - * 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 + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} * @private */ - TimeAxis.prototype._repaintMajorLine = function (x, orientation, className) { - // reuse redundant line - var line = this.dom.redundant.lines.shift(); - if (!line) { - // create vertical line - line = document.createElement("div"); - this.dom.background.appendChild(line); - } - this.dom.lines.push(line); + ItemSet.prototype._constructByEndArray = function (array) { + var endArray = []; - var props = this.props; - if (orientation == "top") { - line.style.top = "0"; - } else { - line.style.top = this.body.domProps.top.height + "px"; + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof RangeItem) { + endArray.push(array[i]); + } } - line.style.left = x - props.majorLineWidth / 2 + "px"; - line.style.height = props.majorLineHeight + "px"; - - line.className = "grid vertical major " + className; + return endArray; + }; - return line; + /** + * 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; }; /** - * 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. + * Start dragging the selected events + * @param {Event} event * @private */ - TimeAxis.prototype._calculateCharSize = function () { - // Note: We calculate char size with every redraw. Size may change, for - // example when any of the timelines parents had display:none for example. + ItemSet.prototype._onDragStart = function (event) { + if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { + return; + } - // 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 item = this.touchParams.item || null; + var me = this; + var props; - 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; + if (item && item.selected) { + var dragLeftItem = this.touchParams.dragLeftItem; + var dragRightItem = this.touchParams.dragRightItem; - // determine the char width and height on the major axis - if (!this.dom.measureCharMajor) { - this.dom.measureCharMajor = document.createElement("DIV"); - this.dom.measureCharMajor.className = "text major measure"; - this.dom.measureCharMajor.style.position = "absolute"; + if (dragLeftItem) { + props = { + item: dragLeftItem, + initialX: event.center.x + }; - this.dom.measureCharMajor.appendChild(document.createTextNode("0")); - this.dom.foreground.appendChild(this.dom.measureCharMajor); + if (me.options.editable.updateTime) { + props.start = item.data.start.valueOf(); + } + if (me.options.editable.updateGroup) { + if ("group" in item.data) props.group = item.data.group; + } + + this.touchParams.itemProps = [props]; + } else if (dragRightItem) { + props = { + item: dragRightItem, + initialX: event.center.x + }; + + if (me.options.editable.updateTime) { + props.end = item.data.end.valueOf(); + } + if (me.options.editable.updateGroup) { + if ("group" in item.data) props.group = item.data.group; + } + + this.touchParams.itemProps = [props]; + } else { + this.touchParams.itemProps = this.getSelection().map(function (id) { + var item = me.items[id]; + var props = { + item: item, + initialX: event.center.x + }; + + 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; + }); + } + + event.stopPropagation(); + event.preventDefault(); } - this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; - this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; }; - module.exports = TimeAxis; + /** + * Drag selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDrag = function (event) { + event.preventDefault(); -/***/ }, -/* 36 */ -/***/ function(module, exports, __webpack_require__) { + 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(); - "use strict"; + // move + this.touchParams.itemProps.forEach(function (props) { + var newProps = {}; + var current = me.body.util.toTime(event.center.x - xOffset); + var initial = me.body.util.toTime(props.initialX - xOffset); + var offset = current - initial; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + if ("start" in props) { + var start = new Date(props.start + offset); + newProps.start = snap ? snap(start, scale, step) : start; + } - // Load custom shapes into CanvasRenderingContext2D - __webpack_require__(48); + 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); + } - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(41); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var dotparser = __webpack_require__(38); - var gephiParser = __webpack_require__(39); - var Images = __webpack_require__(37); - var Activator = __webpack_require__(49); + 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); + } + }); + }); - var Groups = _interopRequire(__webpack_require__(50)); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit("change"); - var NodesHandler = _interopRequire(__webpack_require__(51)); + event.stopPropagation(); + } + }; - var EdgesHandler = _interopRequire(__webpack_require__(52)); + /** + * Update an items properties + * @param {Item} item + * @param {Object} props Can contain properties start, end, and group. + * @private + */ + 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 PhysicsEngine = _interopRequire(__webpack_require__(53)); + /** + * Move an item to another group + * @param {Item} item + * @param {String | Number} groupId + * @private + */ + ItemSet.prototype._moveToGroup = function (item, groupId) { + var group = this.groups[groupId]; + if (group && group.groupId != item.data.group) { + var oldGroup = item.parent; + oldGroup.remove(item); + oldGroup.order(); + group.add(item); + group.order(); - var ClusterEngine = _interopRequire(__webpack_require__(54)); + item.data.group = group.groupId; + } + }; - var CanvasRenderer = _interopRequire(__webpack_require__(55)); + /** + * End of dragging selected items + * @param {Event} event + * @private + */ + ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + // prepare a change set for the changed items + var changes = [], + me = this, + dataset = this.itemsData.getDataSet(); - var Canvas = _interopRequire(__webpack_require__(56)); + 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 View = _interopRequire(__webpack_require__(57)); + 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 InteractionHandler = _interopRequire(__webpack_require__(58)); + // 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); - var SelectionHandler = _interopRequire(__webpack_require__(59)); + me.stackDirty = true; // force re-stacking of all items next redraw + me.body.emitter.emit("change"); + } + }); + } + }); - var LayoutEngine = _interopRequire(__webpack_require__(60)); + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); + } - var ManipulationSystem = _interopRequire(__webpack_require__(61)); + event.stopPropagation(); + } + }; /** - * @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 + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private */ - function Network(container, data, options) { - var _this = this; - if (!(this instanceof Network)) { - throw new SyntaxError("Constructor must be called with the new operator"); + 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; } - // set constant values - this.options = {}; - this.defaultOptions = { - clickToUse: false - }; - util.extend(this.options, this.defaultOptions); + var oldSelection = this.getSelection(); - // containers for nodes and edges - this.body = { - nodes: {}, - nodeIndices: [], - edges: {}, - edgeIndices: [], - data: { - nodes: null, // A DataSet or DataView - edges: null // A DataSet or DataView - }, - functions: { - createNode: function () {}, - createEdge: function () {}, - getPointer: function () {} - }, - emitter: { - on: this.on.bind(this), - off: this.off.bind(this), - emit: this.emit.bind(this), - once: this.once.bind(this) - }, - eventListeners: { - onTap: function () {}, - onTouch: function () {}, - onDoubleTap: function () {}, - onHold: function () {}, - onDragStart: function () {}, - onDrag: function () {}, - onDragEnd: function () {}, - onMouseWheel: function () {}, - onPinch: function () {}, - onMouseMove: function () {}, - onRelease: function () {} - }, - container: container, - view: { - scale: 1, - translation: { x: 0, y: 0 } - } - }; + var item = ItemSet.itemFromTarget(event); + var selection = item ? [item.id] : []; + this.setSelection(selection); + + var newSelection = this.getSelection(); - // bind the event listeners - this.bindEventListeners(); + // 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 + }); + } + }; - // setting up all modules - var images = new Images(function () { - return _this.body.emitter.emit("_requestRedraw"); - }); // object with images + /** + * Handle creation and updates of an item on double tap + * @param event + * @private + */ + ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; - this.groups = new Groups(); // object with groups - this.canvas = new Canvas(this.body); // DOM handler - this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler - this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key - this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms - this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into - this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations - this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout - this.clustering = new ClusterEngine(this.body); // clustering api - this.manipulation = new ManipulationSystem(this.body, this.canvas, this.selectionHandler); // data manipulation system + var me = this, + snap = this.options.snap || null, + item = ItemSet.itemFromTarget(event); - this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options - this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options + if (item) { + // update item - // create the DOM elements - this.canvas.create(); + // 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(); - // apply options - this.setOptions(options); + var newItem = { + start: snap ? snap(start, scale, step) : start, + content: "new item" + }; - // load data (the disable start variable will be the same as the enabled clustering) - this.setData(data); - } + // 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; + } - // Extend Network with an Emitter mixin - Emitter(Network.prototype); + newItem[this.itemsData._fieldId] = util.randomUUID(); + var group = this.groupFromTarget(event); + if (group) { + newItem.group = group.groupId; + } + // execute async handler to customize (or cancel) adding an item + this.options.onAdd(newItem, function (item) { + if (item) { + me.itemsData.getDataSet().add(item); + // TODO: need to trigger a redraw? + } + }); + } + }; /** - * Set options - * @param {Object} options + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private */ - Network.prototype.setOptions = function (options) { - if (options !== undefined) { - // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. - options = this.layoutEngine.setOptions(options.layout, options); + ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; - this.groups.setOptions(options.groups); - this.nodesHandler.setOptions(options.nodes); - this.edgesHandler.setOptions(options.edges); - this.physics.setOptions(options.physics); - this.canvas.setOptions(options.canvas); - this.renderer.setOptions(options.rendering); - this.view.setOptions(options.view); - this.interactionHandler.setOptions(options.interaction); - this.selectionHandler.setOptions(options.selection); - this.clustering.setOptions(options.clustering); - this.manipulation.setOptions(options.manipulation); + 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 - if (options.clickToUse !== undefined) { - if (options.clickToUse === true) { - if (this.activator === undefined) { - this.activator = new Activator(this.frame); - this.activator.on("change", this._createKeyBinds.bind(this)); - } - } else { - if (this.activator !== undefined) { - this.activator.destroy(); - delete this.activator; + // 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 + } } - this.body.emitter.emit("activate"); } } else { - this.body.emitter.emit("activate"); + // add/remove this item from the current selection + var index = selection.indexOf(item.id); + if (index == -1) { + // item is not yet selected -> select it + selection.push(item.id); + } else { + // item is already selected -> deselect it + selection.splice(index, 1); + } } - this.canvas.setSize(); + this.setSelection(selection); + + this.body.emitter.emit("select", { + items: this.getSelection() + }); } }; - /** - * Update the this.body.nodeIndices with the most recent node index list + * 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._updateVisibleIndices = function () { - var nodes = this.body.nodes; - var edges = this.body.edges; - this.body.nodeIndices = []; - this.body.edgeIndices = []; + ItemSet._getItemRange = function (itemsData) { + var max = null; + var min = null; - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].options.hidden === false) { - this.body.nodeIndices.push(nodeId); - } + itemsData.forEach(function (data) { + if (min == null || data.start < min) { + min = data.start; } - } - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].options.hidden === false) { - this.body.edgeIndices.push(edgeId); + if (data.end != undefined) { + if (max == null || data.end > max) { + max = data.end; + } + } else { + if (max == null || data.start > max) { + max = data.start; } } - } - }; - - Network.prototype.bindEventListeners = function () { - var _this = this; - // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. - this.body.emitter.on("_dataChanged", function (params) { - var t0 = new Date().valueOf(); - // update shortcut lists - _this._updateVisibleIndices(); - _this.physics.updatePhysicsIndices(); - - // call the dataUpdated event because the only difference between the two is the updating of the indices - _this.body.emitter.emit("_dataUpdated"); - - console.log("_dataChanged took:", new Date().valueOf() - t0); }); - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on("_dataUpdated", function () { - var t0 = new Date().valueOf(); - // update values - _this._updateValueRange(_this.body.nodes); - _this._updateValueRange(_this.body.edges); - // start simulation (can be called safely, even if already running) - _this.body.emitter.emit("startSimulation"); - - console.log("_dataUpdated took:", new Date().valueOf() - t0); - }); + return { + min: min, + max: max + }; }; /** - * 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. + * 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.setData = function (data) { - // reset the physics engine. - this.body.emitter.emit("resetPhysics"); - this.body.emitter.emit("_resetData"); + ItemSet.itemFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty("timeline-item")) { + return target["timeline-item"]; + } + target = target.parentNode; + } - // unselect all to ensure no selections from old data are carried over. - this.selectionHandler.unselectAll(); + return null; + }; - 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."); - } + /** + * 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; + //} + // - // 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; + 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; } - } else if (data && data.gephi) { - // parse DOT file - if (data && data.gephi) { - var gephiData = gephiParser.parseGephi(data.gephi); - this.setData(gephiData); - return; + + 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; + } } - } else { - this.nodesHandler.setData(data && data.nodes, true); - this.edgesHandler.setData(data && data.edges, true); } - // emit change in data - this.body.emitter.emit("_dataChanged"); - - // find a stable position or start animating to a stable position - this.body.emitter.emit("initPhysics"); + return null; }; - /** - * 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; + * 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 */ - Network.prototype.destroy = function () { - this.body.emitter.emit("destroy"); - - // clear events - this.body.emitter.off(); + ItemSet.itemSetFromTarget = function (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty("timeline-itemset")) { + return target["timeline-itemset"]; + } + target = target.parentNode; + } - // remove the container and everything inside it recursively - util.recursiveDOMDelete(this.body.container); + return null; }; + module.exports = ItemSet; + +/***/ }, +/* 29 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + var moment = __webpack_require__(2); + var DateUtil = __webpack_require__(26); + var util = __webpack_require__(1); /** - * 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 + * @constructor TimeStep + * The class TimeStep is an iterator for dates. You provide a start date and an + * end date. The class itself determines the best scale (step size) based on the + * provided start Date, end Date, and minimumStep. + * + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * + * Alternatively, you can set a scale by hand. + * After creation, you can initialize the class by executing first(). Then you + * can iterate from the start date to the end date via next(). You can check if + * the end date is reached with the function hasNext(). After each step, you can + * retrieve the current date via getCurrent(). + * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours, + * days, to years. + * + * Version: 1.2 + * + * @param {Date} [start] The start date, for example new Date(2010, 9, 21) + * or new Date(2010, 9, 21, 23, 45, 00) + * @param {Date} [end] The end date + * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds */ - Network.prototype._updateValueRange = function (obj) { - var id; + function TimeStep(start, end, minimumStep, hiddenDates) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; - var valueTotal = 0; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = valueMin === undefined ? value : Math.min(value, valueMin); - valueMax = valueMax === undefined ? value : Math.max(value, valueMax); - valueTotal += value; - } - } - } + this.autoScale = true; + this.scale = "day"; + this.step = 1; - // 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); - } - } - } - }; + // initialize the range + this.setRange(start, end, minimumStep); + // hidden Dates options + this.switchedDay = false; + this.switchedMonth = false; + this.switchedYear = false; + this.hiddenDates = hiddenDates; + if (hiddenDates === undefined) { + this.hiddenDates = []; + } - /** - * Scale the network - * @param {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._setScale = function (scale) { - this.body.view.scale = scale; - }; + this.format = TimeStep.FORMAT; // default formatting + } - /** - * Get the current scale of the network - * @return {Number} scale Scaling factor 1.0 is unscaled - * @private - */ - Network.prototype._getScale = function () { - return this.body.view.scale; + // 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: "" + } }; - /** - * Load the XY positions of the nodes into the dataset. + * 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 */ - Network.prototype.storePositions = function () { - // todo: incorporate fixed instead of allowedtomove, add support for clusters and hierarchical. - var dataArray = []; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - var allowedToMoveX = !this.body.nodes.xFixed; - var allowedToMoveY = !this.body.nodes.yFixed; - if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { - dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y), allowedToMoveX: allowedToMoveX, allowedToMoveY: allowedToMoveY }); - } - } - } - this.body.data.nodes.update(dataArray); + TimeStep.prototype.setFormat = function (format) { + var defaultFormat = util.deepExtend({}, TimeStep.FORMAT); + this.format = util.deepExtend(defaultFormat, format); }; /** - * Return the positions of the nodes. + * Set a new range + * If minimumStep is provided, the step size is chosen as close as possible + * to the minimumStep but larger than minimumStep. If minimumStep is not + * provided, the scale is set to 1 DAY. + * The minimumStep should correspond with the onscreen size of about 6 characters + * @param {Date} [start] The start date and time. + * @param {Date} [end] The end date and time. + * @param {int} [minimumStep] Optional. Minimum step size in milliseconds */ - Network.prototype.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) }; - } - } + TimeStep.prototype.setRange = function (start, end, minimumStep) { + if (!(start instanceof Date) || !(end instanceof Date)) { + throw "No legal start or end date in method setRange"; } - return dataArray; - }; + this._start = start != undefined ? new Date(start.valueOf()) : new Date(); + this._end = end != undefined ? new Date(end.valueOf()) : new Date(); - /** - * Returns true when the Network is active. - * @returns {boolean} - */ - Network.prototype.isActive = function () { - return !this.activator || this.activator.active; + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } }; - /** - * Sets the scale - * @returns {Number} + * Set the range iterator to the start date. */ - Network.prototype.setScale = function () { - return this._setScale(); + TimeStep.prototype.first = function () { + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); }; - /** - * Returns the scale - * @returns {Number} + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date */ - Network.prototype.getScale = function () { - return this._getScale(); - }; + 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 (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; + } + } + }; /** - * Check if a node is a cluster. - * @param nodeId - * @returns {*} + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date */ - 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; - } + TimeStep.prototype.hasNext = function () { + return this.current.valueOf() <= this._end.valueOf(); }; /** - * Returns the scale - * @returns {Number} + * Do the next step */ - Network.prototype.getCenterCoordinates = function () { - return this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); - }; + 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": - Network.prototype.getBoundingBox = function (nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].boundingBox; - } - }; - Network.prototype.getConnectedNodes = function (nodeId) { - var nodeList = []; - if (this.body.nodes[nodeId] !== undefined) { - var node = this.body.nodes[nodeId]; - var nodeObj = { nodeId: true }; // used to quickly check if node already exists - for (var i = 0; i < node.edges.length; i++) { - var edge = node.edges[i]; - if (edge.toId == nodeId) { - if (nodeObj[edge.fromId] === undefined) { - nodeList.push(edge.fromId); - nodeObj[edge.fromId] = true; - } - } else if (edge.fromId == nodeId) { - if (nodeObj[edge.toId] === undefined) { - nodeList.push(edge.toId); - nodeObj[edge.toId] = true; - } - } + this.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; } } - 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); + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case "millisecond": + if (this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0);break; + case "second": + if (this.current.getSeconds() < this.step) this.current.setSeconds(0);break; + case "minute": + if (this.current.getMinutes() < this.step) this.current.setMinutes(0);break; + case "hour": + if (this.current.getHours() < this.step) this.current.setHours(0);break; + case "weekday": + // intentional fall through + case "day": + if (this.current.getDate() < this.step + 1) this.current.setDate(1);break; + case "month": + if (this.current.getMonth() < this.step) this.current.setMonth(0);break; + case "year": + break; // nothing to do for year + default: + break; } } - return edgesList; - }; - - Network.prototype.generateColorObject = function (color) { - return util.parseColor(color); - }; - module.exports = Network; + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); + } -/***/ }, -/* 37 */ -/***/ function(module, exports, __webpack_require__) { + DateUtil.stepOverHiddenDates(this, prev); + }; - "use strict"; /** - * @class Images - * This class loads images and keeps them stored. + * Get the current datetime + * @return {Date} current The current date */ - function Images(callback) { - this.images = {}; - this.imageBroken = {}; - this.callback = callback; - } + TimeStep.prototype.getCurrent = function () { + return this.current; + }; /** + * 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 {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 + * @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. */ - 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) { - console.error("Could not load brokenImage:", brokenUrl); - delete this.src; - if (me.callback) { - me.callback(this); - } - } else { - console.error("Could not load image:", url); - this.src = brokenUrl; - me.imageBroken[url] = true; - } - } - }; - - img.src = url; + TimeStep.prototype.setScale = function (params) { + if (params && typeof params.scale == "string") { + this.scale = params.scale; + this.step = params.step > 0 ? params.step : 1; + this.autoScale = false; } - - return img; }; - module.exports = Images; - -/***/ }, -/* 38 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - /** - * 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 + * Enable or disable autoscaling + * @param {boolean} enable If true, autoascaling is set true */ - function parseDOT(data) { - dot = data; - return parseGraph(); - } - - // token types enumeration - var TOKENTYPE = { - NULL: 0, - DELIMITER: 1, - IDENTIFIER: 2, - UNKNOWN: 3 - }; - - // map with all delimiters - var DELIMITERS = { - "{": true, - "}": true, - "[": true, - "]": true, - ";": true, - "=": true, - ",": true, - - "->": true, - "--": true + TimeStep.prototype.setAutoScale = function (enable) { + this.autoScale = enable; }; - 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 - - /** - * 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); - } /** - * 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. + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds */ - function next() { - index++; - c = dot.charAt(index); - } + TimeStep.prototype.setMinimumStep = function (minimumStep) { + if (minimumStep == undefined) { + return; + } - /** - * Preview the next character from the dot file. - * @return {String} cNext - */ - function nextPreview() { - return dot.charAt(index + 1); - } + //var b = asc + ds; - /** - * 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); - } + 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; - /** - * Merge all options of object b into object b - * @param {Object} a - * @param {Object} b - * @return {Object} a - */ - function merge(a, b) { - if (!a) { - a = {}; + // find the smallest step that is larger than the provided minimumStep + if (stepYear * 1000 > minimumStep) { + this.scale = "year";this.step = 1000; } - - if (b) { - for (var name in b) { - if (b.hasOwnProperty(name)) { - a[name] = b[name]; - } - } + if (stepYear * 500 > minimumStep) { + this.scale = "year";this.step = 500; } - return a; - } - - /** - * 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; - } + if (stepYear * 100 > minimumStep) { + this.scale = "year";this.step = 100; } - } - - /** - * Add a node to a graph object. If there is already a node with - * the same id, their attributes will be merged. - * @param {Object} graph - * @param {Object} node - */ - function addNode(graph, node) { - var i, len; - var current = null; - - // find root graph (in case of subgraph) - var graphs = [graph]; // list with all graphs from current graph to root graph - var root = graph; - while (root.parent) { - graphs.push(root.parent); - root = root.parent; + 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; } - - // 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; - } - } + if (stepSecond * 10 > minimumStep) { + this.scale = "second";this.step = 10; } - - if (!current) { - // this is a new node - current = { - id: node.id - }; - if (graph.node) { - // clone default attributes - current.attr = merge(current.attr, graph.node); - } + if (stepSecond * 5 > minimumStep) { + this.scale = "second";this.step = 5; } - - // add node to this (sub)graph and all its parent graphs - for (i = graphs.length - 1; i >= 0; i--) { - var g = graphs[i]; - - if (!g.nodes) { - g.nodes = []; - } - if (g.nodes.indexOf(current) == -1) { - g.nodes.push(current); - } + if (stepSecond > minimumStep) { + this.scale = "second";this.step = 1; } - - // merge attributes - if (node.attr) { - current.attr = merge(current.attr, node.attr); + if (stepMillisecond * 200 > minimumStep) { + this.scale = "millisecond";this.step = 200; } - } - - /** - * Add an edge to a graph object - * @param {Object} graph - * @param {Object} edge - */ - function addEdge(graph, edge) { - if (!graph.edges) { - graph.edges = []; + if (stepMillisecond * 100 > minimumStep) { + this.scale = "millisecond";this.step = 100; } - graph.edges.push(edge); - if (graph.edge) { - var attr = merge({}, graph.edge); // clone default attributes - edge.attr = merge(attr, edge.attr); // merge attributes + if (stepMillisecond * 50 > minimumStep) { + this.scale = "millisecond";this.step = 50; } - } - - /** - * Create an edge to a graph object - * @param {Object} graph - * @param {String | Number | Object} from - * @param {String | Number | Object} to - * @param {String} type - * @param {Object | null} attr - * @return {Object} edge - */ - function createEdge(graph, from, to, type, attr) { - var edge = { - from: from, - to: to, - type: type - }; - - if (graph.edge) { - edge.attr = merge({}, graph.edge); // clone default attributes + if (stepMillisecond * 10 > minimumStep) { + this.scale = "millisecond";this.step = 10; } - edge.attr = merge(edge.attr || {}, attr); // merge attributes - - return edge; - } + if (stepMillisecond * 5 > minimumStep) { + this.scale = "millisecond";this.step = 5; + } + if (stepMillisecond > minimumStep) { + this.scale = "millisecond";this.step = 1; + } + }; /** - * Get next token in the current dot file. - * The token and token type are available as token and tokenType + * 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 */ - function getToken() { - tokenType = TOKENTYPE.NULL; - token = ""; - - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); - } - - do { - var isComment = false; + TimeStep.snap = function (date, scale, step) { + var clone = new Date(date.valueOf()); - // 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; + 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); } - // skip over whitespaces - while (c == " " || c == "\t" || c == "\n" || c == "\r") { - // space, tab, enter - next(); + 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; } - } while (isComment); - - // check for end of dot file - if (c == "") { - // token is still empty - tokenType = TOKENTYPE.DELIMITER; - return; - } - - // check for delimiters consisting of 2 characters - var c2 = c + nextPreview(); - if (DELIMITERS[c2]) { - tokenType = TOKENTYPE.DELIMITER; - token = c2; - next(); - next(); - return; - } - - // check for delimiters consisting of 1 character - if (DELIMITERS[c]) { - tokenType = TOKENTYPE.DELIMITER; - token = c; - next(); - return; - } - - // check for an identifier (number or string) - // TODO: more precise parsing of numbers/strings (and the port separator ':') - if (isAlphaNumeric(c) || c == "-") { - token += c; - next(); - - while (isAlphaNumeric(c)) { - token += c; - next(); + 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 (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 + 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; } - tokenType = TOKENTYPE.IDENTIFIER; - return; - } - - // 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(); + 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; } - if (c != "\"") { - throw newSyntaxError("End of string \" expected"); + 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; } - next(); - tokenType = TOKENTYPE.IDENTIFIER; - return; + } else if (scale == "millisecond") { + var _step = step > 5 ? step / 2 : 1; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / _step) * _step); } - // 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) + "\""); - } + return clone; + }; /** - * Parse a graph. - * @returns {Object} graph + * Check if the current value is a major value (for example when the step + * is DAY, a major value is each first day of the MONTH) + * @return {boolean} true if current date is major, else false. */ - function parseGraph() { - var graph = {}; - - first(); - getToken(); - - // optional strict keyword - if (token == "strict") { - graph.strict = true; - getToken(); - } - - // graph or digraph keyword - if (token == "graph" || token == "digraph") { - graph.type = token; - getToken(); - } - - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - graph.id = token; - getToken(); - } - - // open angle bracket - if (token != "{") { - throw newSyntaxError("Angle bracket { expected"); - } - getToken(); - - // statements - parseStatements(graph); - - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); + 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; + } } - getToken(); - // end of file - if (token !== "") { - throw newSyntaxError("End of file expected"); + 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; } - getToken(); - - // remove temporary default options - delete graph.node; - delete graph.edge; - delete graph.graph; + }; - return graph; - } /** - * Parse a list with statements. - * @param {Object} graph + * 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 */ - function parseStatements(graph) { - while (token !== "" && token != "}") { - parseStatement(graph); - if (token == ";") { - getToken(); - } + TimeStep.prototype.getLabelMinor = function (date) { + if (date == undefined) { + date = this.current; } - } + + var format = this.format.minorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ""; + }; /** - * 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 + * Returns formatted text for the major axis label, depending on the current + * date and the scale. For example when scale is MINUTE, the major scale is + * hours, and the hour will be formatted as "hh". + * @param {Date} [date] custom date. if not provided, current date is taken */ - function parseStatement(graph) { - // parse subgraph - var subgraph = parseSubgraph(graph); - if (subgraph) { - // edge statements - parseEdge(graph, subgraph); - - return; + TimeStep.prototype.getLabelMajor = function (date) { + if (date == undefined) { + date = this.current; } - // parse an attribute statement - var attr = parseAttributeStatement(graph); - if (attr) { - return; - } + var format = this.format.majorLabels[this.scale]; + return format && format.length > 0 ? moment(date).format(format) : ""; + }; - // parse node - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Identifier expected"); - } - var id = token; // id can be a string or a number - getToken(); + 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; - 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); + function even(value) { + return value / step % 2 == 0 ? " even" : " odd"; } - } - - /** - * Parse a subgraph - * @param {Object} graph parent graph object - * @return {Object | null} subgraph - */ - function parseSubgraph(graph) { - var subgraph = null; - - // optional subgraph keyword - if (token == "subgraph") { - subgraph = {}; - subgraph.type = "subgraph"; - getToken(); - // optional graph id - if (tokenType == TOKENTYPE.IDENTIFIER) { - subgraph.id = token; - getToken(); + 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 ""; } - // open angle bracket - if (token == "{") { - getToken(); + function currentWeek(date) { + return date.isSame(new Date(), "week") ? " current-week" : ""; + } - if (!subgraph) { - subgraph = {}; - } - subgraph.parent = graph; - subgraph.node = graph.node; - subgraph.edge = graph.edge; - subgraph.graph = graph.graph; + function currentMonth(date) { + return date.isSame(new Date(), "month") ? " current-month" : ""; + } - // statements - parseStatements(subgraph); + function currentYear(date) { + return date.isSame(new Date(), "year") ? " current-year" : ""; + } - // close angle bracket - if (token != "}") { - throw newSyntaxError("Angle bracket } expected"); - } - getToken(); + switch (this.scale) { + case "millisecond": + return even(date.milliseconds()).trim(); - // remove temporary default options - delete subgraph.node; - delete subgraph.edge; - delete subgraph.graph; - delete subgraph.parent; + case "second": + return even(date.seconds()).trim(); - // register at the parent graph - if (!graph.subgraphs) { - graph.subgraphs = []; - } - graph.subgraphs.push(subgraph); - } + case "minute": + return even(date.minutes()).trim(); - return subgraph; - } + case "hour": + var hours = date.hours(); + if (this.step == 4) { + hours = hours + "-h" + (hours + 4); + } + return "h" + hours + today(date) + even(date.hours()); - /** - * 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(); + case "weekday": + return date.format("dddd").toLowerCase() + today(date) + currentWeek(date) + even(date.date()); - // node attributes - graph.node = parseAttributeList(); - return "node"; - } else if (token == "edge") { - getToken(); + case "day": + var day = date.date(); + var month = date.format("MMMM").toLowerCase(); + return "day" + day + " " + month + currentMonth(date) + even(day - 1); - // edge attributes - graph.edge = parseAttributeList(); - return "edge"; - } else if (token == "graph") { - getToken(); + case "month": + return date.format("MMMM").toLowerCase() + currentMonth(date) + even(date.month()); - // graph attributes - graph.graph = parseAttributeList(); - return "graph"; + case "year": + var year = date.year(); + return "year" + year + currentYear(date) + even(year); + + default: + return ""; } + }; - return null; - } + module.exports = TimeStep; - /** - * 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); +/***/ }, +/* 30 */ +/***/ function(module, exports, __webpack_require__) { - // edge statements - parseEdge(graph, id); - } + "use strict"; + + var util = __webpack_require__(1); + var stack = __webpack_require__(31); + var RangeItem = __webpack_require__(32); /** - * Parse an edge or a series of edges - * @param {Object} graph - * @param {String | Number} from Id of the from node + * @constructor Group + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ - function parseEdge(graph, from) { - while (token == "->" || token == "--") { - var to; - var type = token; - getToken(); + function Group(groupId, data, itemSet) { + this.groupId = groupId; + this.subgroups = {}; + this.subgroupIndex = 0; + this.subgroupOrderer = data && data.subgroupOrder; + this.itemSet = itemSet; - 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(); + this.dom = {}; + this.props = { + label: { + width: 0, + height: 0 } + }; + this.className = null; - // parse edge attributes - var attr = parseAttributeList(); + 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; + }); - // create edge - var edge = createEdge(graph, from, to, type, attr); - addEdge(graph, edge); + this._create(); - from = to; - } + this.setData(data); } /** - * Parse a set with attributes, - * for example [label="1.000", shape=solid] - * @return {Object | null} attr + * Create DOM elements for the group + * @private */ - function parseAttributeList() { - var attr = null; - - while (token == "[") { - getToken(); - attr = {}; - while (token !== "" && token != "]") { - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute name expected"); - } - var name = token; + Group.prototype._create = function () { + var label = document.createElement("div"); + label.className = "vlabel"; + this.dom.label = label; - getToken(); - if (token != "=") { - throw newSyntaxError("Equal sign = expected"); - } - getToken(); + var inner = document.createElement("div"); + inner.className = "inner"; + label.appendChild(inner); + this.dom.inner = inner; - if (tokenType != TOKENTYPE.IDENTIFIER) { - throw newSyntaxError("Attribute value expected"); - } - var value = token; - setValue(attr, name, value); // name can be a path + var foreground = document.createElement("div"); + foreground.className = "group"; + foreground["timeline-group"] = this; + this.dom.foreground = foreground; - getToken(); - if (token == ",") { - getToken(); - } - } + this.dom.background = document.createElement("div"); + this.dom.background.className = "group"; - if (token != "]") { - throw newSyntaxError("Bracket ] expected"); - } - getToken(); - } + this.dom.axis = document.createElement("div"); + this.dom.axis.className = "group"; - return attr; - } + // 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); + }; /** - * Create a syntax error with extra information on current token and index. - * @param {String} message - * @returns {SyntaxError} err + * Set the group data for this group + * @param {Object} data Group data, can contain properties content and className */ - function newSyntaxError(message) { - return new SyntaxError(message + ", got \"" + chop(token, 30) + "\" (char " + index + ")"); - } + 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 + } - /** - * 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) + "..."; - } + // update title + this.dom.label.title = data && data.title || ""; - /** - * 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); - } - }); + if (!this.dom.inner.firstChild) { + util.addClassName(this.dom.inner, "hidden"); } else { - if (Array.isArray(array2)) { - array2.forEach(function (elem2) { - fn(array1, elem2); - }); - } else { - fn(array1, array2); - } + util.removeClassName(this.dom.inner, "hidden"); } - } - /** - * 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: {} - }; + // update className + var className = data && data.className || null; + if (className != this.className) { + if (this.className) { + util.removeClassName(this.dom.label, this.className); + util.removeClassName(this.dom.foreground, this.className); + util.removeClassName(this.dom.background, this.className); + util.removeClassName(this.dom.axis, this.className); + } + util.addClassName(this.dom.label, className); + util.addClassName(this.dom.foreground, className); + util.addClassName(this.dom.background, className); + util.addClassName(this.dom.axis, className); + this.className = className; + } - // 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); - }); + // 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; } + }; - // 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; - }; + /** + * Get the width of the group label + * @return {number} width + */ + Group.prototype.getLabelWidth = function () { + return this.props.label.width; + }; - dotData.edges.forEach(function (dotEdge) { - var from, to; - if (dotEdge.from instanceof Object) { - from = dotEdge.from.nodes; - } else { - from = { - id: dotEdge.from - }; - } - if (dotEdge.to instanceof Object) { - to = dotEdge.to.nodes; - } else { - to = { - id: dotEdge.to - }; - } + /** + * Repaint this group + * @param {{start: number, end: number}} range + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @param {boolean} [restack=false] Force restacking of all items + * @return {boolean} Returns true if the group is resized + */ + Group.prototype.redraw = function (range, margin, restack) { + var resized = false; - if (dotEdge.from instanceof Object && dotEdge.from.edges) { - dotEdge.from.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - 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); - }); + // 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; - if (dotEdge.to instanceof Object && dotEdge.to.edges) { - dotEdge.to.edges.forEach(function (subEdge) { - var graphEdge = convertEdge(subEdge); - graphData.edges.push(graphEdge); - }); - } + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); }); + + restack = true; } - // copy the options - if (dotData.attr) { - graphData.options = dotData.attr; + // 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); } - return graphData; - } + // recalculate the height of the group + var height = this._calculateHeight(margin); - // exports - exports.parseDOT = parseDOT; - exports.DOTToGraph = DOTToGraph; + // 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; -/***/ }, -/* 39 */ -/***/ function(module, exports, __webpack_require__) { + // 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; - "use strict"; + // apply new height + this.dom.background.style.height = height + "px"; + this.dom.foreground.style.height = height + "px"; + this.dom.label.style.height = height + "px"; - function parseGephi(gephiJSON, options) { - var edges = []; - var nodes = []; - this.options = { - edges: { - inheritColor: true - }, - nodes: { - allowedToMove: false, - parseColor: false + // update vertical position of items after they are re-stacked and the height of the group is calculated + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + var item = this.visibleItems[i]; + item.repositionY(margin); + } + + return resized; + }; + + /** + * recalculate the height of the group + * @param {{item: {horizontal: number, vertical: number}, axis: number}} margin + * @returns {number} Returns the height + * @private + */ + Group.prototype._calculateHeight = function (margin) { + // recalculate the height of the group + var height; + var visibleItems = this.visibleItems; + //var visibleSubgroups = []; + //this.visibleSubgroups = 0; + this.resetSubgroups(); + var me = this; + if (visibleItems.length > 0) { + var min = visibleItems[0].top; + var max = visibleItems[0].top + visibleItems[0].height; + util.forEach(visibleItems, function (item) { + min = Math.min(min, item.top); + max = Math.max(max, item.top + item.height); + if (item.data.subgroup !== undefined) { + me.subgroups[item.data.subgroup].height = Math.max(me.subgroups[item.data.subgroup].height, item.height); + me.subgroups[item.data.subgroup].visible = true; + } + }); + if (min > margin.axis) { + // there is an empty gap between the lowest item and the axis + var offset = min - margin.axis; + max -= offset; + util.forEach(visibleItems, function (item) { + item.top -= offset; + }); } - }; + height = max + margin.item.vertical / 2; + } else { + height = margin.axis + margin.item.vertical; + } + height = Math.max(height, this.props.label.height); - if (options !== undefined) { - this.options.nodes.allowedToMove = options.allowedToMove | false; - this.options.nodes.parseColor = options.parseColor | false; - this.options.edges.inheritColor = options.inheritColor | true; + return height; + }; + + /** + * Show this group: attach to the DOM + */ + Group.prototype.show = function () { + if (!this.dom.label.parentNode) { + this.itemSet.dom.labelSet.appendChild(this.dom.label); } - 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.dom.foreground.parentNode) { + this.itemSet.dom.foreground.appendChild(this.dom.foreground); } - 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); + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); } - return { nodes: nodes, edges: edges }; - } + if (!this.dom.axis.parentNode) { + this.itemSet.dom.axis.appendChild(this.dom.axis); + } + }; - exports.parseGephi = parseGephi; + /** + * Hide this group: remove from the DOM + */ + Group.prototype.hide = function () { + var label = this.dom.label; + if (label.parentNode) { + label.parentNode.removeChild(label); + } -/***/ }, -/* 40 */ -/***/ function(module, exports, __webpack_require__) { + var foreground = this.dom.foreground; + if (foreground.parentNode) { + foreground.parentNode.removeChild(foreground); + } - "use strict"; + var background = this.dom.background; + if (background.parentNode) { + background.parentNode.removeChild(background); + } - // first check if moment.js is already loaded in the browser window, if so, - // use this instance. Else, load via commonjs. - module.exports = typeof window !== "undefined" && window.moment || __webpack_require__(63); + var axis = this.dom.axis; + if (axis.parentNode) { + axis.parentNode.removeChild(axis); + } + }; -/***/ }, -/* 41 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Add an item to the group + * @param {Item} item + */ + Group.prototype.add = function (item) { + this.items[item.id] = item; + item.setParent(this); - "use strict"; + // 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(); - // Only load hammer.js when in a browser environment - // (loading hammer.js in a node.js environment gives errors) - if (typeof window !== "undefined") { - var propagating = __webpack_require__(65); - var Hammer = window.Hammer || __webpack_require__(64); - module.exports = propagating(Hammer); - } else { - module.exports = function () { - throw Error("hammer.js is only available in a browser, not in node.js."); - }; - } + 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); + } + }; -/***/ }, -/* 42 */ -/***/ function(module, exports, __webpack_require__) { + 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); + } - "use strict"; + if (sortArray.length > 0) { + for (var i = 0; i < sortArray.length; i++) { + this.subgroups[sortArray[i].subgroup].index = i; + } + } + } + }; - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(43); - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - var Range = __webpack_require__(17); - var ItemSet = __webpack_require__(32); - var Activator = __webpack_require__(49); - var DateUtil = __webpack_require__(15); - var CustomTime = __webpack_require__(27); + Group.prototype.resetSubgroups = function () { + for (var subgroup in this.subgroups) { + if (this.subgroups.hasOwnProperty(subgroup)) { + this.subgroups[subgroup].visible = false; + } + } + }; /** - * 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 + * Remove an item from the group + * @param {Item} item */ - function Core() {} + Group.prototype.remove = function (item) { + delete this.items[item.id]; + item.setParent(null); + + // remove from visible items + var index = this.visibleItems.indexOf(item); + if (index != -1) this.visibleItems.splice(index, 1); + + // TODO: also remove from ordered items? + }; - // turn Core into an event emitter - Emitter(Core.prototype); /** - * 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 + * Remove an item from the corresponding DataSet + * @param {Item} item */ - Core.prototype._create = function (container) { - this.dom = {}; + Group.prototype.removeFromDataSet = function (item) { + this.itemSet.removeItem(item.id); + }; - 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"; + /** + * Reorder the items + */ + Group.prototype.order = function () { + var array = util.toArray(this.items); + var startArray = []; + var endArray = []; - 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); + 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 + }; - this.dom.centerContainer.appendChild(this.dom.center); - this.dom.leftContainer.appendChild(this.dom.left); - this.dom.rightContainer.appendChild(this.dom.right); + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); + }; - this.dom.centerContainer.appendChild(this.dom.shadowTop); - this.dom.centerContainer.appendChild(this.dom.shadowBottom); - this.dom.leftContainer.appendChild(this.dom.shadowTopLeft); - this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft); - this.dom.rightContainer.appendChild(this.dom.shadowTopRight); - this.dom.rightContainer.appendChild(this.dom.shadowBottomRight); - this.on("rangechange", this.redraw.bind(this)); + /** + * 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; - 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); - } + // 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 { - // redraw immediately - me._redraw(); + return 1; } - }); + }; - // 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 = {}; + // 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); + } + } - 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; - }); + // we do a binary search for the items that have only start values. + var initialPosByStart = util.binarySearchCustom(orderedItems.byStart, searchFunction, "data", "start"); - // 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)); + // 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; + }); - function onMouseWheel(event) { - if (me.isActive()) { - me.emit("mousewheel", event); + // if the window has changed programmatically without overlapping the old window, the ranged items with start < lowerBound and end > upperbound are not shown. + // We therefore have to brute force check all items in the byEnd list + if (this.checkRangedItems == true) { + this.checkRangedItems = false; + for (i = 0; i < orderedItems.byEnd.length; i++) { + this._checkIfVisibleWithReference(orderedItems.byEnd[i], visibleItems, visibleItemsLookup, range); } + } else { + // we do a binary search for the items that have defined end times. + var initialPosByEnd = util.binarySearchCustom(orderedItems.byEnd, searchFunction, "data", "end"); + + // trace the visible items from the inital start pos both ways until an invisible item is found, we only look at the end values. + this._traceVisible(initialPosByEnd, orderedItems.byEnd, visibleItems, visibleItemsLookup, function (item) { + return item.data.end < lowerBound || item.data.end > upperBound; + }); } - 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; + // 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(); + } - // attach the root panel to the provided container - if (!container) throw new Error("No container provided"); - container.appendChild(this.dom.root); + // 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; }; - /** - * 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); + Group.prototype._traceVisible = function (initialPos, items, visibleItems, visibleItemsLookup, breakCondition) { + var item; + var i; - if ("hiddenDates" in this.options) { - DateUtil.convertHiddenOptions(this.body, this.options.hiddenDates); + 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 ("clickToUse" in options) { - if (options.clickToUse) { - if (!this.activator) { - this.activator = new Activator(this.dom.root); - } + for (i = initialPos + 1; i < items.length; i++) { + item = items[i]; + if (breakCondition(item)) { + break; } else { - if (this.activator) { - this.activator.destroy(); - delete this.activator; + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); } } } + } + }; - // enable/disable autoResize - this._initAutoResize(); + + /** + * 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(); + } + }; + + + /** + * this function is very similar to the _checkIfInvisible() but it does not + * return booleans, hides the item if it should not be seen and always adds to + * the visibleItems. + * this one is for brute forcing and hiding. + * + * @param {Item} item + * @param {Array} visibleItems + * @param {{start:number, end:number}} range + * @private + */ + Group.prototype._checkIfVisibleWithReference = function (item, visibleItems, visibleItemsLookup, range) { + if (item.isVisible(range)) { + if (visibleItemsLookup[item.id] === undefined) { + visibleItemsLookup[item.id] = true; + visibleItems.push(item); + } + } else { + if (item.displayed) item.hide(); } + }; + + + + module.exports = Group; - // propagate options to all components - this.components.forEach(function (component) { - return component.setOptions(options); - }); +/***/ }, +/* 31 */ +/***/ function(module, exports, __webpack_require__) { - // 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."); - } + "use strict"; - // redraw everything - this._redraw(); - }; + // Utility functions for ordering and stacking of items + var EPSILON = 0.001; // used when checking collisions, to prevent round-off errors /** - * Returns true when the Timeline is active. - * @returns {boolean} + * Order items by their start data + * @param {Item[]} items */ - Core.prototype.isActive = function () { - return !this.activator || this.activator.active; + exports.orderByStart = function (items) { + items.sort(function (a, b) { + return a.data.start - b.data.start; + }); }; /** - * Destroy the Core, clean up all DOM elements and event listeners. + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} items */ - Core.prototype.destroy = function () { - // unbind datasets - this.clear(); + 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; - // remove all event listeners - this.off(); + return aTime - bTime; + }); + }; - // stop checking for changed size - this._stopAutoResize(); + /** + * 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; - // remove from DOM - if (this.dom.root.parentNode) { - this.dom.root.parentNode.removeChild(this.dom.root); + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; + } } - this.dom = null; - // remove Activator - if (this.activator) { - this.activator.destroy(); - delete this.activator; - } + // 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; - // cleanup hammer touch events - for (var event in this.listeners) { - if (this.listeners.hasOwnProperty(event)) { - delete this.listeners[event]; + 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); } } - this.listeners = null; - this.hammer = null; - - // give all components the opportunity to cleanup - this.components.forEach(function (component) { - return component.destroy(); - }); - - this.body = null; }; /** - * Set a custom time bar - * @param {Date} time - * @param {int} id + * 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. */ - Core.prototype.setCustomTime = function (time, id) { - if (!this.customTime) { - throw new Error("Cannot get custom time: Custom time bar is not enabled"); - } - - var barId = id || 0; + exports.nostack = function (items, margin, subgroups) { + var i, iMax, newTop; - this.components.forEach(function (element, index, components) { - if (element instanceof CustomTime && element.options.id === barId) { - element.setCustomTime(time); + // 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; } - }); + } }; /** - * Retrieve the current custom time. - * @return {Date} customTime - * @param {int} id + * 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 */ - Core.prototype.getCustomTime = function (id) { - if (!this.customTime) { - throw new Error("Cannot get custom time: Custom time bar is not enabled"); - } + 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; + }; - var barId = id || 0, - customTime = this.customTime.getCustomTime(); +/***/ }, +/* 32 */ +/***/ function(module, exports, __webpack_require__) { - this.components.forEach(function (element, index, components) { - if (element instanceof CustomTime && element.options.id === barId) { - customTime = element.getCustomTime(); - } - }); + "use strict"; - return customTime; - }; + var Hammer = __webpack_require__(19); + var Item = __webpack_require__(33); /** - * 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 + * @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 */ - Core.prototype.addCustomTime = function (time, id) { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); - } - - 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; - - // 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 the ID is not provided, generate one, otherwise just use it - if (id === undefined) { - numIds = this.customBarIds.filter(function (element) { - return util.isNumber(element); - }); - - 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"); - } - }); + 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 - customBarId = id; + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data.id); + } + if (data.end == undefined) { + throw new Error("Property \"end\" missing in item " + data.id); + } } - this.customBarIds.push(customBarId); - - customTime = new CustomTime(this.body, { - showCustomTime: true, - time: ts, - id: customBarId - }); + Item.call(this, data, conversion, options); + } - this.components.push(customTime); - this.redraw(); + RangeItem.prototype = new Item(null, null, null); - return customBarId; - }; + RangeItem.prototype.baseClassName = "item range"; /** - * 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 + * 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 */ - Core.prototype.removeCustomTime = function (id) { - var me = this; - - 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(); - } - } - }); + RangeItem.prototype.isVisible = function (range) { + // determine visibility + return this.data.start < range.end && this.data.end > range.start; }; - /** - * Get the id's of the currently visible items. - * @returns {Array} The ids of the visible items + * Repaint the item */ - Core.prototype.getVisibleItems = function () { - return this.itemSet && this.itemSet.getVisibleItems() || []; - }; + 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; - /** - * 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.dirty = true; } - // clear groups - if (!what || what.groups) { - this.setGroups(null); + // 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; - // clear options of timeline and of each of the components - if (!what || what.options) { - this.components.forEach(function (component) { - return component.setOptions(component.defaultOptions); - }); + // 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.setOptions(this.defaultOptions); // this will also do a redraw + // 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 + // turn off max-width to be able to calculate the real width + // this causes an extra browser repaint/reflow, but so be it + this.dom.content.style.maxWidth = "none"; + this.props.content.width = this.dom.content.offsetWidth; + this.height = this.dom.box.offsetHeight; + this.dom.content.style.maxWidth = ""; + + this.dirty = false; } + + this._repaintDeleteButton(dom.box); + this._repaintDragLeft(); + this._repaintDragRight(); }; /** - * 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. + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - 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; + RangeItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); } - - var animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(range.start, range.end, animate); }; /** - * Calculate the data range of the items and applies a 5% window around it. - * @returns {{start: Date | null, end: Date | null}} - * @protected + * Hide the item from the DOM (when visible) + * @return {Boolean} changed */ - Core.prototype._getDataRange = function () { - // apply the data range as range - var dataRange = this.getItemRange(); + RangeItem.prototype.hide = function () { + if (this.displayed) { + var box = this.dom.box; - // 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 + if (box.parentNode) { + box.parentNode.removeChild(box); } - start = new Date(start.valueOf() - interval * 0.05); - end = new Date(end.valueOf() + interval * 0.05); - } - return { - start: start, - end: end - }; + this.top = null; + this.left = null; + + this.displayed = false; + } }; /** - * 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. + * Reposition the item horizontally + * @Override */ - 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); + RangeItem.prototype.repositionX = function () { + var parentWidth = this.parent.width; + var start = this.conversion.toScreen(this.data.start); + var end = this.conversion.toScreen(this.data.end); + var contentLeft; + var contentWidth; + + // limit the width of the this, as browsers cannot draw very wide divs + if (start < -parentWidth) { + start = -parentWidth; + } + if (end > 2 * parentWidth) { + end = 2 * parentWidth; + } + var boxWidth = Math.max(end - start, 1); + + if (this.overflow) { + this.left = start; + this.width = boxWidth + this.props.content.width; + contentWidth = this.props.content.width; + + // 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 { - animate = options && options.animate !== undefined ? options.animate : true; - this.range.setRange(start, end, animate); + this.left = start; + this.width = boxWidth; + contentWidth = Math.min(end - start - 2 * this.options.padding, this.props.content.width); } - }; - /** - * 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(); + this.dom.box.style.left = this.left + "px"; + this.dom.box.style.width = boxWidth + "px"; - var start = t - interval / 2; - var end = t + interval / 2; - var animate = options && options.animate !== undefined ? options.animate : true; + switch (this.options.align) { + case "left": + this.dom.content.style.left = "0"; + break; - this.range.setRange(start, end, animate); + case "right": + this.dom.content.style.left = Math.max(boxWidth - contentWidth - 2 * this.options.padding, 0) + "px"; + break; + + case "center": + this.dom.content.style.left = Math.max((boxWidth - contentWidth - 2 * this.options.padding) / 2, 0) + "px"; + break; + + 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"; + } }; /** - * Get the visible window - * @return {{start: Date, end: Date}} Visible range + * Reposition the item vertically + * @Override */ - Core.prototype.getWindow = function () { - var range = this.range.getRange(); - return { - start: new Date(range.start), - end: new Date(range.end) - }; - }; + RangeItem.prototype.repositionY = function () { + var orientation = this.options.orientation, + box = this.dom.box; - /** - * Force a redraw. Can be overridden by implementations of Core - */ - Core.prototype.redraw = function () { - this._redraw(); + if (orientation == "top") { + box.style.top = this.top + "px"; + } else { + box.style.top = this.parent.height - this.top - this.height + "px"; + } }; /** - * Redraw for internal use. Redraws all components. See also the public - * method redraw. + * Repaint a drag area on the left side of the range when the range is selected * @protected */ - Core.prototype._redraw = function () { - var resized = false; - var options = this.options; - var props = this.props; - var dom = this.dom; - - if (!dom) return; // when destroyed + 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; - DateUtil.updateHiddenDates(this.body, this.options.hiddenDates); + //// TODO: this should be redundant? + //Hammer(dragLeft, { + // preventDefault: true + //}).on('drag', function () { + // //console.log('drag left') + // }); - // 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"); + 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; } + }; - // 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, ""); + /** + * 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; - // 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; + //// TODO: this should be redundant? + //Hammer(dragRight, { + // preventDefault: true + //}).on('drag', function () { + // //console.log('drag right') + //}); - // 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; + 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 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 = RangeItem; - // TODO: compensate borders when any of the panels is empty. +/***/ }, +/* 33 */ +/***/ 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 Hammer = __webpack_require__(19); + var util = __webpack_require__(1); - // calculate the widths of the panels - props.root.width = dom.root.offsetWidth; - props.background.width = props.root.width - borderRootWidth; - props.left.width = dom.leftContainer.clientWidth || -props.border.left; - props.leftContainer.width = props.left.width; - props.right.width = dom.rightContainer.clientWidth || -props.border.right; - props.rightContainer.width = props.right.width; - var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth; - props.center.width = centerWidth; - props.centerContainer.width = centerWidth; - props.top.width = centerWidth; - props.bottom.width = centerWidth; + /** + * @constructor 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 || {}; - // 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.selected = false; + this.displayed = false; + this.dirty = true; - 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.top = null; + this.left = null; + this.width = null; + this.height = null; + } - // 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"; + Item.prototype.stack = true; - // update the scrollTop, feasible range for the offset can be changed - // when the height of the Core or of the contents of the center changed - this._updateScrollTop(); + /** + * Select current item + */ + Item.prototype.select = function () { + this.selected = true; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - // reposition the scrollable contents - var offset = this.props.scrollTop; - if (options.orientation == "bottom") { - offset += Math.max(this.props.centerContainer.height - this.props.center.height - this.props.border.top - this.props.border.bottom, 0); - } - 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"; + /** + * Unselect current item + */ + Item.prototype.unselect = function () { + this.selected = false; + this.dirty = true; + if (this.displayed) this.redraw(); + }; - // 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; + /** + * 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(); + }; - // 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?"); + /** + * Set a parent for the item + * @param {ItemSet | Group} parent + */ + Item.prototype.setParent = function (parent) { + if (this.displayed) { + this.hide(); + this.parent = parent; + if (this.parent) { + this.show(); } - this.redrawCount = 0; + } else { + this.parent = parent; } - - 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."); + /** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ + Item.prototype.isVisible = function (range) { + // Should be implemented by Item implementations + return false; }; /** - * 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. + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed */ - Core.prototype.setCurrentTime = function (time) { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); - } - - this.currentTime.setCurrentTime(time); + Item.prototype.show = function () { + return false; }; /** - * Get the current time. - * Only applicable when option `showCurrentTime` is true. - * @return {Date} Returns the current time. + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed */ - Core.prototype.getCurrentTime = function () { - if (!this.currentTime) { - throw new Error("Option showCurrentTime must be true"); - } - - return this.currentTime.getCurrentTime(); + Item.prototype.hide = function () { + return false; }; /** - * 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 + * Repaint the item */ - // TODO: move this function to Range - Core.prototype._toTime = function (x) { - return DateUtil.toTime(this, x, this.props.center.width); - }; + Item.prototype.redraw = function () {}; /** - * 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 + * Reposition the Item horizontally */ - // 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); - }; + Item.prototype.repositionX = function () {}; /** - * 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 + * Reposition the Item vertically */ - // TODO: move this function to Range - Core.prototype._toScreen = function (time) { - return DateUtil.toScreen(this, time, this.props.center.width); - }; + Item.prototype.repositionY = function () {}; + + /** + * Repaint a delete button on the top right of the item when the item is selected + * @param {HTMLElement} anchor + * @protected + */ + Item.prototype._repaintDeleteButton = function (anchor) { + 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"; + // TODO: be able to destroy the delete button + new Hammer(deleteButton).on("tap", function (event) { + me.parent.removeFromDataSet(me); + event.stopPropagation(); + event.preventDefault(); + }); + 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; + } + }; /** - * 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. + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents * @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; - }; + 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); + } + } + this.content = content; + } + }; /** - * Initialize watching when option autoResize is true + * Set HTML contents for the item + * @param {Element} element HTML element to fill with the contents * @private */ - Core.prototype._initAutoResize = function () { - if (this.options.autoResize == true) { - this._startAutoResize(); + Item.prototype._updateTitle = function (element) { + if (this.data.title != null) { + element.title = this.data.title || ""; } else { - this._stopAutoResize(); + element.removeAttribute("title"); } }; /** - * Watch for changes in the size of the container. On resize, the Panel will - * automatically redraw itself. + * 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 */ - Core.prototype._startAutoResize = function () { - var me = this; - - this._stopAutoResize(); + Item.prototype._updateDataAttributes = function (element) { + if (this.options.dataAttributes && this.options.dataAttributes.length > 0) { + var attributes = []; - this._onResize = function () { - if (me.options.autoResize != true) { - // stop watching when the option autoResize is changed to false - me._stopAutoResize(); + if (Array.isArray(this.options.dataAttributes)) { + attributes = this.options.dataAttributes; + } else if (this.options.dataAttributes == "all") { + attributes = Object.keys(this.data); + } else { return; } - if (me.dom.root) { - // check whether the frame is resized - // Note: we compare offsetWidth here, not clientWidth. For some reason, - // IE does not restore the clientWidth from 0 to the actual width after - // changing the timeline's container display style from none to visible - if (me.dom.root.offsetWidth != me.props.lastWidth || me.dom.root.offsetHeight != me.props.lastHeight) { - me.props.lastWidth = me.dom.root.offsetWidth; - me.props.lastHeight = me.dom.root.offsetHeight; + for (var i = 0; i < attributes.length; i++) { + var name = attributes[i]; + var value = this.data[name]; - me.emit("change"); + if (value != null) { + element.setAttribute("data-" + name, value); + } else { + element.removeAttribute("data-" + name); } } - }; - - // add event listener to window resize - util.addEventListener(window, "resize", this._onResize); - - this.watchTimer = setInterval(this._onResize, 1000); + } }; /** - * Stop watching for a resize of the frame. + * Update custom styles of the element + * @param element * @private */ - Core.prototype._stopAutoResize = function () { - if (this.watchTimer) { - clearInterval(this.watchTimer); - this.watchTimer = undefined; + Item.prototype._updateStyle = function (element) { + // remove old styles + if (this.style) { + util.removeCssText(element, this.style); + this.style = null; } - // remove event listener on window.resize - util.removeEventListener(window, "resize", this._onResize); - this._onResize = null; + // append new styles + if (this.data.style) { + util.addCssText(element, this.data.style); + this.style = this.data.style; + } }; + module.exports = Item; + // should be implemented by the item + // should be implemented by the item + // should be implemented by the item + +/***/ }, +/* 34 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var util = __webpack_require__(1); + var Group = __webpack_require__(30); + /** - * Apply a scrollTop - * @param {Number} scrollTop - * @returns {Number} scrollTop Returns the applied scrollTop - * @private + * @constructor BackgroundGroup + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ - Core.prototype._setScrollTop = function (scrollTop) { - this.props.scrollTop = scrollTop; - this._updateScrollTop(); - return this.props.scrollTop; - }; + function BackgroundGroup(groupId, data, itemSet) { + Group.call(this, groupId, data, itemSet); + + this.width = 0; + this.height = 0; + this.top = 0; + this.left = 0; + } + + BackgroundGroup.prototype = Object.create(Group.prototype); /** - * Update the current scrollTop when the height of the containers has been changed - * @returns {Number} scrollTop Returns the applied scrollTop - * @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 */ - 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; - } + BackgroundGroup.prototype.redraw = function (range, margin, restack) { + var resized = false; - // 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; + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - return this.props.scrollTop; + // calculate actual size + this.width = this.dom.background.offsetWidth; + + // apply new height (just always zero for BackgroundGroup + this.dom.background.style.height = "0"; + + // update vertical position of items after they are re-stacked and the height of the group is calculated + for (var i = 0, ii = this.visibleItems.length; i < ii; i++) { + var item = this.visibleItems[i]; + item.repositionY(margin); + } + + return resized; }; /** - * Get the current scrollTop - * @returns {number} scrollTop - * @private + * Show this group: attach to the DOM */ - Core.prototype._getScrollTop = function () { - return this.props.scrollTop; + BackgroundGroup.prototype.show = function () { + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); + } }; - module.exports = Core; + module.exports = BackgroundGroup; /***/ }, -/* 43 */ +/* 35 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var Hammer = __webpack_require__(41); + var Item = __webpack_require__(33); + var util = __webpack_require__(1); /** - * Register a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * @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 */ - exports.onTouch = function (hammer, callback) { - callback.inputHandler = function (event) { - if (event.isFirst) { - callback(event); + function BoxItem(data, conversion, options) { + this.props = { + dot: { + width: 0, + height: 0 + }, + line: { + width: 0, + height: 0 } }; - 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); + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data); } - }; + } - return hammer.on("hammer.input", callback.inputHandler); - }; + Item.call(this, data, conversion, options); + } + + BoxItem.prototype = new Item(null, null, null); /** - * Unregister a touch event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible */ - exports.offTouch = function (hammer, callback) { - hammer.off("hammer.input", callback.inputHandler); + 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; }; /** - * Unregister a release event, taking place before a gesture - * @param {Hammer} hammer A hammer instance - * @param {function} callback Callback, called as callback(event) + * Repaint the item */ - exports.offRelease = exports.offTouch; + BoxItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; -/***/ }, -/* 44 */ -/***/ function(module, exports, __webpack_require__) { + // create main box + dom.box = document.createElement("DIV"); - "use strict"; + // contents box (inside the background box). used for making margins + dom.content = document.createElement("DIV"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - // English - exports.en = { - current: "current", - time: "time" - }; - exports.en_EN = exports.en; - exports.en_US = exports.en; + // line to axis + dom.line = document.createElement("DIV"); + dom.line.className = "line"; - // Dutch - exports.nl = { - custom: "aangepaste", - time: "tijd" - }; - exports.nl_NL = exports.nl; - exports.nl_BE = exports.nl; + // dot on axis + dom.dot = document.createElement("DIV"); + dom.dot.className = "dot"; -/***/ }, -/* 45 */ -/***/ function(module, exports, __webpack_require__) { + // attach this item as attribute + dom.box["timeline-item"] = this; - "use strict"; + this.dirty = true; + } - /** - * Created by Alex on 11/11/2014. - */ - var DOMutil = __webpack_require__(2); - var Points = __webpack_require__(47); + // 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; - function Line(groupId, options) { - this.groupId = groupId; - this.options = options; - } + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.box); + this._updateDataAttributes(this.dom.box); + this._updateStyle(this.dom.box); - 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 class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = "item box" + className; + dom.line.className = "item line" + className; + dom.dot.className = "item dot" + className; + + // recalculate size + this.props.dot.height = dom.dot.offsetHeight; + this.props.dot.width = dom.dot.offsetWidth; + this.props.line.width = dom.line.offsetWidth; + this.width = dom.box.offsetWidth; + this.height = dom.box.offsetHeight; + + this.dirty = false; } - return { min: yMin, max: yMax, yAxisOrientation: this.options.yAxisOrientation }; + + this._repaintDeleteButton(dom.box); }; + /** + * Show the item in the DOM (when not already displayed). The items DOM will + * be created when needed. + */ + BoxItem.prototype.show = function () { + if (!this.displayed) { + this.redraw(); + } + }; /** - * draw a line graph - * - * @param dataset - * @param group + * Hide the item from the DOM (when visible) */ - 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); - } + BoxItem.prototype.hide = function () { + if (this.displayed) { + var dom = this.dom; - // construct path from dataset - if (group.options.catmullRom.enabled == true) { - d = Line._catmullRom(dataset, group); - } else { - d = Line._linear(dataset); - } + 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); - // 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); + this.top = null; + this.left = null; - // draw points - if (group.options.drawPoints.enabled == true) { - Points.draw(dataset, group, framework); - } - } + this.displayed = false; } }; + /** + * Reposition the item horizontally + * @Override + */ + BoxItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); + var align = this.options.align; + var left; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; + + // calculate left position of the box + if (align == "right") { + this.left = start - this.width; + } else if (align == "left") { + this.left = start; + } else { + // default or 'center' + this.left = start - this.width / 2; + } + + // reposition box + box.style.left = this.left + "px"; + + // reposition line + line.style.left = start - this.props.line.width / 2 + "px"; + // reposition dot + dot.style.left = start - this.props.dot.width / 2 + "px"; + }; /** - * This uses an uniform parametrization of the CatmullRom algorithm: - * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al. - * @param data - * @returns {string} - * @private + * Reposition the item vertically + * @Override */ - 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; + BoxItem.prototype.repositionY = function () { + var orientation = this.options.orientation; + var box = this.dom.box; + var line = this.dom.line; + var dot = this.dom.dot; + if (orientation == "top") { + box.style.top = (this.top || 0) + "px"; - // 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 + 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; - // 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 }; + box.style.top = (this.parent.height - this.top - this.height || 0) + "px"; + line.style.top = itemSetHeight - lineHeight + "px"; + line.style.bottom = "0"; + } - d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; + dot.style.top = -this.props.dot.height / 2 + "px"; + }; + + module.exports = BoxItem; + +/***/ }, +/* 36 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var Item = __webpack_require__(33); + + /** + * @constructor PointItem + * @extends Item + * @param {Object} data Object containing parameters start + * content, className. + * @param {{toScreen: function, toTime: function}} conversion + * Conversion functions from time to screen and vice versa + * @param {Object} [options] Configuration options + * // TODO: describe available options + */ + function PointItem(data, conversion, options) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; + + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data); + } } - return d; + 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; }; /** - * 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 + * Repaint the item */ - 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; + PointItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - 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)); + // background box + dom.point = document.createElement("div"); + // className is updated in redraw() - // Catmull-Rom to Cubic Bezier conversion matrix + // contents box, right from the dot + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.point.appendChild(dom.content); - // A = 2d1^2a + 3d1^a * d2^a + d3^2a - // B = 2d3^2a + 3d3^a * d2^a + d2^2a + // dot at start + dom.dot = document.createElement("div"); + dom.point.appendChild(dom.dot); + + // attach this item as attribute + dom.point["timeline-item"] = this; + + this.dirty = true; + } + + // append DOM to parent DOM + if (!this.parent) { + throw new Error("Cannot redraw item: no parent attached"); + } + if (!dom.point.parentNode) { + var foreground = this.parent.dom.foreground; + if (!foreground) { + throw new Error("Cannot redraw item: parent has no foreground container element"); + } + foreground.appendChild(dom.point); + } + this.displayed = true; + + // Update DOM when item is marked dirty. An item is marked dirty when: + // - the item is not yet rendered + // - the item's data is changed + // - the item is selected/deselected + if (this.dirty) { + this._updateContents(this.dom.content); + this._updateTitle(this.dom.point); + this._updateDataAttributes(this.dom.point); + this._updateStyle(this.dom.point); - // [ 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 ] + // 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; - 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); + // 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; - 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; - } + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + "px"; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, - y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + dom.dot.style.top = (this.height - this.props.dot.height) / 2 + "px"; + dom.dot.style.left = this.props.dot.width / 2 + "px"; - bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, - y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; + this.dirty = false; + } - if (bp1.x == 0 && bp1.y == 0) { - bp1 = p1; - } - if (bp2.x == 0 && bp2.y == 0) { - bp2 = p2; - } - d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; - } + this._repaintDeleteButton(dom.point); + }; - return d; + /** + * 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(); } }; /** - * this generates the SVG path for a linear drawing between datapoints. - * @param data - * @returns {string} - * @private + * Hide the item from the DOM (when visible) */ - 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; + 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; } - return d; }; - module.exports = Line; + /** + * Reposition the item horizontally + * @Override + */ + PointItem.prototype.repositionX = function () { + var start = this.conversion.toScreen(this.data.start); -/***/ }, -/* 46 */ -/***/ function(module, exports, __webpack_require__) { + this.left = start - this.props.dot.width; - "use strict"; + // reposition point + this.dom.point.style.left = this.left + "px"; + }; /** - * Created by Alex on 11/11/2014. + * Reposition the item vertically + * @Override */ - var DOMutil = __webpack_require__(2); - var Points = __webpack_require__(47); - - function Bargraph(groupId, options) { - this.groupId = groupId; - this.options = options; - } + PointItem.prototype.repositionY = function () { + var orientation = this.options.orientation, + point = this.dom.point; - 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 }; + if (orientation == "top") { + point.style.top = this.top + "px"; } 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; + point.style.top = this.parent.height - this.top - this.height + "px"; } }; + module.exports = PointItem; + +/***/ }, +/* 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); /** - * draw a bar graph - * - * @param groupIds - * @param processedGroupData + * @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 */ - 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; - } - } + // 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 } - } - - if (barPoints == 0) { - return; - } + }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true - // sort by time and by group - combinedData.sort(function (a, b) { - if (a.x == b.x) { - return a.groupId - b.groupId; - } else { - return a.x - b.x; + // validate data + if (data) { + if (data.start == undefined) { + throw new Error("Property \"start\" missing in item " + data.id); } - }); - - // get intersections - Bargraph._getDataIntersections(intersections, combinedData); + if (data.end == undefined) { + throw new Error("Property \"end\" missing in item " + data.id); + } + } - // plot barchart - for (i = 0; i < combinedData.length; i++) { - group = framework.groups[combinedData[i].groupId]; - var minWidth = 0.1 * group.options.barChart.width; + Item.call(this, data, conversion, options); - key = combinedData[i].x; - var heightOffset = 0; - if (intersections[key] === undefined) { - if (i + 1 < combinedData.length) { - coreDistance = Math.abs(combinedData[i + 1].x - key); - } - if (i > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); - } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - } else { - var nextKey = i + (intersections[key].amount - intersections[key].resolved); - var prevKey = i - (intersections[key].resolved + 1); - if (nextKey < combinedData.length) { - coreDistance = Math.abs(combinedData[nextKey].x - key); - } - if (prevKey > 0) { - coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); - } - drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); - intersections[key].resolved += 1; + this.emptyContent = false; + } - 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); - } - } - }; + BackgroundItem.prototype = new Item(null, null, null); + BackgroundItem.prototype.baseClassName = "item background"; + BackgroundItem.prototype.stack = false; /** - * Fill the intersections object with counters of how many datapoints share the same x coordinates - * @param intersections - * @param combinedData - * @private + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible */ - 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; - } - } + BackgroundItem.prototype.isVisible = function (range) { + // determine visibility + return this.data.start < range.end && this.data.end > range.start; }; - /** - * 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 + * Repaint the item */ - Bargraph._getSafeDrawData = function (coreDistance, group, minWidth) { - var width, offset; - if (coreDistance < group.options.barChart.width && coreDistance > 0) { - width = coreDistance < minWidth ? minWidth : coreDistance; + BackgroundItem.prototype.redraw = function () { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - 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; - } - } + // background box + dom.box = document.createElement("div"); + // className is updated in redraw() - return { width: width, offset: offset }; - }; + // contents box + dom.content = document.createElement("div"); + dom.content.className = "content"; + dom.box.appendChild(dom.content); - 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 = {}; + // Note: we do NOT attach this item as attribute to the DOM, + // such that background items cannot be selected + //dom.box['timeline-item'] = this; - Bargraph._getDataIntersections(intersections, barCombinedData); - groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData); - groupRanges[groupLabel].yAxisOrientation = orientation; - groupIds.push(groupLabel); + this.dirty = true; } - }; - 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; - } + // append DOM to parent DOM + if (!this.parent) { + throw new Error("Cannot redraw item: no parent attached"); } - for (var xpos in intersections) { - if (intersections.hasOwnProperty(xpos)) { - yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin; - yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax; + if (!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; - return { min: yMin, max: yMax }; - }; + // 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); - module.exports = Bargraph; + // update class + var className = (this.data.className ? " " + this.data.className : "") + (this.selected ? " selected" : ""); + dom.box.className = this.baseClassName + className; -/***/ }, -/* 47 */ -/***/ function(module, exports, __webpack_require__) { + // determine from css whether this box has overflow + this.overflow = window.getComputedStyle(dom.content).overflow !== "hidden"; - "use strict"; + // 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; + } + }; /** - * Created by Alex on 11/11/2014. + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ - var DOMutil = __webpack_require__(2); - - function Points(groupId, options) { - this.groupId = groupId; - this.options = options; - } - + BackgroundItem.prototype.show = RangeItem.prototype.show; - 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 }; - }; + /** + * Hide the item from the DOM (when visible) + * @return {Boolean} changed + */ + BackgroundItem.prototype.hide = RangeItem.prototype.hide; - Points.prototype.draw = function (dataset, group, framework, offset) { - Points.draw(dataset, group, framework, offset); - }; + /** + * Reposition the item horizontally + * @Override + */ + BackgroundItem.prototype.repositionX = RangeItem.prototype.repositionX; /** - * draw the data points - * - * @param {Array} dataset - * @param {Object} JSONcontainer - * @param {Object} svg | SVG DOM element - * @param {GraphGroup} group - * @param {Number} [offset] + * Reposition the item vertically + * @Override */ - Points.draw = function (dataset, group, framework, offset) { - if (offset === undefined) { - offset = 0; + 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 = ""; + } } - 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); + // and in the case of no subgroups: + else { + // we want backgrounds with groups to only show in groups. + if (this.parent instanceof BackgroundGroup) { + // if the item is not in a group: + height = Math.max(this.parent.height, this.parent.itemSet.body.domProps.center.height, this.parent.itemSet.body.domProps.centerContainer.height); + this.dom.box.style.top = onTop ? "0" : ""; + this.dom.box.style.bottom = onTop ? "" : "0"; + } else { + height = this.parent.height; + // same alignment for items when orientation is top or bottom + this.dom.box.style.top = this.parent.top + "px"; + this.dom.box.style.bottom = ""; + } } + this.dom.box.style.height = height + "px"; }; - - module.exports = Points; + module.exports = BackgroundItem; /***/ }, -/* 48 */ +/* 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); + /** - * Canvas shapes used by Network + * 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 */ - 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); - }; + function Activator(container) { + this.active = 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); + this.dom = { + container: container }; - /** - * 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(); + this.dom.overlay = document.createElement("div"); + this.dom.overlay.className = "overlay"; - // the change in radius and the offset is here to center the shape - r *= 1.15; - y += 0.275 * r; + this.dom.container.appendChild(this.dom.overlay); - 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.hammer = Hammer(this.dom.overlay, { prevent_default: false }); + this.hammer.on("tap", this._onTapOverlay.bind(this)); + // block all touch events (except tap) + var me = this; + var events = ["touch", "pinch", "doubletap", "hold", "dragstart", "drag", "dragend", "mousewheel", "DOMMouseScroll" // DOMMouseScroll is needed for Firefox + ]; + events.forEach(function (event) { + me.hammer.on(event, function (event) { + event.stopPropagation(); + }); + }); - 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(); + // attach a tap event to the window, in order to deactivate when clicking outside the timeline + this.windowHammer = Hammer(window, { prevent_default: false }); + this.windowHammer.on("tap", function (event) { + // deactivate when clicked outside the container + if (!_hasParent(event.target, container)) { + me.deactivate(); + } + }); - }; + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } + this.keycharm = keycharm(); - /** - * 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(); + // keycharm listener only bounded when active) + this.escListener = this.deactivate.bind(this); + } - // the change in radius and the offset is here to center the shape - r *= 1.15; - y -= 0.275 * r; + // turn into an event emitter + Emitter(Activator.prototype); - 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 + // The currently active activator + Activator.current = null; - this.moveTo(x, y + (h - ir)); - this.lineTo(x + s2, y - ir); - this.lineTo(x - s2, y - ir); - this.lineTo(x, y + (h - ir)); - this.closePath(); - }; + /** + * Destroy the activator. Cleans up all created DOM and event listeners + */ + Activator.prototype.destroy = function () { + this.deactivate(); - /** - * 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(); + // remove dom + this.dom.overlay.parentNode.removeChild(this.dom.overlay); - // the change in radius and the offset is here to center the shape - r *= 0.82; - y += 0.1 * r; + // cleanup hammer instances + this.hammer = null; + this.windowHammer = null; + // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + }; - 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)); - } + /** + * Activate the element + * Overlay is hidden, element is decorated with a blue shadow border + */ + Activator.prototype.activate = function () { + // we allow only one active activator at a time + if (Activator.current) { + Activator.current.deactivate(); + } + Activator.current = this; - this.closePath(); - }; + this.active = true; + this.dom.overlay.style.display = "none"; + util.addClassName(this.dom.container, "vis-active"); - /** - * Draw a Diamond shape - * @param {Number} x horizontal center - * @param {Number} y vertical center - * @param {Number} r radius, half the length of the sides of the triangle - */ - CanvasRenderingContext2D.prototype.diamond = function (x, y, r) { - // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ - this.beginPath(); + this.emit("change"); + this.emit("activate"); - this.lineTo(x, y + r); - this.lineTo(x + r, y); - this.lineTo(x, y - r); - this.lineTo(x - r, y); + // 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); + }; + /** + * 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); - this.closePath(); - }; + this.emit("change"); + this.emit("deactivate"); + }; - /** - * 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); - }; + /** + * Handle a tap event: activate the container + * @param event + * @private + */ + Activator.prototype._onTapOverlay = function (event) { + // activate the container + this.activate(); + event.stopPropagation(); + }; - /** - * 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 + /** + * 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; + } - 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); - }; + module.exports = Activator; +/***/ }, +/* 39 */ +/***/ function(module, exports, __webpack_require__) { - /** - * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas - */ - CanvasRenderingContext2D.prototype.database = function (x, y, w, h) { - var f = 1 / 3; - var wEllipse = w; - var hEllipse = h * f; + var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; + /** + * Created by Alex on 11/6/2014. + */ - 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 + // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 + // if the module has no dependencies, the above pattern can be simplified to + (function (root, factory) { + if (true) { + // AMD. Register as an anonymous module. + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.keycharm = factory(); + } + }(this, function () { - this.beginPath(); - this.moveTo(xe, ym); + function keycharm(options) { + var preventDefault = options && options.preventDefault || false; - this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); - this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + var container = options && options.container || window; + var _exportFunctions = {}; + var _bound = {keydown:{}, keyup:{}}; + var _keys = {}; + var i; - this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); - this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + // 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};} - this.lineTo(xe, ymb); + // 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}; - this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); - this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); - this.lineTo(x, ym); - }; + var down = function(event) {handleEvent(event,'keydown');}; + var up = function(event) {handleEvent(event,'keyup');}; - /** - * 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); + // 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); + } + } - // inner tail - // TODO: allow to customize different shapes - var xi = x - length * 0.9 * Math.cos(angle); - var yi = y - length * 0.9 * Math.sin(angle); + if (preventDefault == true) { + event.preventDefault(); + } + } + }; - // 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); + // bind a key to a callback + _exportFunctions.bind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (_bound[type][_keys[key].code] === undefined) { + _bound[type][_keys[key].code] = []; + } + _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); + }; - // 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.beginPath(); - this.moveTo(x, y); - this.lineTo(xl, yl); - this.lineTo(xi, yi); - this.lineTo(xr, yr); - this.closePath(); - }; + // bind all keys to a call back (demo purposes) + _exportFunctions.bindAll = function(callback, type) { + if (type === undefined) { + type = 'keydown'; + } + for (var key in _keys) { + if (_keys.hasOwnProperty(key)) { + _exportFunctions.bind(key,callback,type); + } + } + }; + + // get the key label from an event + _exportFunctions.getKey = function(event) { + for (var key in _keys) { + if (_keys.hasOwnProperty(key)) { + if (event.shiftKey == true && _keys[key].shift == true && event.keyCode == _keys[key].code) { + return key; + } + else if (event.shiftKey == false && _keys[key].shift == false && event.keyCode == _keys[key].code) { + return key; + } + else if (event.keyCode == _keys[key].code && key == 'shift') { + return key; + } + } + } + return "unknown key, currently not supported"; + }; + + // unbind either a specific callback from a key or all of them (by leaving callback undefined) + _exportFunctions.unbind = function(key, callback, type) { + if (type === undefined) { + type = 'keydown'; + } + if (_keys[key] === undefined) { + throw new Error("unsupported key: " + key); + } + if (callback !== undefined) { + var newBindings = []; + var bound = _bound[type][_keys[key].code]; + if (bound !== undefined) { + for (var i = 0; i < bound.length; i++) { + if (!(bound[i].fn == callback && bound[i].shift == _keys[key].shift)) { + newBindings.push(_bound[type][_keys[key].code][i]); + } + } + } + _bound[type][_keys[key].code] = newBindings; + } + else { + _bound[type][_keys[key].code] = []; + } + }; + + // reset all bound variables. + _exportFunctions.reset = function() { + _bound = {keydown:{}, keyup:{}}; + }; + + // unbind all listeners and reset all variables. + _exportFunctions.destroy = function() { + _bound = {keydown:{}, keyup:{}}; + container.removeEventListener('keydown', down, true); + container.removeEventListener('keyup', up, true); + }; + + // create listeners. + container.addEventListener('keydown',down,true); + container.addEventListener('keyup',up,true); + + // return the public functions. + return _exportFunctions; + } + + return keycharm; + })); + - /** - * 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; - } - }; - // TODO: add diamond shape - } /***/ }, -/* 49 */ +/* 40 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var keycharm = __webpack_require__(78); - var Emitter = __webpack_require__(62); - var Hammer = __webpack_require__(41); + 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); /** - * 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 + * A custom time bar + * @param {{range: Range, dom: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCustomTime] + * @constructor CustomTime + * @extends Component */ - function Activator(container) { - this.active = false; - - this.dom = { - container: container - }; - - this.dom.overlay = document.createElement("div"); - this.dom.overlay.className = "overlay"; - this.dom.container.appendChild(this.dom.overlay); + function CustomTime(body, options) { + this.body = body; - this.hammer = Hammer(this.dom.overlay, { prevent_default: false }); - this.hammer.on("tap", this._onTapOverlay.bind(this)); + // default options + this.defaultOptions = { + showCustomTime: false, + locales: locales, + locale: "en", + id: 0 + }; + this.options = util.extend({}, this.defaultOptions); - // 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(); - }); - }); + if (options && options.time) { + this.customTime = options.time; + } else { + this.customTime = new Date(); + } - // 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.eventParams = {}; // stores state parameters while dragging the bar - if (this.keycharm !== undefined) { - this.keycharm.destroy(); - } - this.keycharm = keycharm(); + // create the DOM + this._create(); - // keycharm listener only bounded when active) - this.escListener = this.deactivate.bind(this); + this.setOptions(options); } - // turn into an event emitter - Emitter(Activator.prototype); - - // The currently active activator - Activator.current = null; + CustomTime.prototype = new Component(); /** - * Destroy the activator. Cleans up all created DOM and event listeners + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCustomTime] */ - Activator.prototype.destroy = function () { - this.deactivate(); - - // remove dom - this.dom.overlay.parentNode.removeChild(this.dom.overlay); + CustomTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(["showCustomTime", "locale", "locales", "id"], this.options, options); - // cleanup hammer instances - this.hammer = null; - this.windowHammer = null; - // FIXME: cleaning up hammer instances doesn't work (Timeline not removed from memory) + // Triggered by addCustomTimeBar, redraw to add new bar + if (this.options.id) { + this.redraw(); + } + } }; /** - * Activate the element - * Overlay is hidden, element is decorated with a blue shadow border + * Create the DOM for the custom time + * @private */ - Activator.prototype.activate = function () { - // we allow only one active activator at a time - if (Activator.current) { - Activator.current.deactivate(); - } - Activator.current = this; - - this.active = true; - this.dom.overlay.style.display = "none"; - util.addClassName(this.dom.container, "vis-active"); + 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; - this.emit("change"); - this.emit("activate"); + 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); - // 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); + // 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(); + }); }; /** - * Deactivate the element - * Overlay is displayed on top of the element + * Destroy the CustomTime bar */ - 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); + CustomTime.prototype.destroy = function () { + this.options.showCustomTime = false; + this.redraw(); // will remove the bar from the DOM - this.emit("change"); - this.emit("deactivate"); - }; + this.hammer.enable(false); + this.hammer = null; - /** - * Handle a tap event: activate the container - * @param event - * @private - */ - Activator.prototype._onTapOverlay = function (event) { - // activate the container - this.activate(); - event.stopPropagation(); + this.body = null; }; /** - * Test whether the element has the requested parent element somewhere in - * its chain of parent nodes. - * @param {HTMLElement} element - * @param {HTMLElement} parent - * @returns {boolean} Returns true when the parent is found somewhere in the - * chain of parent nodes. - * @private + * Repaint the component + * @return {boolean} Returns true if the component is resized */ - function _hasParent(element, parent) { - while (element) { - if (element === parent) { - return true; + 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); } - element = element.parentNode; - } - return false; - } - - module.exports = Activator; - -/***/ }, -/* 50 */ -/***/ function(module, exports, __webpack_require__) { - "use strict"; + var x = this.body.util.toScreen(this.customTime); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + 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); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + 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); + } + } - var util = __webpack_require__(1); + return false; + }; /** - * @class Groups - * This class can store groups and options specific for groups. + * Set custom time. + * @param {Date | number | string} time */ - var Groups = (function () { - function Groups() { - _classCallCheck(this, Groups); - - this.clear(); - this.defaultIndex = 0; - this.groupsArray = []; - this.groupIndex = 0; - - this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue - { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow - { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red - { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green - { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta - { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple - { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange - { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue - { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink - { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint - - { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - - { border: "#FF6000", background: "#FF6000", highlight: { border: "#FF6000", background: "#FF6000" }, hover: { border: "#FF6000", background: "#FF6000" } }, // 12: real orange - { border: "#97C2FC", background: "#2B7CE9", highlight: { border: "#D2E5FF", background: "#2B7CE9" }, hover: { border: "#D2E5FF", background: "#2B7CE9" } }, // 13: blue - { border: "#399605", background: "#255C03", highlight: { border: "#399605", background: "#255C03" }, hover: { border: "#399605", background: "#255C03" } }, // 14: green - { border: "#B70054", background: "#FF007E", highlight: { border: "#B70054", background: "#FF007E" }, hover: { border: "#B70054", background: "#FF007E" } }, // 15: magenta - { border: "#AD85E4", background: "#7C29F0", highlight: { border: "#D3BDF0", background: "#7C29F0" }, hover: { border: "#D3BDF0", background: "#7C29F0" } }, // 16: purple - { border: "#4557FA", background: "#000EA1", highlight: { border: "#6E6EFD", background: "#000EA1" }, hover: { border: "#6E6EFD", background: "#000EA1" } }, // 17: darkblue - { border: "#FFC0CB", background: "#FD5A77", highlight: { border: "#FFD1D9", background: "#FD5A77" }, hover: { border: "#FFD1D9", background: "#FD5A77" } }, // 18: pink - { border: "#C2FABC", background: "#74D66A", highlight: { border: "#E6FFE3", background: "#74D66A" }, hover: { border: "#E6FFE3", background: "#74D66A" } }, // 19: mint - - { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; - - this.options = {}; - this.defaultOptions = { - useDefaultGroups: true - }; - util.extend(this.options, this.defaultOptions); - } - - _prototypeProperties(Groups, null, { - setOptions: { - value: function setOptions(options) { - var optionFields = ["useDefaultGroups"]; - - if (options !== undefined) { - for (var groupName in options) { - if (options.hasOwnProperty(groupName)) { - if (optionFields.indexOf(groupName) == -1) { - var group = options[groupName]; - this.add(groupName, group); - } - } - } - } - }, - writable: true, - configurable: true - }, - clear: { - - - /** - * Clear all groups - */ - value: function clear() { - this.groups = {}; - this.groupsArray = []; - }, - writable: true, - configurable: true - }, - get: { - - /** - * get group options of a groupname. If groupname is not found, a new group - * is added. - * @param {*} groupname Can be a number, string, Date, etc. - * @return {Object} group The created group, containing all group options - */ - value: function get(groupname) { - var group = this.groups[groupname]; - if (group == undefined) { - if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { - // create new group - var index = this.groupIndex % this.groupsArray.length; - this.groupIndex++; - group = {}; - group.color = this.groups[this.groupsArray[index]]; - this.groups[groupname] = group; - } else { - // create new group - var index = this.defaultIndex % this.defaultGroups.length; - this.defaultIndex++; - group = {}; - group.color = this.defaultGroups[index]; - this.groups[groupname] = group; - } - } - - return group; - }, - writable: true, - configurable: true - }, - add: { - - /** - * Add a custom group style - * @param {String} groupName - * @param {Object} style An object containing borderColor, - * backgroundColor, etc. - * @return {Object} group The created group object - */ - value: function add(groupName, style) { - this.groups[groupName] = style; - this.groupsArray.push(groupName); - return style; - }, - writable: true, - configurable: true - } - }); - - return Groups; - })(); - - module.exports = Groups; - // 20:bright red - -/***/ }, -/* 51 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + CustomTime.prototype.setCustomTime = function (time) { + this.customTime = util.convert(time, "Date"); + this.redraw(); + }; /** - * Created by Alex on 3/4/2015. + * Retrieve the current custom time. + * @return {Date} customTime */ + CustomTime.prototype.getCustomTime = function () { + return new Date(this.customTime.valueOf()); + }; - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); - - var Node = _interopRequire(__webpack_require__(66)); - - var NodesHandler = (function () { - function NodesHandler(body, images, groups, layoutEngine) { - var _this = this; - _classCallCheck(this, NodesHandler); - - this.body = body; - this.images = images; - this.groups = groups; - this.layoutEngine = layoutEngine; - - // create the node API in the body container - this.body.functions.createNode = this.create.bind(this); - - this.nodesListeners = { - add: function (event, params) { - _this.add(params.items); - }, - update: function (event, params) { - _this.update(params.items, params.data); - }, - remove: function (event, params) { - _this.remove(params.items); - } - }; - + /** + * Start moving horizontally + * @param {Event} event + * @private + */ + CustomTime.prototype._onDragStart = function (event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; - this.options = {}; - this.defaultOptions = { - borderWidth: 1, - borderWidthSelected: undefined, - color: { - border: "#2B7CE9", - background: "#97C2FC", - highlight: { - border: "#2B7CE9", - background: "#D2E5FF" - }, - hover: { - border: "#2B7CE9", - background: "#D2E5FF" - } - }, - fixed: { - x: false, - y: false - }, - font: { - color: "#343434", - size: 14, // px - face: "arial", - background: "none", - stroke: 0, // px - strokeColor: "white", - align: "horizontal" - }, - group: undefined, - hidden: false, - icon: { - face: undefined, //'FontAwesome', - code: undefined, //'\uf007', - size: undefined, //50, - color: undefined //'#aa00ff' - }, - image: undefined, // --> URL - label: undefined, - level: undefined, - mass: 1, - physics: true, - scaling: { - min: 10, - max: 30, - label: { - enabled: true, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 3 - }, - customScalingFunction: function (min, max, total, value) { - if (max == min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - shape: "ellipse", - size: 25, - value: 1, - x: undefined, - y: undefined - }; - util.extend(this.options, this.defaultOptions); - } + event.stopPropagation(); + event.preventDefault(); + }; - _prototypeProperties(NodesHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options) { - util.selectiveNotDeepExtend(["color"], this.options, options); - if (options.color) { - this.options.color = util.parseColor(options.color); - } - } - }, - writable: true, - configurable: true - }, - setData: { + /** + * Perform moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; - /** - * Set a data set with nodes for the network - * @param {Array | DataSet | DataView} nodes The data containing the nodes. - * @private - */ - value: function setData(nodes) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var oldNodesData = this.body.data.nodes; + var x = this.body.util.toScreen(this.eventParams.customTime) + event.deltaX; + var time = this.body.util.toTime(x); - 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"); - } + this.setCustomTime(time); - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.off(event, callback); - }); - } + // fire a timechange event + this.body.emitter.emit("timechange", { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - // remove drawn nodes - this.body.nodes = {}; + event.stopPropagation(); + event.preventDefault(); + }; - 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); - }); + /** + * Stop moving operating. + * @param {Event} event + * @private + */ + CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; - // draw all new nodes - var ids = this.body.data.nodes.getIds(); - this.add(ids, true); - } + // fire a timechanged event + this.body.emitter.emit("timechanged", { + id: this.options.id, + time: new Date(this.customTime.valueOf()) + }); - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - add: { + event.stopPropagation(); + event.preventDefault(); + }; + module.exports = CustomTime; - /** - * Add nodes - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var id; - var newNodes = []; - for (var i = 0; i < ids.length; i++) { - id = ids[i]; - var properties = this.body.data.nodes.get(id); - var node = this.create(properties);; - newNodes.push(node); - this.body.nodes[id] = node; // note: this may replace an existing node - } +/***/ }, +/* 41 */ +/***/ function(module, exports, __webpack_require__) { - this.layoutEngine.positionInitially(newNodes); + "use strict"; - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - update: { + // English + exports.en = { + current: "current", + time: "time" + }; + exports.en_EN = exports.en; + exports.en_US = exports.en; - /** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids, changedData) { - var nodes = this.body.nodes; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = changedData[i]; - if (node !== undefined) { - // update node - node.setOptions(data, this.constants); - } else { - dataChanged = true; - // create node - node = this.create(properties); - nodes[id] = node; - } - } + // Dutch + exports.nl = { + custom: "aangepaste", + time: "tijd" + }; + exports.nl_NL = exports.nl; + exports.nl_BE = exports.nl; - if (dataChanged === true) { - this.body.emitter.emit("_dataChanged"); - } else { - this.body.emitter.emit("_dataUpdated"); - } - }, - writable: true, - configurable: true - }, - remove: { +/***/ }, +/* 42 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var nodes = this.body.nodes; + "use strict"; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - delete nodes[id]; - } + 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); - this.body.emitter.emit("_dataChanged"); - }, - writable: true, - configurable: true - }, - create: { - value: function create(properties) { - var constructorClass = arguments[1] === undefined ? Node : arguments[1]; - return new constructorClass(properties, this.body, this.images, this.groups, this.options); - }, - writable: true, - configurable: true + /** + * A horizontal time axis + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body + * @param {Object} [options] See TimeAxis.setOptions for the available + * options. + * @constructor TimeAxis + * @extends Component + */ + function TimeAxis(body, options) { + this.dom = { + foreground: null, + lines: [], + majorTexts: [], + minorTexts: [], + redundant: { + lines: [], + majorTexts: [], + minorTexts: [] } - }); + }; + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 + }, + lineTop: 0 + }; - return NodesHandler; - })(); + 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); - module.exports = NodesHandler; + this.body = body; -/***/ }, -/* 52 */ -/***/ function(module, exports, __webpack_require__) { + // create the HTML DOM + this._create(); - "use strict"; + this.setOptions(options); + } - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + TimeAxis.prototype = new Component(); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + /** + * 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); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // 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); + } + } + } + }; /** - * Created by Alex on 3/4/2015. + * Create the HTML DOM for the TimeAxis */ + TimeAxis.prototype._create = function () { + this.dom.foreground = document.createElement("div"); + this.dom.background = document.createElement("div"); + this.dom.foreground.className = "timeaxis foreground"; + this.dom.background.className = "timeaxis background"; + }; - var util = __webpack_require__(1); - var DataSet = __webpack_require__(3); - var DataView = __webpack_require__(4); + /** + * 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); + } - var Edge = _interopRequire(__webpack_require__(67)); + this.body = null; + }; - var EdgesHandler = (function () { - function EdgesHandler(body, images, groups) { - var _this = this; - _classCallCheck(this, EdgesHandler); + /** + * 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; - this.body = body; - this.images = images; - this.groups = groups; + // determine the correct parent DOM element (depending on option orientation) + var parent = options.orientation == "top" ? this.body.dom.top : this.body.dom.bottom; + var parentChanged = foreground.parentNode !== parent; - // create the edge API in the body container - this.body.functions.createEdge = this.create.bind(this); + // calculate character width and height + this._calculateCharSize(); - this.edgesListeners = { - add: function (event, params) { - _this.add(params.items); - }, - update: function (event, params) { - _this.update(params.items); - }, - remove: function (event, params) { - _this.remove(params.items); - } - }; + // 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; - this.options = {}; - this.defaultOptions = { - arrows: { - to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} - middle: { enabled: false, scaleFactor: 1 }, - from: { enabled: false, scaleFactor: 1 } - }, - color: { - color: "#848484", - highlight: "#848484", - hover: "#848484", - inherit: { - enabled: true, - source: "from", // from / true - useGradients: false // release in 4.0 - }, - opacity: 1 - }, - dashes: { - enabled: false, - preset: "dotted", - length: 10, - gap: 5, - altLength: undefined - }, - font: { - color: "#343434", - size: 14, // px - face: "arial", - background: "none", - stroke: 1, // px - strokeColor: "white", - align: "horizontal" - }, - hidden: false, - hoverWidth: 1.5, - label: undefined, - length: undefined, - physics: true, - scaling: { - min: 1, - max: 15, - label: { - enabled: true, - min: 14, - max: 30, - maxVisible: 30, - drawThreshold: 3 - }, - customScalingFunction: function (min, max, total, value) { - if (max == min) { - return 0.5; - } else { - var scale = 1 / (max - min); - return Math.max(0, (value - min) * scale); - } - } - }, - selfReferenceSize: 20, - smooth: { - enabled: true, - dynamic: true, - type: "continuous", - roundness: 0.5 - }, - title: undefined, - width: 1, - widthSelectionMultiplier: 2, - value: 1 - }; + // 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; - util.extend(this.options, this.defaultOptions); + 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); - // this allows external modules to force all dynamic curves to turn static. - this.body.emitter.on("_forceDisableDynamicCurves", function (type) { - var emitChange = false; - for (var edgeId in _this.body.edges) { - if (_this.body.edges.hasOwnProperty(edgeId)) { - var edgeOptions = _this.body.edges[edgeId].options.smooth; - if (edgeOptions.enabled === true && edgeOptions.dynamic === true) { - if (type === undefined) { - edge.setOptions({ smooth: false }); - } else { - edge.setOptions({ smooth: { dynamic: false, type: type } }); - } - emitChange = true; - } - } - } - if (emitChange === true) { - _this.body.emitter.emit("_dataChanged"); - } - }); + foreground.style.height = this.props.height + "px"; - // this is called when options of EXISTING nodes or edges have changed. - this.body.emitter.on("_dataUpdated", function () { - _this.reconnectEdges(); - _this.markAllEdgesAsDirty(); - }); + 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); } - _prototypeProperties(EdgesHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.mergeOptions(this.options, options, "smooth"); - util.mergeOptions(this.options, options, "dashes"); + return this._isResized() || parentChanged; + }; - // hanlde multiple input cases for arrows - if (options.arrows !== undefined) { - if (typeof options.arrows === "string") { - var arrows = options.arrows.toLowerCase(); - if (arrows.indexOf("to") != -1) { - this.options.arrows.to.enabled = true; - } - if (arrows.indexOf("middle") != -1) { - this.options.arrows.middle.enabled = true; - } - if (arrows.indexOf("from") != -1) { - this.options.arrows.from.enabled = true; - } - } else if (typeof options.arrows === "object") { - util.mergeOptions(this.options.arrows, options.arrows, "to"); - util.mergeOptions(this.options.arrows, options.arrows, "middle"); - util.mergeOptions(this.options.arrows, options.arrows, "from"); - } else { - throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); - } - } + /** + * Repaint major and minor text labels and vertical grid lines + * @private + */ + TimeAxis.prototype._repaintLabels = function () { + var orientation = this.options.orientation; - // hanlde multiple input cases for color - if (options.color !== undefined) { - if (util.isString(options.color)) { - util.assignAllKeys(this.options.color, options.color); - this.options.color.inherit.enabled = false; - } else { - util.extend(this.options.color, options.color); - if (options.color.inherit === undefined) { - this.options.color.inherit.enabled = false; - } - } - util.mergeOptions(this.options.color, options.color, "inherit"); - } + // 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 = []; - // font cases are handled by the Label class - } - }, - writable: true, - configurable: true - }, - setData: { + 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++; - /** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private - * @private - */ - value: function setData(edges) { - var _this = this; - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var oldEdgesData = this.body.data.edges; + cur = step.getCurrent(); + isMajor = step.isMajor(); + className = step.getClassName(); - 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"); - } + xPrev = x; + x = this.body.util.toScreen(cur); + width = x - xPrev; + if (prevLine) { + prevLine.style.width = width + "px"; + } - // TODO: is this null or undefined or false? - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.off(event, callback); - }); + if (this.options.showMinorLabels) { + this._repaintMinorText(x, step.getLabelMinor(), orientation, className); + } + + if (isMajor && this.options.showMajorLabels) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; } + this._repaintMajorText(x, step.getLabelMajor(), orientation, className); + } + prevLine = this._repaintMajorLine(x, orientation, className); + } else { + prevLine = this._repaintMinorLine(x, orientation, className); + } - // remove drawn edges - this.body.edges = {}; + step.next(); + } - // TODO: is this null or undefined or false? - if (this.body.data.edges) { - // subscribe to new dataset - util.forEach(this.edgesListeners, function (callback, event) { - _this.body.data.edges.on(event, callback); - }); + // create a major label on the left when needed + if (this.options.showMajorLabels) { + var leftTime = this.body.util.toTime(0), + leftText = step.getLabelMajor(leftTime), + widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation - // draw all new nodes - var ids = this.body.data.edges.getIds(); - this.add(ids, true); - } + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText, orientation, className); + } + } - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - add: { + // 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 + */ + TimeAxis.prototype._repaintMinorText = function (x, text, orientation, className) { + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); - /** - * Add edges - * @param {Number[] | String[]} ids - * @private - */ - value: function add(ids) { - var doNotEmit = arguments[1] === undefined ? false : arguments[1]; - var edges = this.body.edges; - var edgesData = this.body.data.edges; + 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); - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; + label.childNodes[0].nodeValue = text; - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); - } + 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 + }; - var data = edgesData.get(id, { showInternalIds: true }); - edges[id] = this.create(data); - } + /** + * 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(); - if (doNotEmit === false) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - update: { + 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); + label.childNodes[0].nodeValue = text; + label.className = "text major " + className; + //label.title = title; // TODO: this is a heavy operation + label.style.top = orientation == "top" ? "0" : this.props.minorLabelHeight + "px"; + label.style.left = x + "px"; + }; - /** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ - value: function update(ids) { - var edges = this.body.edges; - var edgesData = this.body.data.edges; - var dataChanged = false; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var data = edgesData.get(id); - var edge = edges[id]; - if (edge === null) { - // update edge - edge.disconnect(); - dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. - edge.connect(); - } else { - // create edge - this.body.edges[id] = this.create(data); - dataChanged = true; - } - } + /** + * Create a minor line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) + * @param {String} className + * @return {Element} Returns the created line + * @private + */ + TimeAxis.prototype._repaintMinorLine = function (x, orientation, className) { + // reuse redundant line + var line = this.dom.redundant.lines.shift(); + if (!line) { + // create vertical line + line = document.createElement("div"); + this.dom.background.appendChild(line); + } + this.dom.lines.push(line); - if (dataChanged === true) { - this.body.emitter.emit("_dataChanged"); - } else { - this.body.emitter.emit("_dataUpdated"); - } - }, - writable: true, - configurable: true - }, - remove: { + 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; + }; - /** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids - * @private - */ - value: function remove(ids) { - var edges = this.body.edges; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge !== undefined) { - if (edge.via != null) { - delete this.body.supportNodes[edge.via.id]; - } - edge.disconnect(); - delete edges[id]; - } - } + /** + * 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); + } + this.dom.lines.push(line); - this.body.emitter.emit("_dataChanged"); - }, - writable: true, - configurable: true - }, - create: { - value: function create(properties) { - return new Edge(properties, this.body, this.options); - }, - writable: true, - configurable: true - }, - markAllEdgesAsDirty: { - value: function markAllEdgesAsDirty() { - for (var edgeId in this.body.edges) { - this.body.edges[edgeId].colorDirty = true; - } - }, - writable: true, - configurable: true - }, - reconnectEdges: { + var props = this.props; + if (orientation == "top") { + line.style.top = "0"; + } else { + line.style.top = this.body.domProps.top.height + "px"; + } + line.style.left = x - props.majorLineWidth / 2 + "px"; + line.style.height = props.majorLineHeight + "px"; + line.className = "grid vertical major " + className; + return line; + }; - /** - * Reconnect all edges - * @private - */ - value: function reconnectEdges() { - var id; - var nodes = this.body.nodes; - var edges = this.body.edges; + /** + * 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. - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } + // 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"; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); - } - } - }, - writable: true, - configurable: true - } - }); + this.dom.measureCharMinor.appendChild(document.createTextNode("0")); + this.dom.foreground.appendChild(this.dom.measureCharMinor); + } + this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; + this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; - return EdgesHandler; - })(); + // 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"; - module.exports = EdgesHandler; + this.dom.measureCharMajor.appendChild(document.createTextNode("0")); + this.dom.foreground.appendChild(this.dom.measureCharMajor); + } + this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; + this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; + }; + + module.exports = TimeAxis; /***/ }, -/* 53 */ +/* 43 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var util = __webpack_require__(1); + var Component = __webpack_require__(25); + var moment = __webpack_require__(2); + var locales = __webpack_require__(41); /** - * Created by Alex on 2/23/2015. + * 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 BarnesHutSolver = _interopRequire(__webpack_require__(68)); - - var Repulsion = _interopRequire(__webpack_require__(69)); - - var HierarchicalRepulsion = _interopRequire(__webpack_require__(70)); - - var SpringSolver = _interopRequire(__webpack_require__(71)); - - var HierarchicalSpringSolver = _interopRequire(__webpack_require__(72)); + // default options + this.defaultOptions = { + showCurrentTime: true, - var CentralGravitySolver = _interopRequire(__webpack_require__(73)); + locales: locales, + locale: "en" + }; + this.options = util.extend({}, this.defaultOptions); + this.offset = 0; - var util = __webpack_require__(1); + this._create(); + this.setOptions(options); + } - var PhysicsEngine = (function () { - function PhysicsEngine(body) { - var _this = this; - _classCallCheck(this, PhysicsEngine); + CurrentTime.prototype = new Component(); - this.body = body; - this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; + /** + * 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.physicsEnabled = true; - this.simulationInterval = 1000 / 60; - this.requiresTimeout = true; - this.previousStates = {}; - this.freezeCache = {}; - this.renderTimer == undefined; + this.bar = bar; + }; - this.stabilized = false; - this.stabilizationIterations = 0; - this.ready = false; // will be set to true if the stabilize + /** + * Destroy the CurrentTime bar + */ + CurrentTime.prototype.destroy = function () { + this.options.showCurrentTime = false; + this.redraw(); // will remove the bar from the DOM and stop refreshing - // default options - this.options = {}; - this.defaultOptions = { - barnesHut: { - thetaInverted: 1 / 0.5, // inverted to save time during calculation - gravitationalConstant: -2000, - centralGravity: 0.3, - springLength: 95, - springConstant: 0.04, - damping: 0.09 - }, - repulsion: { - centralGravity: 0, - springLength: 200, - springConstant: 0.05, - nodeDistance: 100, - damping: 0.09 - }, - hierarchicalRepulsion: { - centralGravity: 0, - springLength: 100, - springConstant: 0.01, - nodeDistance: 120, - damping: 0.09 - }, - solver: "BarnesHut", - timestep: 0.5, - maxVelocity: 50, - minVelocity: 0.1, // px/s - stabilization: { - enabled: true, - iterations: 1000, // maximum number of iteration to stabilize - updateInterval: 100, - onlyDynamicEdges: false, - zoomExtent: true - } - }; - util.extend(this.options, this.defaultOptions); + this.body = null; + }; - this.body.emitter.on("initPhysics", function () { - _this.initPhysics(); - }); - this.body.emitter.on("resetPhysics", function () { - _this.stopSimulation();_this.ready = false; - }); - this.body.emitter.on("disablePhysics", function () { - _this.physicsEnabled = false;_this.stopSimulation(); - }); - this.body.emitter.on("restorePhysics", function () { - _this.setOptions(_this.options); - if (_this.ready === true) { - _this.stabilized = false; - _this.runSimulation(); - } - }); - this.body.emitter.on("startSimulation", function () { - if (_this.ready === true) { - _this.stabilized = false; - _this.runSimulation(); + /** + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] + */ + CurrentTime.prototype.setOptions = function (options) { + if (options) { + // copy all options that we know + util.selectiveExtend(["showCurrentTime", "locale", "locales"], this.options, options); + } + }; + + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + CurrentTime.prototype.redraw = function () { + if (this.options.showCurrentTime) { + var parent = this.body.dom.backgroundVertical; + if (this.bar.parentNode != parent) { + // attach to the dom + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); } - }); - this.body.emitter.on("stopSimulation", function () { - _this.stopSimulation(); - }); + parent.appendChild(this.bar); + + this.start(); + } + + var now = new Date(new Date().valueOf() + this.offset); + var x = this.body.util.toScreen(now); + + var locale = this.options.locales[this.options.locale]; + var title = locale.current + " " + locale.time + ": " + moment(now).format("dddd, MMMM Do YYYY, H:mm:ss"); + title = title.charAt(0).toUpperCase() + title.substring(1); + + this.bar.style.left = x + "px"; + this.bar.title = title; + } else { + // remove the line from the DOM + if (this.bar.parentNode) { + this.bar.parentNode.removeChild(this.bar); + } + this.stop(); } - _prototypeProperties(PhysicsEngine, null, { - setOptions: { - value: function setOptions(options) { - if (options === false) { - this.physicsEnabled = false; - this.stopSimulation(); - } else { - this.physicsEnabled = true; - if (options !== undefined) { - util.selectiveNotDeepExtend(["stabilization"], this.options, options); - util.mergeOptions(this.options, options, "stabilization"); - } - this.init(); - } - }, - writable: true, - configurable: true - }, - init: { - value: function init() { - var options; - if (this.options.solver == "repulsion") { - options = this.options.repulsion; - this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - } else if (this.options.solver == "hierarchicalRepulsion") { - options = this.options.hierarchicalRepulsion; - this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); - this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); - } else { - // barnesHut - options = this.options.barnesHut; - this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); - this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); - } + return false; + }; - this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); - this.modelOptions = options; - }, - writable: true, - configurable: true - }, - initPhysics: { - value: function initPhysics() { - if (this.physicsEnabled === true) { - this.stabilized = false; - if (this.options.stabilization.enabled === true) { - this.stabilize(); - } else { - this.ready = true; - this.body.emitter.emit("zoomExtent", { duration: 0 }, true); - this.runSimulation(); - } - } else { - this.ready = true; - this.body.emitter.emit("_redraw"); - } - }, - writable: true, - configurable: true - }, - stopSimulation: { - value: function stopSimulation() { - this.stabilized = true; - if (this.viewFunction !== undefined) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.viewFunction = undefined; - this.body.emitter.emit("_stopRendering"); - } - }, - writable: true, - configurable: true - }, - runSimulation: { - value: function runSimulation() { - if (this.physicsEnabled === true) { - if (this.viewFunction === undefined) { - this.viewFunction = this.simulationStep.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); - } - } else { - this.body.emitter.emit("_redraw"); - } - }, - writable: true, - configurable: true - }, - simulationStep: { - value: function simulationStep() { - // check if the physics have settled - var startTime = Date.now(); - this.physicsTick(); - var physicsTime = Date.now() - startTime; + /** + * Start auto refreshing the current time bar + */ + CurrentTime.prototype.start = function () { + var me = this; - // run double speed if it is a little graph - if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { - this.physicsTick(); + function update() { + me.stop(); - // this makes sure there is no jitter. The decision is taken once to run it at double speed. - this.runDoubleSpeed = true; - } + // 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; - 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: { + me.redraw(); - /** - * A single simulation step (or "tick") in the physics simulation - * - * @private - */ - value: function physicsTick() { - if (this.stabilized === false) { - this.calculateForces(); - this.stabilized = this.moveNodes(); + // start a renderTimer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); + } - // 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; - } - } + update(); + }; - this.stabilizationIterations++; - } - }, - writable: true, - configurable: true - }, - updatePhysicsIndices: { + /** + * Stop auto refreshing the current time bar + */ + CurrentTime.prototype.stop = function () { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } + }; - /** - * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also - * handled in the calculateForces function. We then use a quadratic curve with the center node as control. - * This function joins the datanodes and invisible (called support) nodes into one object. - * We do this so we do not contaminate this.body.nodes with the support nodes. - * - * @private - */ - value: function updatePhysicsIndices() { - this.physicsBody.forces = {}; - this.physicsBody.physicsNodeIndices = []; - this.physicsBody.physicsEdgeIndices = []; - var nodes = this.body.nodes; - var edges = this.body.edges; + /** + * Set a current time. This can be used for example to ensure that a client's + * time is synchronized with a shared server time. + * @param {Date | String | Number} time A Date, unix timestamp, or + * ISO date string. + */ + CurrentTime.prototype.setCurrentTime = function (time) { + var t = util.convert(time, "Date").valueOf(); + var now = new Date().valueOf(); + this.offset = t - now; + this.redraw(); + }; - // get node indices for physics - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - if (nodes[nodeId].options.physics === true) { - this.physicsBody.physicsNodeIndices.push(nodeId); - } - } - } + /** + * Get the current time. + * @return {Date} Returns the current time. + */ + CurrentTime.prototype.getCurrentTime = function () { + return new Date(new Date().valueOf() + this.offset); + }; - // get edge indices for physics - for (var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - if (edges[edgeId].options.physics === true) { - this.physicsBody.physicsEdgeIndices.push(edgeId); - } - } - } + module.exports = CurrentTime; - // get the velocity and the forces vector - for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { - var nodeId = this.physicsBody.physicsNodeIndices[i]; - this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; +/***/ }, +/* 44 */ +/***/ 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 (nodes[nodeId] === undefined) { - delete this.physicsBody.velocities[nodeId]; - } - } - }, - writable: true, - configurable: true - }, - revert: { - value: function revert() { - var nodeIds = Object.keys(this.previousStates); - var nodes = this.body.nodes; - var velocities = this.physicsBody.velocities; + 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); - for (var i = 0; i < nodeIds.length; i++) { - var nodeId = nodeIds[i]; - if (nodes[nodeId] !== undefined) { - velocities[nodeId].x = this.previousStates[nodeId].vx; - velocities[nodeId].y = this.previousStates[nodeId].vy; - nodes[nodeId].x = this.previousStates[nodeId].x; - nodes[nodeId].y = this.previousStates[nodeId].y; - } else { - delete this.previousStates[nodeId]; - } - } - }, - writable: true, - configurable: true - }, - moveNodes: { - value: function moveNodes() { - var nodesPresent = false; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; - var stabilized = true; - var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); + /** + * 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; + } - 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; - } + var me = this; + this.defaultOptions = { + start: null, + end: null, + autoResize: true, - if (nodesPresent == true) { - if (vminCorrected > 0.5 * this.options.maxVelocity) { - return false; - } else { - return stabilized; - } - } - return true; - }, - writable: true, - configurable: 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) }, - _performStep: { - value: function _performStep(nodeId, maxVelocity) { - var node = this.body.nodes[nodeId]; - var timestep = this.options.timestep; - var forces = this.physicsBody.forces; - var velocities = this.physicsBody.velocities; + 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) + } + }; - // store the state so we can revert - this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - if (node.options.fixed.x === false) { - var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force - var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration - velocities[nodeId].x += ax * timestep; // velocity - velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; - node.x += velocities[nodeId].x * timestep; // position - } else { - forces[nodeId].x = 0; - velocities[nodeId].x = 0; - } + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + //this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - if (node.options.fixed.y === false) { - var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force - var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration - velocities[nodeId].y += ay * timestep; // velocity - velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; - node.y += velocities[nodeId].y * timestep; // position - } else { - forces[nodeId].y = 0; - velocities[nodeId].y = 0; - } + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - 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: { + // custom time bar + // Note: time bar will be attached in this.setOptions when selected + this.customTime = new CustomTime(this.body); + this.components.push(this.customTime); + + // item set + this.linegraph = new LineGraph(this.body); + this.components.push(this.linegraph); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // apply options + if (options) { + this.setOptions(options); + } + + // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS! + if (groups) { + this.setGroups(groups); + } + + // create itemset + if (items) { + this.setItems(items); + } else { + this._redraw(); + } + } + // Extend the functionality from Core + Graph2d.prototype = new Core(); + /** + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + */ + Graph2d.prototype.setItems = function (items) { + var initialLoad = this.itemsData == null; + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; + } else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; + } else { + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: "Date", + end: "Date" + } + }); + } + // set items + this.itemsData = newDataSet; + this.linegraph && this.linegraph.setItems(newDataSet); + if (initialLoad) { + if (this.options.start != undefined || this.options.end != undefined) { + var start = this.options.start != undefined ? this.options.start : null; + var end = this.options.end != undefined ? this.options.end : null; + this.setWindow(start, end, { animate: false }); + } else { + this.fit({ animate: false }); + } + } + }; + /** + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups + */ + Graph2d.prototype.setGroups = function (groups) { + // convert to type DataSet when needed + var newDataSet; + if (!groups) { + newDataSet = null; + } else if (groups instanceof DataSet || groups instanceof DataView) { + newDataSet = groups; + } else { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } + this.groupsData = newDataSet; + this.linegraph.setGroups(newDataSet); + }; - /** - * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization - * because only the supportnodes for the smoothCurves have to settle. - * - * @private - */ - value: function _freezeNodes() { - var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].x && nodes[id].y) { - this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; - nodes[id].options.fixed.x = true; - nodes[id].options.fixed.y = true; - } - } - } - }, - writable: true, - configurable: true - }, - _restoreFrozenNodes: { + /** + * Returns an object containing an SVG element with the icon of the group (size determined by iconWidth and iconHeight), the label of the group (content) and the yAxisOrientation of the group (left or right). + * @param groupId + * @param width + * @param height + */ + Graph2d.prototype.getLegend = function (groupId, width, height) { + if (width === undefined) { + width = 15; + } + if (height === undefined) { + height = 15; + } + if (this.linegraph.groups[groupId] !== undefined) { + return this.linegraph.groups[groupId].getLegend(width, height); + } else { + return "cannot find group:" + groupId; + } + }; - /** - * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. - * - * @private - */ - value: function _restoreFrozenNodes() { - var nodes = this.body.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (this.freezeCache[id] !== undefined) { - nodes[id].options.fixed.x = this.freezeCache[id].x; - nodes[id].options.fixed.y = this.freezeCache[id].y; - } - } - } - this.freezeCache = {}; - }, - writable: true, - configurable: true - }, - stabilize: { + /** + * This checks if the visible option of the supplied group (by ID) is true or false. + * @param groupId + * @returns {*} + */ + Graph2d.prototype.isGroupVisible = function (groupId) { + if (this.linegraph.groups[groupId] !== undefined) { + return this.linegraph.groups[groupId].visible && (this.linegraph.options.groups.visibility[groupId] === undefined || this.linegraph.options.groups.visibility[groupId] == true); + } else { + return false; + } + }; - /** - * Find a stable position for all nodes - * @private - */ - value: function stabilize() { - if (this.options.stabilization.onlyDynamicEdges == true) { - this._freezeNodes(); - } - this.stabilizationSteps = 0; - setTimeout(this._stabilizationBatch.bind(this), 0); - }, - writable: true, - configurable: true - }, - _stabilizationBatch: { - value: function _stabilizationBatch() { - var count = 0; - while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { - this.physicsTick(); - this.stabilizationSteps++; - count++; - } + /** + * Get the data range of the item set. + * @returns {{min: Date, max: Date}} range A range with a start and end Date. + * When no minimum is found, min==null + * When no maximum is found, max==null + */ + Graph2d.prototype.getItemRange = function () { + var min = null; + var max = null; - 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 }); + // 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; } + } + } + } - if (this.options.stabilization.onlyDynamicEdges == true) { - this._restoreFrozenNodes(); - } + return { + min: min != null ? new Date(min) : null, + max: max != null ? new Date(max) : null + }; + }; - this.body.emitter.emit("stabilizationIterationsDone"); - this.body.emitter.emit("_requestRedraw"); - this.ready = true; - }, - writable: true, - configurable: true - } - }); - return PhysicsEngine; - })(); - module.exports = PhysicsEngine; + module.exports = Graph2d; /***/ }, -/* 54 */ +/* 45 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(6); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); + var Component = __webpack_require__(25); + var DataAxis = __webpack_require__(46); + var GraphGroup = __webpack_require__(48); + var Legend = __webpack_require__(52); + var BarGraphFunctions = __webpack_require__(51); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var UNGROUPED = "__ungrouped__"; // reserved group id for ungrouped items /** - * Created by Alex on 24-Feb-15. + * This is the constructor of the LineGraph. It requires a Timeline body and options. + * + * @param body + * @param options + * @constructor */ + function LineGraph(body, options) { + this.id = util.randomUUID(); + this.body = body; - var util = __webpack_require__(1); - var Cluster = _interopRequire(__webpack_require__(74)); + 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: {} + } + }; - var ClusterEngine = (function () { - function ClusterEngine(body) { - _classCallCheck(this, ClusterEngine); + // 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; - this.body = body; - this.clusteredNodes = {}; - } + var me = this; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - _prototypeProperties(ClusterEngine, null, { - setOptions: { - value: function setOptions(options) {}, - writable: true, - configurable: true + // listeners for the DataSet of the items + this.itemListeners = { + add: function (event, params, senderId) { + me._onAdd(params.items); }, - clusterByConnectionCount: { + update: function (event, params, senderId) { + me._onUpdate(params.items); + }, + remove: function (event, params, senderId) { + me._onRemove(params.items); + } + }; - /** - * - * @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(); - } + // 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); + } + }; - 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); - } - } + 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 - for (var i = 0; i < nodesToCluster.length; i++) { - var node = this.body.nodes[nodesToCluster[i]]; - this.clusterByConnection(node, options, {}, {}, false); - } - this.body.emitter.emit("_dataChanged"); - }, - writable: true, - configurable: true - }, - clusterByNodeData: { + this.svgElements = {}; + this.setOptions(options); + this.groupsUsingDefaultStyles = [0]; + this.COUNTER = 0; + this.body.emitter.on("rangechanged", function () { + me.lastStart = me.body.range.start; + me.svg.style.left = util.option.asSize(-me.props.width); + me.redraw.call(me, true); + }); + // create the HTML DOM + this._create(); + this.framework = { svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups }; + this.body.emitter.emit("change"); + } - /** - * loop over all nodes, check if they adhere to the condition and cluster if needed. - * @param options - * @param refreshData - */ - value: function clusterByNodeData() { - var options = arguments[0] === undefined ? {} : arguments[0]; - var refreshData = arguments[1] === undefined ? true : arguments[1]; - if (options.joinCondition === undefined) { - throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); - } + LineGraph.prototype = new Component(); - // check if the options object is fine, append if needed - options = this._checkOptions(options); + /** + * Create the HTML DOM for the ItemSet + */ + LineGraph.prototype._create = function () { + var frame = document.createElement("div"); + frame.className = "LineGraph"; + this.dom.frame = frame; - var childNodesObj = {}; - var childEdgesObj = {}; + // 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); - // 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]; - } - } + // data axis + this.options.dataAxis.orientation = "left"; + this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups); - this._cluster(childNodesObj, childEdgesObj, options, refreshData); - }, - writable: true, - configurable: true - }, - clusterOutliers: { + 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(); + }; - /** - * Cluster all nodes in the network that have only 1 edge - * @param options - * @param refreshData - */ - value: function clusterOutliers(options) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; - options = this._checkOptions(options); - var clusters = []; + /** + * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element. + * @param {object} options + */ + LineGraph.prototype.setOptions = function (options) { + if (options) { + var fields = ["sampling", "defaultGroup", "height", "graphHeight", "yAxisOrientation", "style", "barChart", "dataAxis", "sort", "groups"]; + if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) { + this.updateSVGheight = true; + this.updateSVGheightOnResize = true; + } else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) { + if (parseInt((options.graphHeight + "").replace("px", "")) < this.body.domProps.centerContainer.height) { + this.updateSVGheight = true; + } + } + util.selectiveDeepExtend(fields, this.options, options); + util.mergeOptions(this.options, options, "catmullRom"); + util.mergeOptions(this.options, options, "drawPoints"); + util.mergeOptions(this.options, options, "shaded"); + util.mergeOptions(this.options, options, "legend"); - // 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 }); - } + 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; } } + } + } - for (var i = 0; i < clusters.length; i++) { - this._cluster(clusters[i].nodes, clusters[i].edges, options, false); - } + if (this.yAxisLeft) { + if (options.dataAxis !== undefined) { + this.yAxisLeft.setOptions(this.options.dataAxis); + this.yAxisRight.setOptions(this.options.dataAxis); + } + } - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - clusterByConnection: { + if (this.legendLeft) { + if (options.legend !== undefined) { + this.legendLeft.setOptions(this.options.legend); + this.legendRight.setOptions(this.options.legend); + } + } - /** - * - * @param nodeId - * @param options - * @param refreshData - */ - value: function clusterByConnection(nodeId, options) { - var refreshData = arguments[2] === undefined ? true : arguments[2]; - // kill conditions - if (nodeId === undefined) { - throw new Error("No nodeId supplied to clusterByConnection!"); - } - if (this.body.nodes[nodeId] === undefined) { - throw new Error("The nodeId given to clusterByConnection does not exist!"); - } + if (this.groups.hasOwnProperty(UNGROUPED)) { + this.groups[UNGROUPED].setOptions(options); + } + } + + // this is used to redraw the graph if the visibility of the groups is changed. + if (this.dom.frame) { + this.redraw(true); + } + }; + + /** + * Hide the component from the DOM + */ + LineGraph.prototype.hide = function () { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } + }; - var node = this.body.nodes[nodeId]; - options = this._checkOptions(options, node); - if (options.clusterNodeProperties.x === undefined) { - options.clusterNodeProperties.x = node.x; - } - if (options.clusterNodeProperties.y === undefined) { - options.clusterNodeProperties.y = node.y; - } - if (options.clusterNodeProperties.fixed === undefined) { - options.clusterNodeProperties.fixed = {}; - options.clusterNodeProperties.fixed.x = node.options.fixed.x; - options.clusterNodeProperties.fixed.y = node.options.fixed.y; - } + /** + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed + */ + LineGraph.prototype.show = function () { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } + }; - var 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); + /** + * Set items + * @param {vis.DataSet | null} items + */ + LineGraph.prototype.setItems = function (items) { + var me = this, + ids, + oldItemsData = this.itemsData; - 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; - } - } + // replace the dataset + if (!items) { + this.itemsData = null; + } else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } else { + throw new TypeError("Data must be an instance of DataSet or DataView"); + } - this._cluster(childNodesObj, childEdgesObj, options, refreshData); - }, - writable: true, - configurable: true - }, - _cloneOptions: { + 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); + } - /** - * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. - * @param objId - * @param type - * @returns {{}} - * @private - */ - value: function _cloneOptions(objId, type) { - var clonedOptions = {}; - if (type === undefined || type == "node") { - util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); - util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); - clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; - } else { - util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); - } - return clonedOptions; - }, - writable: true, - configurable: true - }, - _createClusterEdges: { + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); + } + this._updateUngrouped(); + //this._updateGraph(); + this.redraw(true); + }; - /** - * This function creates the edges that will be attached to the cluster. - * - * @param childNodesObj - * @param childEdgesObj - * @param newEdges - * @param options - * @private - */ - value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { - var edge, childNodeId, childNode; - var childKeys = Object.keys(childNodesObj); - for (var i = 0; i < childKeys.length; i++) { - childNodeId = childKeys[i]; - childNode = childNodesObj[childNodeId]; + /** + * Set groups + * @param {vis.DataSet} groups + */ + LineGraph.prototype.setGroups = function (groups) { + var me = this; + var ids; - // 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; + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - 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; - } + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - 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: { + // 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); + }); - /** - * 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 = {}; - } + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } + this._onUpdate(); + }; - return options; - }, - writable: true, - configurable: true - }, - _cluster: { - /** - * - * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node - * @param {Object} childEdgesObj | object with edge objects, id as keys - * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} - * @param {Boolean} refreshData | when true, do not wrap up - * @private - */ - value: function _cluster(childNodesObj, childEdgesObj, options) { - var refreshData = arguments[3] === undefined ? true : arguments[3]; - // kill condition: no children so cant cluster - if (Object.keys(childNodesObj).length == 0) { - return; - } + /** + * Update the data + * @param [ids] + * @private + */ + LineGraph.prototype._onUpdate = function (ids) { + this._updateUngrouped(); + this._updateAllGroupData(); + //this._updateGraph(); + this.redraw(true); + }; + LineGraph.prototype._onAdd = function (ids) { + this._onUpdate(ids); + }; + LineGraph.prototype._onRemove = function (ids) { + this._onUpdate(ids); + }; + LineGraph.prototype._onUpdateGroups = function (groupIds) { + for (var i = 0; i < groupIds.length; i++) { + var group = this.groupsData.get(groupIds[i]); + this._updateGroup(group, groupIds[i]); + } - // check if we have an unique id; - if (options.clusterNodeProperties.id === undefined) { - options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); - } - var clusterId = options.clusterNodeProperties.id; + //this._updateGraph(); + this.redraw(true); + }; + LineGraph.prototype._onAddGroups = function (groupIds) { + this._onUpdateGroups(groupIds); + }; - // 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); - } + /** + * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph + * @param {Array} groupIds + * @private + */ + LineGraph.prototype._onRemoveGroups = function (groupIds) { + for (var i = 0; i < groupIds.length; i++) { + if (this.groups.hasOwnProperty(groupIds[i])) { + if (this.groups[groupIds[i]].options.yAxisOrientation == "right") { + this.yAxisRight.removeGroup(groupIds[i]); + this.legendRight.removeGroup(groupIds[i]); + this.legendRight.redraw(); + } else { + this.yAxisLeft.removeGroup(groupIds[i]); + this.legendLeft.removeGroup(groupIds[i]); + this.legendLeft.redraw(); + } + delete this.groups[groupIds[i]]; + } + } + this._updateUngrouped(); + //this._updateGraph(); + this.redraw(true); + }; - // 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"; - } + /** + * update a group object with the group dataset entree + * + * @param group + * @param groupId + * @private + */ + LineGraph.prototype._updateGroup = function (group, groupId) { + if (!this.groups.hasOwnProperty(groupId)) { + this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles); + if (this.groups[groupId].options.yAxisOrientation == "right") { + this.yAxisRight.addGroup(groupId, this.groups[groupId]); + this.legendRight.addGroup(groupId, this.groups[groupId]); + } else { + this.yAxisLeft.addGroup(groupId, this.groups[groupId]); + this.legendLeft.addGroup(groupId, this.groups[groupId]); + } + } else { + this.groups[groupId].update(group); + if (this.groups[groupId].options.yAxisOrientation == "right") { + this.yAxisRight.updateGroup(groupId, this.groups[groupId]); + this.legendRight.updateGroup(groupId, this.groups[groupId]); + } else { + this.yAxisLeft.updateGroup(groupId, this.groups[groupId]); + this.legendLeft.updateGroup(groupId, this.groups[groupId]); + } + } + this.legendLeft.redraw(); + this.legendRight.redraw(); + }; - // 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; + /** + * this updates all groups, it is used when there is an update the the itemset. + * + * @private + */ + LineGraph.prototype._updateAllGroupData = function () { + if (this.itemsData != null) { + var groupsContent = {}; + var groupId; + for (groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + groupsContent[groupId] = []; + } + } + 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."); } - if (clusterNodeProperties.x === undefined) { - if (pos === undefined) { - pos = this._getClusterPosition(childNodesObj); + 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]); + } + } + } + }; + + + /** + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. This anonymous group is called 'graph'. + * @protected + */ + LineGraph.prototype._updateUngrouped = function () { + if (this.itemsData && this.itemsData != null) { + var ungroupedCounter = 0; + for (var itemId in this.itemsData._data) { + if (this.itemsData._data.hasOwnProperty(itemId)) { + var item = this.itemsData._data[itemId]; + if (item != undefined) { + if (item.hasOwnProperty("group")) { + if (item.group === undefined) { + item.group = UNGROUPED; + } + } else { + item.group = UNGROUPED; } - clusterNodeProperties.y = pos.y; - clusterNodeProperties.allowedToMoveY = true; + 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(); + }; - // force the ID to remain the same - clusterNodeProperties.id = clusterId; + /** + * Redraw the component, mandatory function + * @return {boolean} Returns true if the component is resized + */ + LineGraph.prototype.redraw = function (forceGraphUpdate) { + var resized = false; - // create the clusterNode - var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); - clusterNode.isCluster = true; - clusterNode.containedNodes = childNodesObj; - clusterNode.containedEdges = childEdgesObj; + // 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; + } - // disable the childEdges - for (var edgeId in childEdgesObj) { - if (childEdgesObj.hasOwnProperty(edgeId)) { - if (this.body.edges[edgeId] !== undefined) { - var edge = this.body.edges[edgeId]; - edge.togglePhysics(false); - edge.options.hidden = true; - } - } - } + // check if this component is resized + resized = this._isResized() || resized; + // check whether zoomed (in that case we need to re-stack everything) + var visibleInterval = this.body.range.end - this.body.range.start; + var zoomed = visibleInterval != this.lastVisibleInterval; + this.lastVisibleInterval = visibleInterval; - // disable the childNodes - for (var nodeId in childNodesObj) { - if (childNodesObj.hasOwnProperty(nodeId)) { - this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; - this.body.nodes[nodeId].togglePhysics(false); - this.body.nodes[nodeId].options.hidden = true; - } - } + // the svg element is three times as big as the width, this allows for fully dragging left and right + // without reloading the graph. the controls for this are bound to events in the constructor + if (resized == true) { + this.svg.style.width = util.option.asSize(3 * this.props.width); + this.svg.style.left = util.option.asSize(-this.props.width); + + // if the height of the graph is set as proportional, change the height of the svg + if ((this.options.height + "").indexOf("%") != -1 || this.updateSVGheightOnResize == true) { + this.updateSVGheight = true; + } + } + + // 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"; + } + + // zoomed is here to ensure that animations are shown correctly. + if (resized == true || zoomed == true || this.abortedGraphUpdate == true || forceGraphUpdate == true) { + resized = this._updateGraph() || resized; + } else { + // move the whole svg while dragging + if (this.lastStart != 0) { + var offset = this.body.range.start - this.lastStart; + var range = this.body.range.end - this.body.range.start; + if (this.props.width != 0) { + var rangePerPixelInv = this.props.width / range; + var xOffset = offset * rangePerPixelInv; + this.svg.style.left = -this.props.width - xOffset + "px"; + } + } + } + + this.legendLeft.redraw(); + this.legendRight.redraw(); + return resized; + }; - // finally put the cluster node into global - this.body.nodes[clusterNodeProperties.id] = clusterNode; + /** + * 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; - // 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(); + // 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); - // set ID to undefined so no duplicates arise - clusterNodeProperties.id = undefined; + // apply sampling, if disabled, it will pass through this function. + this._applySampling(groupIds, groupsData); + // we transform the X coordinates to detect collisions + for (i = 0; i < groupIds.length; i++) { + preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]); + } - // wrap up - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - isCluster: { + // 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; - /** - * Check if a node is a cluster. - * @param nodeId - * @returns {*} - */ - value: function isCluster(nodeId) { - if (this.body.nodes[nodeId] !== undefined) { - return this.body.nodes[nodeId].isCluster === true; - } else { - console.log("Node does not exist."); - return false; + // 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); } - }, - writable: true, - configurable: true - }, - _getClusterPosition: { - /** - * 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; + // 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); + } } - return { x: 0.5 * (minX + maxX), y: 0.5 * (minY + maxY) }; - }, - writable: true, - configurable: true - }, - openCluster: { + BarGraphFunctions.draw(groupIds, processedGroupData, this.framework); + } + } + } + // cleanup unused svg elements + DOMutil.cleanupElements(this.svgElements); + return false; + }; - /** - * Open a cluster by calling this function. - * @param {String} clusterNodeId | the ID of the cluster node - * @param {Boolean} refreshData | wrap up afterwards if not true - */ - value: function openCluster(clusterNodeId) { - var refreshData = arguments[1] === undefined ? true : arguments[1]; - // kill conditions - if (clusterNodeId === undefined) { - throw new Error("No clusterNodeId supplied to openCluster."); + + /** + * first select and preprocess the data from the datasets. + * the groups have their preselection of data, we now loop over this data to see + * what data we need to draw. Sorted data is much faster. + * more optimization is possible by doing the sampling before and using the binary search + * to find the end date to determine the increment. + * + * @param {array} groupIds + * @param {object} groupsData + * @param {date} minDate + * @param {date} maxDate + * @private + */ + LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) { + var group, i, j, item; + if (groupIds.length > 0) { + for (i = 0; i < groupIds.length; i++) { + group = this.groups[groupIds[i]]; + groupsData[groupIds[i]] = []; + var dataContainer = groupsData[groupIds[i]]; + // optimization for sorted data + if (group.options.sort == true) { + var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, "x", "before")); + for (j = guess; j < group.itemsData.length; j++) { + item = group.itemsData[j]; + if (item !== undefined) { + if (item.x > maxDate) { + dataContainer.push(item); + break; + } else { + dataContainer.push(item); + } + } } - if (this.body.nodes[clusterNodeId] === undefined) { - throw new Error("The clusterNodeId supplied to openCluster does not exist."); + } 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); + } + } } - if (this.body.nodes[clusterNodeId].containedNodes === undefined) { - console.log("The node:" + clusterNodeId + " is not a cluster.");return; - }; - - var clusterNode = this.body.nodes[clusterNodeId]; - var containedNodes = clusterNode.containedNodes; - var containedEdges = clusterNode.containedEdges; + } + } + } + }; - // release nodes - for (var nodeId in containedNodes) { - if (containedNodes.hasOwnProperty(nodeId)) { - var containedNode = this.body.nodes[nodeId]; - containedNode = containedNodes[nodeId]; - // inherit position - containedNode.x = clusterNode.x; - containedNode.y = clusterNode.y; - // inherit speed - containedNode.vx = clusterNode.vx; - containedNode.vy = clusterNode.vy; + /** + * + * @param groupIds + * @param groupsData + * @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; - containedNode.options.hidden = false; - containedNode.togglePhysics(true); + // 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))); - delete this.clusteredNodes[nodeId]; + var sampledData = []; + for (var j = 0; j < amountOfPoints; j += increment) { + sampledData.push(dataContainer[j]); } + groupsData[groupIds[i]] = sampledData; } + } + } + } + }; - // release edges - for (var edgeId in containedEdges) { - if (containedEdges.hasOwnProperty(edgeId)) { - var edge = this.body.edges[edgeId]; - edge.options.hidden = false; - edge.togglePhysics(true); - } - } - // remove all temporary edges - for (var i = 0; i < clusterNode.edges.length; i++) { - var edgeId = clusterNode.edges[i].id; - var viaId = this.body.edges[edgeId].via.id; - if (viaId) { - this.body.edges[edgeId].via = undefined; - delete this.body.nodes[viaId]; + /** + * + * + * @param {array} groupIds + * @param {object} groupsData + * @param {object} groupRanges | this is being filled here + * @private + */ + LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) { + var groupData, group, i; + var barCombinedDataLeft = []; + var barCombinedDataRight = []; + var options; + if (groupIds.length > 0) { + for (i = 0; i < groupIds.length; i++) { + groupData = groupsData[groupIds[i]]; + options = this.groups[groupIds[i]].options; + if (groupData.length > 0) { + group = this.groups[groupIds[i]]; + // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups. + if (options.barChart.handleOverlap == "stack" && options.style == "bar") { + if (options.yAxisOrientation == "left") { + barCombinedDataLeft = barCombinedDataLeft.concat(group.getYRange(groupData)); + } else { + barCombinedDataRight = barCombinedDataRight.concat(group.getYRange(groupData)); } - // this removes the edge from node.edges, which is why edgeIds is formed - this.body.edges[edgeId].disconnect(); - delete this.body.edges[edgeId]; + } else { + groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]); } + } + } - // remove clusterNode - delete this.body.nodes[clusterNodeId]; + // 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"); + } + }; - if (refreshData === true) { - this.body.emitter.emit("_dataChanged"); - } - }, - writable: true, - configurable: true - }, - _connectEdge: { + /** + * 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; + } + } + // 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; - /** - * 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; + 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; + } } - edge.connect(); - }, - writable: true, - configurable: true - }, - _getClusterStack: { + } + } - /** - * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node - * @param nodeId - * @returns {Array} - * @private - */ - value: function _getClusterStack(nodeId) { - var stack = []; - var max = 100; - var counter = 0; + if (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; - 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: { + 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; + } - /** - * 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: { + // 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); + } - /** - * 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; + return resized; + }; + + + /** + * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function + * + * @param {boolean} axisUsed + * @returns {boolean} + * @private + * @param axis + */ + LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { + var changed = false; + if (axisUsed == false) { + if (axis.dom.frame.parentNode && axis.hidden == false) { + axis.hide(); + changed = true; + } + } else { + if (!axis.dom.frame.parentNode && axis.hidden == true) { + axis.show(); + changed = true; + } + } + return changed; + }; + + + /** + * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the + * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for + * the yAxis. + * + * @param datapoints + * @returns {Array} + * @private + */ + LineGraph.prototype._convertXcoordinates = function (datapoints) { + var extractedData = []; + var xValue, yValue; + var toScreen = this.body.util.toScreen; - 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; + for (var i = 0; i < datapoints.length; i++) { + xValue = toScreen(datapoints[i].x) + this.props.width; + yValue = datapoints[i].y; + extractedData.push({ x: xValue, y: yValue }); + } - var variance = averageSquared - Math.pow(average, 2); - var standardDeviation = Math.sqrt(variance); + return extractedData; + }; - var hubThreshold = Math.floor(average + 2 * standardDeviation); - // always have at least one to cluster - if (hubThreshold > largestHub) { - hubThreshold = largestHub; - } + /** + * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the + * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for + * the yAxis. + * + * @param datapoints + * @param group + * @returns {Array} + * @private + */ + LineGraph.prototype._convertYcoordinates = function (datapoints, group) { + var extractedData = []; + var xValue, yValue; + var toScreen = this.body.util.toScreen; + var axis = this.yAxisLeft; + var svgHeight = Number(this.svg.style.height.replace("px", "")); + if (group.options.yAxisOrientation == "right") { + axis = this.yAxisRight; + } - return hubThreshold; - }, - writable: true, - configurable: true - } - }); + 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 ClusterEngine; - })(); + group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0))); + + return extractedData; + }; - module.exports = ClusterEngine; + + module.exports = LineGraph; /***/ }, -/* 55 */ +/* 46 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(6); + var Component = __webpack_require__(25); + var DataStep = __webpack_require__(47); /** - * Created by Alex on 26-Feb-15. + * A horizontal time axis + * @param {Object} [options] See DataAxis.setOptions for the available + * options. + * @constructor DataAxis + * @extends Component + * @param body */ + function DataAxis(body, options, svg, linegraphOptions) { + this.id = util.randomUUID(); + this.body = body; - if (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.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.body = body; - this.canvas = canvas; + this.linegraphOptions = linegraphOptions; + this.linegraphSVG = svg; + this.props = {}; + this.DOMelements = { // dynamic elements + lines: {}, + labels: {}, + title: {} + }; - this.redrawRequested = false; - this.renderTimer = false; - this.requiresTimeout = true; - this.renderingActive = false; - this.renderRequests = 0; - this.pixelRatio = undefined; + this.dom = {}; - // redefined in this._redraw - this.canvasTopLeft = { x: 0, y: 0 }; - this.canvasBottomRight = { x: 0, y: 0 }; + this.range = { start: 0, end: 0 }; - this.dragging = false; + this.options = util.extend({}, this.defaultOptions); + this.conversionFactor = 1; - this.body.emitter.on("dragStart", function () { - _this.dragging = true; - }); - this.body.emitter.on("dragEnd", function () { - return _this.dragging = false; - }); - this.body.emitter.on("_redraw", function () { - if (_this.renderingActive === false) { - _this._redraw(); - } - }); - this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); - this.body.emitter.on("_startRendering", function () { - _this.renderRequests += 1;_this.renderingActive = true;_this.startRendering(); - }); - this.body.emitter.on("_stopRendering", function () { - _this.renderRequests -= 1;_this.renderingActive = _this.renderRequests > 0; - }); + this.setOptions(options); + this.width = Number(("" + this.options.width).replace("px", "")); + this.minWidth = this.width; + this.height = this.linegraphSVG.offsetHeight; + this.hidden = false; - this.options = {}; - this.defaultOptions = { - hideEdgesOnDrag: false, - hideNodesOnDrag: false - }; - util.extend(this.options, this.defaultOptions); + this.stepPixels = 25; + this.stepPixelsForced = 25; + this.zeroCrossing = -1; - this._determineBrowserMethod(); - } + this.lineOffset = 0; + this.master = true; + this.svgElements = {}; + this.iconsRemoved = false; - _prototypeProperties(CanvasRenderer, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); - } - }, - writable: true, - configurable: true - }, - startRendering: { - value: function startRendering() { - if (this.renderingActive === true) { - if (!this.renderTimer) { - if (this.requiresTimeout == true) { - this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function - } else { - this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function - } - } - } else {} - }, - writable: true, - configurable: true - }, - renderStep: { - value: function renderStep() { - // reset the renderTimer so a new scheduled animation step can be set - this.renderTimer = undefined; - if (this.requiresTimeout == true) { - // this schedules a new simulation step - this.startRendering(); - } + this.groups = {}; + this.amountOfGroups = 0; - this._redraw(); + // create the HTML DOM + this._create(); - if (this.requiresTimeout == false) { - // this schedules a new simulation step - this.startRendering(); - } - }, - writable: true, - configurable: true - }, - redraw: { + var me = this; + this.body.emitter.on("verticalDrag", function () { + me.dom.lineContainer.style.top = me.body.domProps.scrollTop + "px"; + }); + } - /** - * Redraw the network with the current data - * chart will be resized too. - */ - value: function redraw() { - this.setSize(this.constants.width, this.constants.height); - this._redraw(); - }, - writable: true, - configurable: true - }, - _requestRedraw: { + DataAxis.prototype = new Component(); - /** - * Redraw the network with the current data - * @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. - * @private - */ - value: function _requestRedraw() { - if (this.redrawRequested !== true && this.renderingActive === false) { - this.redrawRequested = true; - if (this.requiresTimeout === true) { - window.setTimeout(this._redraw.bind(this, false), 0); - } else { - window.requestAnimationFrame(this._redraw.bind(this, false)); - } - } - }, - writable: true, - configurable: true - }, - _redraw: { - value: function _redraw() { - var hidden = arguments[0] === undefined ? false : arguments[0]; - this.body.emitter.emit("initRedraw"); - this.redrawRequested = false; - var ctx = this.canvas.frame.canvas.getContext("2d"); + DataAxis.prototype.addGroup = function (label, graphOptions) { + if (!this.groups.hasOwnProperty(label)) { + this.groups[label] = graphOptions; + } + this.amountOfGroups += 1; + }; - if (this.pixelRation === undefined) { - this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); - } + DataAxis.prototype.updateGroup = function (label, graphOptions) { + this.groups[label] = graphOptions; + }; - ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + DataAxis.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; + } + }; - // clear the canvas - var w = this.canvas.frame.canvas.clientWidth; - var h = this.canvas.frame.canvas.clientHeight; - ctx.clearRect(0, 0, w, h); - this.body.emitter.emit("beforeDrawing", ctx); + 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); - // set scaling and translation - ctx.save(); - ctx.translate(this.body.view.translation.x, this.body.view.translation.y); - ctx.scale(this.body.view.scale, this.body.view.scale); + this.minWidth = Number(("" + this.options.width).replace("px", "")); - 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 }); + if (redraw == true && this.dom.frame) { + this.hide(); + this.show(); + } + } + }; - if (hidden === false) { - if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { - this._drawEdges(ctx); - } - } - if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { - this._drawNodes(ctx, hidden); - } + /** + * Create the HTML DOM for the DataAxis + */ + DataAxis.prototype._create = function () { + this.dom.frame = document.createElement("div"); + this.dom.frame.style.width = this.options.width; + this.dom.frame.style.height = this.height; - if (this.controlNodesActive === true) { - this._drawControlNodes(ctx); - } + this.dom.lineContainer = document.createElement("div"); + this.dom.lineContainer.style.width = "100%"; + this.dom.lineContainer.style.height = this.height; + this.dom.lineContainer.style.position = "relative"; - //this.physics.nodesSolver._debug(ctx,"#F00F0F"); + // create svg element for graph drawing. + this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.svg.style.position = "absolute"; + this.svg.style.top = "0px"; + this.svg.style.height = "100%"; + this.svg.style.width = "100%"; + this.svg.style.display = "block"; + this.dom.frame.appendChild(this.svg); + }; - this.body.emitter.emit("afterDrawing", ctx); + DataAxis.prototype._redrawGroupIcons = function () { + DOMutil.prepareElements(this.svgElements); - // restore original scaling and translation - ctx.restore(); + var x; + var iconWidth = this.options.iconWidth; + var iconHeight = 15; + var iconOffset = 4; + var y = iconOffset + 0.5 * iconHeight; - if (hidden === true) { - ctx.clearRect(0, 0, w, h); - } + if (this.options.orientation == "left") { + x = iconOffset; + } else { + x = this.width - iconWidth - iconOffset; + } - }, - writable: true, - configurable: true - }, - _drawNodes: { + 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; + }; - /** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @param {Boolean} [alwaysShow] - * @private - */ - value: function _drawNodes(ctx) { - var alwaysShow = arguments[1] === undefined ? false : arguments[1]; - var nodes = this.body.nodes; - var nodeIndices = this.body.nodeIndices; - var node; - var selected = []; + DataAxis.prototype._cleanupIcons = function () { + if (this.iconsRemoved == false) { + DOMutil.prepareElements(this.svgElements); + DOMutil.cleanupElements(this.svgElements); + this.iconsRemoved = true; + } + }; - // draw unselected nodes; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - // set selected nodes aside - if (node.isSelected()) { - selected.push(nodeIndices[i]); - } else { - if (alwaysShow === true) { - node.draw(ctx); - } - // todo: replace check - //else if (node.inArea() === true) { - node.draw(ctx); - //} - } - } + /** + * Create the HTML DOM for the DataAxis + */ + DataAxis.prototype.show = function () { + this.hidden = false; + if (!this.dom.frame.parentNode) { + if (this.options.orientation == "left") { + this.body.dom.left.appendChild(this.dom.frame); + } else { + this.body.dom.right.appendChild(this.dom.frame); + } + } - // draw the selected nodes on top - for (var i = 0; i < selected.length; i++) { - node = nodes[selected[i]]; - node.draw(ctx); - } - }, - writable: true, - configurable: true - }, - _drawEdges: { + if (!this.dom.lineContainer.parentNode) { + this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer); + } + }; + /** + * Create the HTML DOM for the DataAxis + */ + DataAxis.prototype.hide = function () { + this.hidden = true; + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } - /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _drawEdges(ctx) { - var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge; + if (this.dom.lineContainer.parentNode) { + this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer); + } + }; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edge.draw(ctx); - } - } - }, - writable: true, - configurable: true - }, - _drawControlNodes: { + /** + * Set a range (start and end) + * @param end + * @param start + * @param end + */ + DataAxis.prototype.setRange = function (start, end) { + if (this.master == false && this.options.alignZeros == true && this.zeroCrossing != -1) { + if (start > 0) { + start = 0; + } + } + this.range.start = start; + this.range.end = end; + }; - /** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _drawControlNodes(ctx) { - var edges = this.body.edges; - var edgeIndices = this.body.edgeIndices; - var edge; + /** + * Repaint the component + * @return {boolean} Returns true if the component is resized + */ + DataAxis.prototype.redraw = function () { + var resized = false; + var activeGroups = 0; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - edge._drawControlNodes(ctx); - } - }, - writable: true, - configurable: true - }, - _determineBrowserMethod: { + // Make sure the line container adheres to the vertical scrolling. + this.dom.lineContainer.style.top = this.body.domProps.scrollTop + "px"; - /** - * 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 + 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", "")); - return CanvasRenderer; - })(); + // 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; - module.exports = CanvasRenderer; + var props = this.props; + var frame = this.dom.frame; -/***/ }, -/* 56 */ -/***/ function(module, exports, __webpack_require__) { + // update classname + frame.className = "dataaxis"; - "use strict"; + // calculate character width and height + this._calculateCharSize(); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var orientation = this.options.orientation; + var showMinorLabels = this.options.showMinorLabels; + var showMajorLabels = this.options.showMajorLabels; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // determine the width and height of the elements for the axis + props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0; + props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0; - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(43); + 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; - var util = __webpack_require__(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; + + if (this.options.icons == true) { + this._redrawGroupIcons(); + } else { + this._cleanupIcons(); + } + + this._redrawTitle(orientation); + } + return resized; + }; /** - * 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. + * Repaint major and minor text labels and vertical grid lines * @private */ - var Canvas = (function () { - function Canvas(body) { - var _this = this; - _classCallCheck(this, Canvas); + DataAxis.prototype._redrawLabels = function () { + var resized = false; + DOMutil.prepareElements(this.DOMelements.lines); + DOMutil.prepareElements(this.DOMelements.labels); - this.body = body; + var orientation = this.options.orientation; - this.options = {}; - this.defaultOptions = { - width: "100%", - height: "100%" - }; - util.extend(this.options, this.defaultOptions); + // calculate range and step (step such that we have space for 7 characters per label) + var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced; - this.body.emitter.once("resize", function (obj) { - _this.body.view.translation.x = obj.width * 0.5;_this.body.view.translation.y = obj.height * 0.5; - }); - this.body.emitter.on("destroy", function () { - return _this.hammer.destroy(); - }); + var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight, this.options.customRange[this.options.orientation], this.master == false && this.options.alignZeros // doess the step have to align zeros? only if not master and the options is on + ); - this.pixelRatio = 1; - } + 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); - _prototypeProperties(Canvas, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); + this.stepPixels = stepPixels; + + var amountOfSteps = this.height / stepPixels; + var stepDifference = 0; + + // the slave axis needs to use the same horizontal lines as the master axis. + if (this.master == false) { + stepPixels = this.stepPixelsForced; + stepDifference = Math.round(this.dom.frame.offsetHeight / stepPixels - amountOfSteps); + for (var i = 0; i < 0.5 * stepDifference; i++) { + step.previous(); + } + amountOfSteps = this.height / stepPixels; + + if (this.zeroCrossing != -1 && this.options.alignZeros == true) { + var zeroStepDifference = step.marginEnd / step.step - this.zeroCrossing; + if (zeroStepDifference > 0) { + for (var i = 0; i < zeroStepDifference; i++) { + step.next(); } - }, - 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); + } else if (zeroStepDifference < 0) { + for (var i = 0; i < -zeroStepDifference; i++) { + step.previous(); } + } + } + } else { + amountOfSteps += 0.25; + } - 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.valueAtZero = step.marginEnd; + var marginStartPos = 0; - this.frame.canvas = document.createElement("canvas"); - this.frame.canvas.style.position = "relative"; - this.frame.appendChild(this.frame.canvas); + // do not draw the first label + var max = 1; - 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); + // Get the number of decimal places + var decimals; + if (this.options.format[orientation] !== undefined) { + decimals = this.options.format[orientation].decimals; + } - this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); - } + 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(); - // add the frame to the container element - this.body.container.appendChild(this.frame); + 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); + } - this.body.view.scale = 1; - this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; + 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._bindHammer(); - }, - writable: true, - configurable: true - }, - _bindHammer: { + if (this.master == true && step.current == 0) { + this.zeroCrossing = max; + } + max++; + } - /** - * This function binds hammer, it can be repeated over and over due to the uniqueness check. - * @private - */ - value: function _bindHammer() { - var _this = this; - if (this.hammer !== undefined) { - this.hammer.destroy(); - } - this.drag = {}; - this.pinch = {}; + if (this.master == false) { + this.conversionFactor = y / (this.valueAtZero - step.current); + } else { + this.conversionFactor = this.dom.frame.offsetHeight / step.marginRange; + } - // init hammer - this.hammer = new Hammer(this.frame.canvas); - this.hammer.get("pinch").set({ enable: true }); + // 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; - hammerUtil.onTouch(this.hammer, function (event) { - _this.body.eventListeners.onTouch(event); - }); - this.hammer.on("tap", function (event) { - _this.body.eventListeners.onTap(event); - }); - this.hammer.on("doubletap", function (event) { - _this.body.eventListeners.onDoubleTap(event); - }); - this.hammer.on("press", function (event) { - _this.body.eventListeners.onHold(event); - }); - this.hammer.on("panstart", function (event) { - _this.body.eventListeners.onDragStart(event); - }); - this.hammer.on("panmove", function (event) { - _this.body.eventListeners.onDrag(event); - }); - this.hammer.on("panend", function (event) { - _this.body.eventListeners.onDragEnd(event); - }); - this.hammer.on("pinch", function (event) { - _this.body.eventListeners.onPinch(event); - }); + // 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; + } - // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? - this.frame.canvas.addEventListener("mousewheel", function (event) { - _this.body.eventListeners.onMouseWheel(event); - }); - this.frame.canvas.addEventListener("DOMMouseScroll", function (event) { - _this.body.eventListeners.onMouseWheel(event); - }); + return resized; + }; - this.frame.canvas.addEventListener("mousemove", function (event) { - _this.body.eventListeners.onMouseMove(event); - }); + DataAxis.prototype.convertValue = function (value) { + var invertedValue = this.valueAtZero - value; + var convertedValue = invertedValue * this.conversionFactor; + return convertedValue; + }; - this.hammerFrame = new Hammer(this.frame); - hammerUtil.onRelease(this.hammerFrame, function (event) { - _this.body.eventListeners.onRelease(event); - }); - }, - writable: true, - configurable: true - }, - setSize: { + /** + * Create a label for the axis at position x + * @private + * @param y + * @param text + * @param orientation + * @param className + * @param characterHeight + */ + DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) { + // reuse redundant label + var label = DOMutil.getDOMElement("div", this.DOMelements.labels, this.dom.frame); //this.dom.redundant.labels.shift(); + label.className = className; + label.innerHTML = text; + if (orientation == "left") { + label.style.left = "-" + this.options.labelOffsetX + "px"; + label.style.textAlign = "right"; + } else { + label.style.right = "-" + this.options.labelOffsetX + "px"; + label.style.textAlign = "left"; + } + label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + "px"; - /** - * 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; + text += ""; - this.frame.canvas.style.width = "100%"; - this.frame.canvas.style.height = "100%"; + var largestWidth = Math.max(this.props.majorCharWidth, this.props.minorCharWidth); + if (this.maxLabelSize < text.length * largestWidth) { + this.maxLabelSize = text.length * largestWidth; + } + }; - this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; - this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; + /** + * Create a minor line for the axis at position y + * @param y + * @param orientation + * @param className + * @param offset + * @param width + */ + DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) { + if (this.master == true) { + var line = DOMutil.getDOMElement("div", this.DOMelements.lines, this.dom.lineContainer); //this.dom.redundant.lines.shift(); + line.className = className; + line.innerHTML = ""; - this.options.width = width; - this.options.height = height; + if (orientation == "left") { + line.style.left = this.width - offset + "px"; + } else { + line.style.right = this.width - offset + "px"; + } - emitEvent = true; - } else { - // this would adapt the width of the canvas to the width from 100% if and only if - // there is a change. + line.style.width = width + "px"; + line.style.top = y + "px"; + } + }; - 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; - } - } + /** + * Create a title for the axis + * @private + * @param orientation + */ + DataAxis.prototype._redrawTitle = function (orientation) { + DOMutil.prepareElements(this.DOMelements.title); - 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: { + // Check if the title is defined for this axes + if (this.options.title[orientation] !== undefined && this.options.title[orientation].text !== undefined) { + var title = DOMutil.getDOMElement("div", this.DOMelements.title, this.dom.frame); + title.className = "yAxis title " + orientation; + title.innerHTML = this.options.title[orientation].text; + // Add style - if provided + if (this.options.title[orientation].style !== undefined) { + util.addCssText(title, this.options.title[orientation].style); + } - /** - * 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: { + if (orientation == "left") { + title.style.left = this.props.titleCharHeight + "px"; + } else { + title.style.right = this.props.titleCharHeight + "px"; + } - /** - * 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: { + title.style.width = this.height + "px"; + } + + // we need to clean up in case we did not use all elements. + DOMutil.cleanupElements(this.DOMelements.title); + }; + + + + + /** + * Determine the size of text on the axis (both major and minor axis). + * The size is calculated only once and then cached in this.props. + * @private + */ + DataAxis.prototype._calculateCharSize = function () { + // determine the char width and height on the minor axis + if (!("minorCharHeight" in this.props)) { + var textMinor = document.createTextNode("0"); + var measureCharMinor = document.createElement("div"); + measureCharMinor.className = "yAxis minor measure"; + measureCharMinor.appendChild(textMinor); + this.dom.frame.appendChild(measureCharMinor); - /** - * 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: { + this.props.minorCharHeight = measureCharMinor.clientHeight; + this.props.minorCharWidth = measureCharMinor.clientWidth; - /** - * 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: { + this.dom.frame.removeChild(measureCharMinor); + } + 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); - /** - * - * @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: { + this.props.majorCharHeight = measureCharMajor.clientHeight; + this.props.majorCharWidth = measureCharMajor.clientWidth; - /** - * - * @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.dom.frame.removeChild(measureCharMajor); + } - return Canvas; - })(); + 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); - module.exports = Canvas; + this.props.titleCharHeight = measureCharTitle.clientHeight; + this.props.titleCharWidth = measureCharTitle.clientWidth; + + this.dom.frame.removeChild(measureCharTitle); + } + }; + + module.exports = DataAxis; /***/ }, -/* 57 */ +/* 47 */ /***/ 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); }; + /** + * @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; + + this.autoScale = true; + this.stepIndex = 0; + this.step = 1; + this.scale = 1; + + this.marginStart; + this.marginEnd; + this.deadSpace = 0; + + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; + + this.alignZeros = alignZeros; + + this.setRange(start, end, minimumStep, containerHeight, customRange); + } + - 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. + * 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 util = __webpack_require__(1); + if (this._start == this._end) { + this._start -= 0.75; + this._end += 1; + } - var View = (function () { - function View(body, canvas) { - var _this = this; - _classCallCheck(this, View); + if (this.autoScale == true) { + this.setMinimumStep(minimumStep, containerHeight); + } - this.body = body; - this.canvas = canvas; + this.setFirst(customRange); + }; - this.animationSpeed = 1 / this.renderRefreshRate; - this.animationEasingFunction = "easeInOutQuint"; - this.easingTime = 0; - this.sourceScale = 0; - this.targetScale = 0; - this.sourceTranslation = 0; - this.targetTranslation = 0; - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; - this.touchTime = 0; + /** + * 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); - this.viewFunction = undefined; + var minorStepIdx = -1; + var magnitudefactor = Math.pow(10, orderOfMagnitude); - 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)); + var start = 0; + if (orderOfMagnitude < 0) { + start = orderOfMagnitude; } - _prototypeProperties(View, null, { - setOptions: { - value: function setOptions() { - var options = arguments[0] === undefined ? {} : arguments[0]; - this.options = options; - }, - writable: true, - configurable: true - }, - _getRange: { + 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]; + }; - // zoomExtent - /** - * Find the center position of the network - * @private - */ - value: function _getRange() { - var specificNodes = arguments[0] === undefined ? [] : arguments[0]; - var minY = 1000000000, - maxY = -1000000000, - minX = 1000000000, - maxX = -1000000000, - node; - if (specificNodes.length > 0) { - for (var i = 0; i < specificNodes.length; i++) { - node = this.body.nodes[specificNodes[i]]; - if (minX > node.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; - } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; - } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.boundingBox.bottom; - } // top is negative, bottom is positive - } - } else { - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (minX > node.shape.boundingBox.left) { - minX = node.shape.boundingBox.left; - } - if (maxX < node.shape.boundingBox.right) { - maxX = node.shape.boundingBox.right; - } - if (minY > node.shape.boundingBox.bottom) { - minY = node.shape.boundingBox.top; - } // top is negative, bottom is positive - if (maxY < node.shape.boundingBox.top) { - maxY = node.shape.boundingBox.bottom; - } // top is negative, bottom is positive - } - } - } - 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: { + /** + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date + */ + DataStep.prototype.setFirst = function (customRange) { + if (customRange === undefined) { + customRange = {}; + } + var niceStart = customRange.min === undefined ? this._start - this.scale * 2 * this.minorSteps[this.stepIndex] : customRange.min; + var niceEnd = customRange.max === undefined ? this._end + this.scale * this.minorSteps[this.stepIndex] : customRange.max; - /** - * @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: { + this.marginEnd = customRange.max === undefined ? this.roundToMinor(niceEnd) : customRange.max; + this.marginStart = customRange.min === undefined ? this.roundToMinor(niceStart) : customRange.min; + // 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; + } - /** - * 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.deadSpace = this.roundToMinor(niceEnd) - niceEnd + this.roundToMinor(niceStart) - niceStart; + this.marginRange = this.marginEnd - this.marginStart; - if (initialZoom === true) { - // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. - var positionDefined = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (node.predefinedPosition == true) { - positionDefined += 1; - } - } - } - if (positionDefined > 0.5 * this.body.nodeIndices.length) { - this.zoomExtent(options, false); - return; - } - range = this._getRange(options.nodes); + this.current = this.marginEnd; + }; - var numberOfNodes = this.body.nodeIndices.length; - zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + 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; + } + }; - // 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; - } + /** + * 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 (zoomLevel > 1) { - zoomLevel = 1; - } + /** + * Do the next step + */ + DataStep.prototype.next = function () { + var prev = this.current; + this.current -= this.step; - var center = this._findCenter(range); - var animationOptions = { position: center, scale: zoomLevel, animation: options }; - this.moveTo(animationOptions); - }, - writable: true, - configurable: true - }, - focusOnNode: { + // safety mechanism: if current time is still unchanged, move to the end + if (this.current == prev) { + this.current = this._end; + } + }; - // animation + /** + * Do the next step + */ + DataStep.prototype.previous = function () { + this.current += this.step; + this.marginEnd += this.step; + this.marginRange = this.marginEnd - this.marginStart; + }; - /** - * Center a node in view. - * - * @param {Number} nodeId - * @param {Number} [options] - */ - value: function focusOnNode(nodeId) { - var options = arguments[1] === undefined ? {} : arguments[1]; - if (this.body.nodes[nodeId] !== undefined) { - var nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y }; - options.position = nodePosition; - options.lockedOnNode = nodeId; - this.moveTo(options); - } else { - console.log("Node: " + nodeId + " cannot be found."); - } - }, - writable: true, - configurable: true - }, - moveTo: { - /** - * - * @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels - * | options.scale = Number // scale to move to - * | options.position = {x:Number, y:Number} // position to move to - * | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to - */ - value: function moveTo(options) { - if (options === undefined) { - options = {}; - return; - } - if (options.offset === undefined) { - options.offset = { x: 0, y: 0 }; - } - if (options.offset.x === undefined) { - options.offset.x = 0; - } - if (options.offset.y === undefined) { - options.offset.y = 0; - } - if (options.scale === undefined) { - options.scale = this.body.view.scale; - } - if (options.position === undefined) { - options.position = this.body.view.translation; - } - if (options.animation === undefined) { - options.animation = { duration: 0 }; - } - if (options.animation === false) { - options.animation = { duration: 0 }; - } - if (options.animation === true) { - options.animation = {}; + /** + * 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; } - if (options.animation.duration === undefined) { - options.animation.duration = 1000; - } // default duration - if (options.animation.easingFunction === undefined) { - options.animation.easingFunction = "easeInOutQuad"; - } // default easing function + } + } + } - this.animateView(options); - }, - writable: true, - configurable: true - }, - animateView: { + return toPrecision; + }; - /** - * - * @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; - } + /** + * 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; + }; - // 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. - } + module.exports = DataStep; - this.sourceScale = this.body.view.scale; - this.sourceTranslation = this.body.view.translation; - this.targetScale = options.scale; +/***/ }, +/* 48 */ +/***/ function(module, exports, __webpack_require__) { - // 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 - }; + "use strict"; - // if the time is set to 0, don't do an animation - if (options.animation.duration == 0) { - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - } else { - this.body.view.scale = this.targetScale; - this.body.view.translation = this.targetTranslation; - this.body.emitter.emit("_requestRedraw"); - } - } else { - this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's - this.animationEasingFunction = options.animation.easingFunction; + 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); + /** + * /** + * @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; + } - this.viewFunction = this._transitionRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); - this.body.emitter.emit("_startRendering"); - } - }, - writable: true, - configurable: true - }, - _lockedRedraw: { - /** - * used to animate smoothly by hijacking the redraw function. - * @private - */ - value: function _lockedRedraw() { - var nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y }; - var viewCenter = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }); - var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node - x: viewCenter.x - nodePosition.x, - y: viewCenter.y - nodePosition.y - }; - var sourceTranslation = this.body.view.translation; - var targetTranslation = { - x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, - y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y - }; + /** + * this 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 = []; + } + }; - this.body.view.translation = targetTranslation; - }, - writable: true, - configurable: true - }, - releaseNode: { - value: function releaseNode() { - if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.lockedOnNodeId = undefined; - this.lockedOnNodeOffset = undefined; - } - }, - writable: true, - configurable: true - }, - _transitionRedraw: { - /** - * - * @param easingTime - * @private - */ - value: function _transitionRedraw() { - var finished = arguments[0] === undefined ? false : arguments[0]; - this.easingTime += this.animationSpeed; - this.easingTime = finished === true ? 1 : this.easingTime; + /** + * this is used for plotting barcharts, this way, we only have to calculate it once. + * @param pos + */ + GraphGroup.prototype.setZeroPosition = function (pos) { + this.zeroPosition = pos; + }; - var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - 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 - }; + /** + * 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); - // cleanup - if (this.easingTime >= 1) { - this.body.emitter.off("initRedraw", this.viewFunction); - this.easingTime = 0; - if (this.lockedOnNodeId != undefined) { - this.viewFunction = this._lockedRedraw.bind(this); - this.body.emitter.on("initRedraw", this.viewFunction); + util.mergeOptions(this.options, options, "catmullRom"); + util.mergeOptions(this.options, options, "drawPoints"); + util.mergeOptions(this.options, options, "shaded"); + + if (options.catmullRom) { + if (typeof options.catmullRom == "object") { + if (options.catmullRom.parametrization) { + if (options.catmullRom.parametrization == "uniform") { + this.options.catmullRom.alpha = 0; + } else if (options.catmullRom.parametrization == "chordal") { + this.options.catmullRom.alpha = 1; + } else { + this.options.catmullRom.parametrization = "centripetal"; + this.options.catmullRom.alpha = 0.5; } - this.body.emitter.emit("animationFinished"); } - }, - writable: true, - configurable: true + } } - }); + } - return View; - })(); + 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); + } + }; - module.exports = View; -/***/ }, -/* 58 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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); + }; - "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + /** + * 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 _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + 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"); + } + + 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); + + var offset = Math.round((iconWidth - 2 * barWidth) / 3); + + DOMutil.drawBar(x + 0.5 * barWidth + offset, y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + " bar", JSONcontainer, SVGcontainer); + DOMutil.drawBar(x + 1.5 * barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + " bar", JSONcontainer, SVGcontainer); + } + }; - 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. + * return the legend entree for this group. * + * @param iconWidth + * @param iconHeight + * @returns {{icon: HTMLElement, label: (group.content|*|string), orientation: (.options.yAxisOrientation|*)}} */ + GraphGroup.prototype.getLegend = function (iconWidth, iconHeight) { + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.drawIcon(0, 0.5 * iconHeight, [], svg, iconWidth, iconHeight); + return { icon: svg, label: this.content, orientation: this.options.yAxisOrientation }; + }; - var util = __webpack_require__(1); - - var NavigationHandler = _interopRequire(__webpack_require__(75)); + GraphGroup.prototype.getYRange = function (groupData) { + return this.type.getYRange(groupData); + }; - var Popup = _interopRequire(__webpack_require__(76)); + GraphGroup.prototype.draw = function (dataset, group, framework) { + this.type.draw(dataset, group, framework); + }; - var InteractionHandler = (function () { - function InteractionHandler(body, canvas, selectionHandler) { - _classCallCheck(this, InteractionHandler); - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; - this.navigationHandler = new NavigationHandler(body, canvas); + module.exports = GraphGroup; - // 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); +/***/ }, +/* 49 */ +/***/ function(module, exports, __webpack_require__) { - this.touchTime = 0; - this.drag = {}; - this.pinch = {}; - this.hoverObj = { nodes: {}, edges: {} }; - this.popup = undefined; - this.popupObj = undefined; - this.popupTimer = undefined; + "use strict"; - this.body.functions.getPointer = this.getPointer.bind(this); + /** + * Created by Alex on 11/11/2014. + */ + var DOMutil = __webpack_require__(6); + var Points = __webpack_require__(50); + function Line(groupId, options) { + this.groupId = groupId; + this.options = options; + } - this.options = {}; - this.defaultOptions = { - dragNodes: true, - dragView: true, - zoomView: true, - hoverEnabled: false, - showNavigationIcons: false, - tooltip: { - delay: 300, - fontColor: "black", - fontSize: 14, // px - fontFace: "verdana", - color: { - border: "#666", - background: "#FFFFC6" - } - }, - keyboard: { - enabled: false, - speed: { x: 10, y: 10, zoom: 0.02 }, - bindToWindow: true - } - }; - util.extend(this.options, this.defaultOptions); + 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(InteractionHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - // extend all but the values in fields - var fields = ["keyboard", "tooltip"]; - util.selectiveNotDeepExtend(fields, this.options, options); - - // merge the keyboard options in. - util.mergeOptions(this.options, options, "keyboard"); - if (options.tooltip) { - util.extend(this.options.tooltip, options.tooltip); - if (options.tooltip.color) { - this.options.tooltip.color = util.parseColor(options.tooltip.color); - } - } - } + /** + * 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); + } - this.navigationHandler.setOptions(this.options); - }, - writable: true, - configurable: true - }, - getPointer: { + // 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); - /** - * 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: { + // draw points + if (group.options.drawPoints.enabled == true) { + Points.draw(dataset, group, framework); + } + } + } + }; - /** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ - value: function onTouch(event) { - if (new Date().valueOf() - this.touchTime > 100) { - this.drag.pointer = this.getPointer(event.center); - this.drag.pinched = false; - this.pinch.scale = this.body.view.scale; - // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) - this.touchTime = new Date().valueOf(); - } - }, - writable: true, - configurable: true - }, - onTap: { - /** - * handle tap/click event: select/unselect a node - * @private - */ - value: function onTap(event) { - var pointer = this.getPointer(event.center); + /** + * 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; - 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("select", this.selectionHandler.getSelection()); - } + // Catmull-Rom to Cubic Bezier conversion matrix + // 0 1 0 0 + // -1/6 1 1/6 0 + // 0 1/6 1 -1/6 + // 0 0 1 0 - this.selectionHandler._generateClickEvent("click", pointer); - }, - writable: true, - configurable: true - }, - onDoubleTap: { + // bp0 = { x: p1.x, y: p1.y }; + bp1 = { x: (-p0.x + 6 * p1.x + p2.x) * normalization, y: (-p0.y + 6 * p1.y + p2.y) * normalization }; + bp2 = { x: (p1.x + 6 * p2.x - p3.x) * normalization, y: (p1.y + 6 * p2.y - p3.y) * normalization }; + // bp0 = { x: p2.x, y: p2.y }; + d += "C" + bp1.x + "," + bp1.y + " " + bp2.x + "," + bp2.y + " " + p2.x + "," + p2.y + " "; + } - /** - * handle doubletap event - * @private - */ - value: function onDoubleTap(event) { - var pointer = this.getPointer(event.center); - this.selectionHandler._generateClickEvent("doubleClick", pointer); - }, - writable: true, - configurable: true - }, - onHold: { + return d; + }; + /** + * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm. + * By default, the centripetal parameterization is used because this gives the nicest results. + * These parameterizations are relatively heavy because the distance between 4 points have to be calculated. + * + * One optimization can be used to reuse distances since this is a sliding window approach. + * @param data + * @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)); - /** - * handle long tap event: multi select nodes - * @private - */ - value: function onHold(event) { - var pointer = this.getPointer(event.center); + // Catmull-Rom to Cubic Bezier conversion matrix - var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); + // A = 2d1^2a + 3d1^a * d2^a + d3^2a + // B = 2d3^2a + 3d3^a * d2^a + d2^2a - if (selectionChanged === true) { - // select or longpress - this.body.emitter.emit("select", this.selectionHandler.getSelection()); - } + // [ 0 1 0 0 ] + // [ -d2^2a /N A/N d1^2a /N 0 ] + // [ 0 d3^2a /M B/M -d2^2a /M ] + // [ 0 0 1 0 ] - this.selectionHandler._generateClickEvent("click", pointer); - }, - writable: true, - configurable: true - }, - onRelease: { + 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; + } - /** - * handle the release of the screen - * - * @private - */ - value: function onRelease(event) { - this.body.emitter.emit("release", event); - }, - writable: true, - configurable: true - }, - onDragStart: { + bp1 = { x: (-d2pow2A * p0.x + A * p1.x + d1pow2A * p2.x) * N, + y: (-d2pow2A * p0.y + A * p1.y + d1pow2A * p2.y) * N }; + bp2 = { x: (d3pow2A * p1.x + B * p2.x - d2pow2A * p3.x) * M, + y: (d3pow2A * p1.y + B * p2.y - d2pow2A * p3.y) * M }; - /** - * 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); - } + 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 + " "; + } - // note: drag.pointer is set in onTouch to get the initial touch location - var node = this.selectionHandler.getNodeAt(this.drag.pointer); + return d; + } + }; - this.drag.dragging = true; - this.drag.selection = []; - this.drag.translation = util.extend({}, this.body.view.translation); // copy the object - this.drag.nodeId = undefined; + /** + * 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; + }; - this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); + module.exports = Line; - if (node !== undefined && this.options.dragNodes === true) { - this.drag.nodeId = node.id; - // select the clicked node if not yet selected - if (node.isSelected() === false) { - this.selectionHandler.unselectAll(); - this.selectionHandler.selectObject(node); - } +/***/ }, +/* 50 */ +/***/ function(module, exports, __webpack_require__) { - 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, + "use strict"; - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: object.x, - y: object.y, - xFixed: object.options.fixed.x, - yFixed: object.options.fixed.y - }; + /** + * Created by Alex on 11/11/2014. + */ + var DOMutil = __webpack_require__(6); - object.options.fixed.x = true; - object.options.fixed.y = true; + function Points(groupId, options) { + this.groupId = groupId; + this.options = options; + } - this.drag.selection.push(s); - } - } - } - }, - writable: true, - configurable: true - }, - onDrag: { + 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 }; + }; - /** - * handle drag event - * @private - */ - value: function onDrag(event) { - var _this = this; - if (this.drag.pinched === true) { - return; - } + Points.prototype.draw = function (dataset, group, framework, offset) { + Points.draw(dataset, group, framework, offset); + }; - // remove the focus on node if it is focussed on by the focusOnNode - this.body.emitter.emit("unlockNode"); + /** + * 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); + } + }; - var pointer = this.getPointer(event.center); - var selection = this.drag.selection; - if (selection && selection.length && this.options.dragNodes === true) { - (function () { - // calculate delta's and new location - var deltaX = pointer.x - _this.drag.pointer.x; - var deltaY = pointer.y - _this.drag.pointer.y; - // update position of all selected nodes - selection.forEach(function (selection) { - var node = selection.node; - // only move the node if it was not fixed initially - if (selection.xFixed === false) { - node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); - } - // only move the node if it was not fixed initially - if (selection.yFixed === false) { - node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); - } - }); + module.exports = Points; - // 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; +/***/ }, +/* 51 */ +/***/ function(module, exports, __webpack_require__) { - 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: { + "use strict"; + /** + * Created by Alex on 11/11/2014. + */ + var DOMutil = __webpack_require__(6); + var Points = __webpack_require__(50); - /** - * handle drag start event - * @private - */ - value: function onDragEnd(event) { - this.drag.dragging = false; - var selection = this.drag.selection; - if (selection && selection.length) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.options.fixed.x = s.xFixed; - s.node.options.fixed.y = s.yFixed; - }); - this.body.emitter.emit("startSimulation"); - } else { - this.body.emitter.emit("_requestRedraw"); - } + function Bargraph(groupId, options) { + this.groupId = groupId; + this.options = options; + } - this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); - }, - writable: true, - configurable: true - }, - onPinch: { + 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; + } + }; - /** - * Handle pinch event - * @param event - * @private - */ - value: function onPinch(event) { - var pointer = this.getPointer(event.center); + /** + * draw a bar graph + * + * @param groupIds + * @param processedGroupData + */ + Bargraph.draw = function (groupIds, processedGroupData, framework) { + var combinedData = []; + var intersections = {}; + var coreDistance; + var key, drawData; + var group; + var i, j; + var barPoints = 0; - this.drag.pinched = true; - if (this.pinch.scale === undefined) { - this.pinch.scale = 1; + // 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; } + } + } + } - // TODO: enabled moving while pinching? - var scale = this.pinch.scale * event.scale; - this.zoom(scale, pointer); - }, - writable: true, - configurable: true - }, - zoom: { + if (barPoints == 0) { + return; + } + // 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; + } + }); - /** - * 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; - } + // get intersections + Bargraph._getDataIntersections(intersections, combinedData); - var preScaleDragPointer = undefined; - if (this.drag !== undefined) { - if (this.drag.dragging === true) { - preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); - } - } - // + this.canvas.frame.canvas.clientHeight / 2 - var translation = this.body.view.translation; + // plot barchart + for (i = 0; i < combinedData.length; i++) { + group = framework.groups[combinedData[i].groupId]; + var minWidth = 0.1 * group.options.barChart.width; - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + key = combinedData[i].x; + var heightOffset = 0; + if (intersections[key] === undefined) { + if (i + 1 < combinedData.length) { + coreDistance = Math.abs(combinedData[i + 1].x - key); + } + if (i > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - key)); + } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + } else { + var nextKey = i + (intersections[key].amount - intersections[key].resolved); + var prevKey = i - (intersections[key].resolved + 1); + if (nextKey < combinedData.length) { + coreDistance = Math.abs(combinedData[nextKey].x - key); + } + if (prevKey > 0) { + coreDistance = Math.min(coreDistance, Math.abs(combinedData[prevKey].x - key)); + } + drawData = Bargraph._getSafeDrawData(coreDistance, group, minWidth); + intersections[key].resolved += 1; - this.body.view.scale = scale; - this.body.view.translation = { x: tx, y: ty }; + 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); + } + } + }; - if (preScaleDragPointer != undefined) { - var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); - this.drag.pointer.x = postScaleDragPointer.x; - this.drag.pointer.y = postScaleDragPointer.y; - } - this.body.emitter.emit("_requestRedraw"); + /** + * 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; + } + } + }; + - if (scaleOld < scale) { - this.body.emitter.emit("zoom", { direction: "+" }); - } else { - this.body.emitter.emit("zoom", { direction: "-" }); - } - } - }, - writable: true, - configurable: true - }, - onMouseWheel: { + /** + * 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; + } + } - /** - * 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; - } + return { width: width, offset: offset }; + }; - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta !== 0) { - // calculate the new scale - var scale = this.body.view.scale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); - } - scale *= 1 + zoom; + 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 = {}; - // calculate the pointer location - var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + Bargraph._getDataIntersections(intersections, barCombinedData); + groupRanges[groupLabel] = Bargraph._getStackedBarYRange(intersections, barCombinedData); + groupRanges[groupLabel].yAxisOrientation = orientation; + groupIds.push(groupLabel); + } + }; - // apply the new scale - this.zoom(scale, pointer); - } + 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; + } + } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); - }, - writable: true, - configurable: true - }, - onMouseMove: { + return { min: yMin, max: yMax }; + }; + module.exports = Bargraph; - /** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private - */ - value: function onMouseMove(event) { - var _this = this; - var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); - var popupVisible = false; +/***/ }, +/* 52 */ +/***/ function(module, exports, __webpack_require__) { - // check if the previously selected node is still selected - if (this.popup !== undefined) { - if (this.popup.hidden === false) { - this._checkHidePopup(pointer); - } + "use strict"; - // 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(); - } - } + var util = __webpack_require__(1); + var DOMutil = __webpack_require__(6); + var Component = __webpack_require__(25); - // 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(); - } + /** + * Legend for Graph2d + */ + function Legend(body, options, side, linegraphOptions) { + this.body = body; + this.defaultOptions = { + enabled: true, + icons: true, + iconSize: 20, + iconSpacing: 6, + left: { + visible: true, + position: "top-left" // top/bottom - left,center,right + }, + right: { + visible: true, + position: "top-left" // top/bottom - left,center,right + } + }; + this.side = side; + this.options = util.extend({}, this.defaultOptions); + this.linegraphOptions = linegraphOptions; - // start a timeout that will check if the mouse is positioned above an element - if (popupVisible === false) { - if (this.popupTimer !== undefined) { - clearInterval(this.popupTimer); // stop any running calculationTimer - this.popupTimer = undefined; - } - if (!this.drag.dragging) { - this.popupTimer = setTimeout(function () { - return _this._checkShowPopup(pointer); - }, this.options.tooltip.delay); - } - } + this.svgElements = {}; + this.dom = {}; + this.groups = {}; + this.amountOfGroups = 0; + this._create(); - /** - * Adding hover highlights - */ - if (this.options.hoverEnabled === true) { - // removing all hover highlights - for (var edgeId in this.hoverObj.edges) { - if (this.hoverObj.edges.hasOwnProperty(edgeId)) { - this.hoverObj.edges[edgeId].hover = false; - delete this.hoverObj.edges[edgeId]; - } - } + this.setOptions(options); + } - // adding hover highlights - var obj = this.selectionHandler.getNodeAt(pointer); - if (obj == undefined) { - obj = this.selectionHandler.getEdgeAt(pointer); - } - if (obj != undefined) { - this.selectionHandler.hoverObject(obj); - } + Legend.prototype = new Component(); - // removing all node hover highlights except for the selected one. - for (var nodeId in this.hoverObj.nodes) { - if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { - if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == undefined) { - this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); - delete this.hoverObj.nodes[nodeId]; - } - } - } - this.body.emitter.emit("_requestRedraw"); - } - }, - writable: true, - configurable: true - }, - _checkShowPopup: { + 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; + }; - /** - * Check if there is an element on the given position in the network - * (a node or edge). If so, and if this element has a title, - * show a popup window with its title. - * - * @param {{x:Number, y:Number}} pointer - * @private - */ - value: function _checkShowPopup(pointer) { - var x = this.canvas._XconvertDOMtoCanvas(pointer.x); - var y = this.canvas._YconvertDOMtoCanvas(pointer.y); - var pointerObj = { - left: x, - top: y, - right: x, - bottom: y - }; + Legend.prototype.removeGroup = function (label) { + if (this.groups.hasOwnProperty(label)) { + delete this.groups[label]; + this.amountOfGroups -= 1; + } + }; - var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; - var nodeUnderCursor = false; - var popupType = "node"; + 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"; - // check if a node is under the cursor. - if (this.popupObj === undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodeIndices = this.body.nodeIndices; - var nodes = this.body.nodes; - var node = undefined; - var overlappingNodes = []; - for (var i = 0; i < nodeIndices.length; i++) { - node = nodes[nodeIndices[i]]; - if (node.isOverlappingWith(pointerObj) === true) { - if (node.getTitle() !== undefined) { - overlappingNodes.push(nodeIndices[i]); - } - } - } + this.dom.textArea = document.createElement("div"); + this.dom.textArea.className = "legendText"; + this.dom.textArea.style.position = "relative"; + this.dom.textArea.style.top = "0px"; - if (overlappingNodes.length > 0) { - // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others - this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; - // if you hover over a node, the title of the edge is not supposed to be shown. - nodeUnderCursor = true; - } - } + this.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%"; - if (this.popupObj === undefined && nodeUnderCursor == false) { - // search the edges for overlap - var edgeIndices = this.body.edgeIndices; - var edges = this.body.edges; - var edge = undefined; - var overlappingEdges = []; - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.isOverlappingWith(pointerObj) === true) { - if (edge.connected === true && edge.getTitle() !== undefined) { - overlappingEdges.push(edgeIndices[i]); - } - } - } + this.dom.frame.appendChild(this.svg); + this.dom.frame.appendChild(this.dom.textArea); + }; - if (overlappingEdges.length > 0) { - this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; - popupType = "edge"; - } - } + /** + * 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); + } + }; - if (this.popupObj !== undefined) { - // show popup message window - if (this.popupObj.id != previousPopupObjId) { - if (this.popup === undefined) { - this.popup = new Popup(this.frame, this.options.tooltip); - } + /** + * 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.popup.popupTargetType = popupType; - this.popup.popupTargetId = this.popupObj.id; + Legend.prototype.setOptions = function (options) { + var fields = ["enabled", "orientation", "icons", "left", "right"]; + util.selectiveDeepExtend(fields, this.options, options); + }; - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - this.popup.setPosition(pointer.x + 3, pointer.y - 5); - this.popup.setText(this.popupObj.getTitle()); - this.popup.show(); - } - } else { - if (this.popup) { - this.popup.hide(); - } - } - }, - writable: true, - configurable: true - }, - _checkHidePopup: { + 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 = ""; + } - /** - * Check if the popup must be hidden, which is the case when the mouse is no - * longer hovering on the object - * @param {{x:Number, y:Number}} pointer - * @private - */ - value: function _checkHidePopup(pointer) { - var x = this.canvas._XconvertDOMtoCanvas(pointer.x); - var y = this.canvas._YconvertDOMtoCanvas(pointer.y); - var pointerObj = { - left: x, - top: y, - right: x, - bottom: y - }; + if (this.options[this.side].position == "top-left" || this.options[this.side].position == "top-right") { + this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px", "")) + "px"; + this.dom.frame.style.bottom = ""; + } else { + var scrollableHeight = this.body.domProps.center.height - this.body.domProps.centerContainer.height; + this.dom.frame.style.bottom = 4 + scrollableHeight + Number(this.body.dom.center.style.top.replace("px", "")) + "px"; + this.dom.frame.style.top = ""; + } - var stillOnObj = false; - if (this.popup.popupTargetType == "node") { - if (this.body.nodes[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + if (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(); + } - // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. - // we initially only check stillOnObj because this is much faster. - if (stillOnObj === true) { - var overNode = this.selectionHandler.getNodeAt(pointer); - stillOnObj = overNode.id == this.popup.popupTargetId; - } - } - } else { - if (this.selectionHandler.getNodeAt(pointer) === undefined) { - if (this.body.edges[this.popup.popupTargetId] !== undefined) { - stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); - } - } + var content = ""; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + content += this.groups[groupId].content + "
"; } + } + } + this.dom.textArea.innerHTML = content; + this.dom.textArea.style.lineHeight = 0.75 * this.options.iconSize + this.options.iconSpacing + "px"; + } + }; + 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; - if (stillOnObj === false) { - this.popupObj = undefined; - this.popup.hide(); + this.svg.style.width = iconWidth + 5 + iconOffset + "px"; + + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + if (this.groups[groupId].visible == true && (this.linegraphOptions.visibility[groupId] === undefined || this.linegraphOptions.visibility[groupId] == true)) { + this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight); + y += iconHeight + this.options.iconSpacing; } - }, - writable: true, - configurable: true + } } - }); - return InteractionHandler; - })(); + DOMutil.cleanupElements(this.svgElements); + } + }; - module.exports = InteractionHandler; + module.exports = Legend; /***/ }, -/* 59 */ +/* 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"); } }; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - /** - * Created by Alex on 2/27/2015. - */ + // Load custom shapes into CanvasRenderingContext2D + __webpack_require__(54); - var Node = __webpack_require__(66); + var Emitter = __webpack_require__(11); + var Hammer = __webpack_require__(19); var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); + var dotparser = __webpack_require__(55); + var gephiParser = __webpack_require__(56); + var Images = __webpack_require__(57); + var Activator = __webpack_require__(38); - var SelectionHandler = (function () { - function SelectionHandler(body, canvas) { - var _this = this; - _classCallCheck(this, SelectionHandler); + var Groups = _interopRequire(__webpack_require__(58)); - this.body = body; - this.canvas = canvas; - this.selectionObj = { nodes: [], edges: [] }; - this.forceSelectEdges = false; + var NodesHandler = _interopRequire(__webpack_require__(59)); - this.options = {}; - this.defaultOptions = { - select: true, - selectConnectedEdges: true - }; - util.extend(this.options, this.defaultOptions); + var EdgesHandler = _interopRequire(__webpack_require__(79)); - this.body.emitter.on("_dataChanged", function () { - _this.updateSelection(); - }); - } + var PhysicsEngine = _interopRequire(__webpack_require__(86)); - _prototypeProperties(SelectionHandler, null, { - setOptions: { - value: function setOptions(options) { - if (options !== undefined) { - util.deepExtend(this.options, options); - } - }, - writable: true, - configurable: true - }, - selectOnPoint: { + var ClusterEngine = _interopRequire(__webpack_require__(93)); + var CanvasRenderer = _interopRequire(__webpack_require__(95)); + var Canvas = _interopRequire(__webpack_require__(96)); - /** - * handles the selection part of the tap; - * - * @param {Object} pointer - * @private - */ - value: function selectOnPoint(pointer) { - var selected = false; - if (this.options.select === true) { - this.unselectAll(); - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - if (obj !== undefined) { - selected = this.selectObject(obj); - } - this.body.emitter.emit("_requestRedraw"); - } - return selected; - }, - writable: true, - configurable: true - }, - selectAdditionalOnPoint: { - value: function selectAdditionalOnPoint(pointer) { - var selectionChanged = false; - if (this.options.select === true) { - var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; + var View = _interopRequire(__webpack_require__(97)); - if (obj !== undefined) { - selectionChanged = true; - if (obj.isSelected() === true) { - this.deselectObject(obj); - } else { - this.selectObject(obj); - } + var InteractionHandler = _interopRequire(__webpack_require__(98)); - this.body.emitter.emit("_requestRedraw"); - } - } - return selectionChanged; - }, - writable: true, - configurable: true + var SelectionHandler = _interopRequire(__webpack_require__(101)); + + var LayoutEngine = _interopRequire(__webpack_require__(102)); + + var ManipulationSystem = _interopRequire(__webpack_require__(103)); + + var ConfigurationSystem = _interopRequire(__webpack_require__(105)); + + /** + * @constructor Network + * Create a network visualization, displaying nodes and edges. + * + * @param {Element} container The DOM element in which the Network will + * be created. Normally a div element. + * @param {Object} data An object containing parameters + * {Array} nodes + * {Array} edges + * @param {Object} options Options + */ + function Network(container, data, options) { + var _this = this; + if (!(this instanceof Network)) { + throw new SyntaxError("Constructor must be called with the new operator"); + } + + // set constant values + this.options = {}; + this.defaultOptions = { + clickToUse: false + }; + util.extend(this.options, this.defaultOptions); + + // containers for nodes and edges + this.body = { + nodes: {}, + nodeIndices: [], + edges: {}, + edgeIndices: [], + data: { + nodes: null, // A DataSet or DataView + edges: null // A DataSet or DataView }, - _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 + functions: { + createNode: function () {}, + createEdge: function () {}, + getPointer: function () {} }, - selectObject: { - value: function selectObject(obj) { - if (obj !== undefined) { - if (obj instanceof Node) { - if (this.options.selectConnectedEdges === true || this.forceSelectEdges === true) { - this._selectConnectedEdges(obj); - } - } - obj.select(); - this._addToSelection(obj); - return true; - } - return false; - }, - writable: true, - configurable: true + emitter: { + on: this.on.bind(this), + off: this.off.bind(this), + emit: this.emit.bind(this), + once: this.once.bind(this) }, - deselectObject: { - value: function deselectObject(obj) { - if (obj.isSelected() === true) { - obj.selected = false; - this._removeFromSelection(obj); - } - }, - writable: true, - configurable: true + eventListeners: { + onTap: function () {}, + onTouch: function () {}, + onDoubleTap: function () {}, + onHold: function () {}, + onDragStart: function () {}, + onDrag: function () {}, + onDragEnd: function () {}, + onMouseWheel: function () {}, + onPinch: function () {}, + onMouseMove: function () {}, + onRelease: function () {} }, - _getAllNodesOverlappingWith: { + container: container, + view: { + scale: 1, + translation: { x: 0, y: 0 } + } + }; + // bind the event listeners + this.bindEventListeners(); + // setting up all modules + var images = new Images(function () { + return _this.body.emitter.emit("_requestRedraw"); + }); // object with images + this.groups = new Groups(); // object with groups + this.canvas = new Canvas(this.body); // DOM handler + this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler + this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key + this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms + this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into + this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations + this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout + this.clustering = new ClusterEngine(this.body); // clustering api + this.manipulation = new ManipulationSystem(this.body, this.canvas, this.selectionHandler); // data manipulation system - /** - * retrieve all nodes overlapping with given object - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getAllNodesOverlappingWith(object) { - var overlappingNodes = []; - var nodes = this.body.nodes; - for (var i = 0; i < this.body.nodeIndices.length; i++) { - var nodeId = this.body.nodeIndices[i]; - if (nodes[nodeId].isOverlappingWith(object)) { - overlappingNodes.push(nodeId); - } - } - return overlappingNodes; - }, - writable: true, - configurable: true - }, - _pointerToPositionObject: { + this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options + this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options + this.configurationSystem = new ConfigurationSystem(this); - /** - * Return a position object in canvasspace from a single point in screenspace - * - * @param pointer - * @returns {{left: number, top: number, right: number, bottom: number}} - * @private - */ - value: function _pointerToPositionObject(pointer) { - var canvasPos = this.canvas.DOMtoCanvas(pointer); - return { - left: canvasPos.x - 1, - top: canvasPos.y + 1, - right: canvasPos.x + 1, - bottom: canvasPos.y - 1 - }; - }, - writable: true, - configurable: true - }, - getNodeAt: { + // create the DOM elements + this.canvas.create(); + // apply options + this.setOptions(options); - /** - * 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); + // load data (the disable start variable will be the same as the enabled clustering) + this.setData(data); + } - // 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: { + // Extend Network with an Emitter mixin + Emitter(Network.prototype); - /** - * retrieve all edges overlapping with given object, selector is around center - * @param {Object} object An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ - value: function _getEdgesOverlappingWith(object, overlappingEdges) { - var edges = this.body.edges; - for (var i = 0; i < this.body.edgeIndices.length; i++) { - var edgeId = this.body.edgeIndices[i]; - if (edges[edgeId].isOverlappingWith(object)) { - overlappingEdges.push(edgeId); - } - } - }, - writable: true, - configurable: true - }, - _getAllEdgesOverlappingWith: { + /** + * Set options + * @param {Object} options + */ + Network.prototype.setOptions = function (options) { + if (options !== undefined) { + // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. + options = this.layoutEngine.setOptions(options.layout, options); - /** - * 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: { + // pass the options to the modules + this.groups.setOptions(options.groups); + this.nodesHandler.setOptions(options.nodes); + this.edgesHandler.setOptions(options.edges); + this.physics.setOptions(options.physics); + this.canvas.setOptions(options.canvas); + this.renderer.setOptions(options.rendering); + this.view.setOptions(options.view); + this.interactionHandler.setOptions(options.interaction); + this.selectionHandler.setOptions(options.selection); + this.clustering.setOptions(options.clustering); + this.manipulation.setOptions(options.manipulation); - /** - * 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); + this.configurationSystem.setOptions(options); - if (overlappingEdges.length > 0) { - return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; - } else { - return undefined; + // handle network global options + if (options.clickToUse !== undefined) { + if (options.clickToUse === true) { + if (this.activator === undefined) { + this.activator = new Activator(this.frame); + this.activator.on("change", this._createKeyBinds.bind(this)); } - }, - writable: true, - configurable: true - }, - _addToSelection: { + } else { + if (this.activator !== undefined) { + this.activator.destroy(); + delete this.activator; + } + this.body.emitter.emit("activate"); + } + } else { + this.body.emitter.emit("activate"); + } + this.canvas.setSize(); + } + }; - /** - * 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: { - /** - * 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: { + /** + * Update the this.body.nodeIndices with the most recent node index list + * @private + */ + Network.prototype._updateVisibleIndices = function () { + var nodes = this.body.nodes; + var edges = this.body.edges; + this.body.nodeIndices = []; + this.body.edgeIndices = []; + + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].options.hidden === false) { + this.body.nodeIndices.push(nodeId); + } + } + } + + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].options.hidden === false) { + this.body.edgeIndices.push(edgeId); + } + } + } + }; + + Network.prototype.bindEventListeners = function () { + var _this = this; + // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed. + this.body.emitter.on("_dataChanged", function (params) { + var t0 = new Date().valueOf(); + // update shortcut lists + _this._updateVisibleIndices(); + _this.physics.updatePhysicsIndices(); + + // call the dataUpdated event because the only difference between the two is the updating of the indices + _this.body.emitter.emit("_dataUpdated"); + + console.log("_dataChanged took:", new Date().valueOf() - t0); + }); + + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on("_dataUpdated", function () { + var t0 = new Date().valueOf(); + // update values + _this._updateValueRange(_this.body.nodes); + _this._updateValueRange(_this.body.edges); + // start simulation (can be called safely, even if already running) + _this.body.emitter.emit("startSimulation"); + + console.log("_dataUpdated took:", new Date().valueOf() - t0); + }); + }; + + /** + * Set nodes and edges, and optionally options as well. + * + * @param {Object} data Object containing parameters: + * {Array | DataSet | DataView} [nodes] Array with nodes + * {Array | DataSet | DataView} [edges] Array with edges + * {String} [dot] String containing data in DOT format + * {String} [gephi] String containing data in gephi JSON format + * {Options} [options] Object with options + * @param {Boolean} [disableStart] | optional: disable the calling of the start function. + */ + Network.prototype.setData = function (data) { + // reset the physics engine. + this.body.emitter.emit("resetPhysics"); + this.body.emitter.emit("_resetData"); + + // unselect all to ensure no selections from old data are carried over. + this.selectionHandler.unselectAll(); + + if (data && data.dot && (data.nodes || data.edges)) { + throw new SyntaxError("Data must contain either parameter \"dot\" or " + " parameter pair \"nodes\" and \"edges\", but not both."); + } + + // set options + this.setOptions(data && data.options); + // set all data + if (data && data.dot) { + // parse DOT file + if (data && data.dot) { + var dotData = dotparser.DOTToGraph(data.dot); + this.setData(dotData); + return; + } + } else if (data && data.gephi) { + // parse DOT file + if (data && data.gephi) { + var gephiData = gephiParser.parseGephi(data.gephi); + this.setData(gephiData); + return; + } + } else { + this.nodesHandler.setData(data && data.nodes, true); + this.edgesHandler.setData(data && data.edges, true); + } + + // emit change in data + this.body.emitter.emit("_dataChanged"); + + // find a stable position or start animating to a stable position + this.body.emitter.emit("initPhysics"); + }; + + + /** + * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. + * var network = new vis.Network(..); + * network.destroy(); + * network = null; + */ + Network.prototype.destroy = function () { + this.body.emitter.emit("destroy"); + // clear events + this.body.emitter.off(); - /** - * 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: { + // remove the container and everything inside it recursively + util.recursiveDOMDelete(this.body.container); + }; - /** - * Unselect all. The selectionObj is useful for this. - * - * @private - */ - value: function unselectAll() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - this.selectionObj.nodes[nodeId].unselect(); - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - this.selectionObj.edges[edgeId].unselect(); - } - } - this.selectionObj = { nodes: {}, edges: {} }; - }, - writable: true, - configurable: true - }, - _getSelectedNodeCount: { + /** + * 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; - /** - * 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: { + // 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; + } + } + } - /** - * 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: { + // 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); + } + } + } + }; - /** - * 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: { + /** + * Scale the network + * @param {Number} scale Scaling factor 1.0 is unscaled + * @private + */ + Network.prototype._setScale = function (scale) { + this.body.view.scale = scale; + }; - /** - * 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: { + /** + * Get the current scale of the network + * @return {Number} scale Scaling factor 1.0 is unscaled + * @private + */ + Network.prototype._getScale = function () { + return this.body.view.scale; + }; - /** - * 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: { + /** + * Load the XY positions of the nodes into the dataset. + */ + Network.prototype.storePositions = function () { + // todo: incorporate fixed instead of allowedtomove, add support for clusters and hierarchical. + var dataArray = []; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + var allowedToMoveX = !this.body.nodes.xFixed; + var allowedToMoveY = !this.body.nodes.yFixed; + if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { + dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y), allowedToMoveX: allowedToMoveX, allowedToMoveY: allowedToMoveY }); + } + } + } + this.body.data.nodes.update(dataArray); + }; - /** - * 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 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) }; } - return true; - }, - writable: true, - configurable: true - }, - _clusterInSelection: { - + } + } 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; + }; - /** - * 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: { - /** - * 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: { + /** + * Returns true when the Network is active. + * @returns {boolean} + */ + Network.prototype.isActive = function () { + return !this.activator || this.activator.active; + }; - /** - * 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: { + /** + * Sets the scale + * @returns {Number} + */ + Network.prototype.setScale = function () { + return this._setScale(); + }; - /** - * 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: { + /** + * Returns the scale + * @returns {Number} + */ + Network.prototype.getScale = function () { + return this._getScale(); + }; + /** + * 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; + } + }; + /** + * 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 }); + }; - /** - * 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: { + Network.prototype.getBoundingBox = function (nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].boundingBox; + } + }; - /** - * 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 }); - } + Network.prototype.getConnectedNodes = function (nodeId) { + var nodeList = []; + if (this.body.nodes[nodeId] !== undefined) { + var node = this.body.nodes[nodeId]; + var nodeObj = { nodeId: true }; // used to quickly check if node already exists + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + if (edge.toId == nodeId) { + if (nodeObj[edge.fromId] === undefined) { + nodeList.push(edge.fromId); + nodeObj[edge.fromId] = true; } - if (object instanceof Node) { - this._hoverConnectedEdges(object); + } else if (edge.fromId == nodeId) { + if (nodeObj[edge.toId] === undefined) { + nodeList.push(edge.toId); + nodeObj[edge.toId] = true; } - }, - writable: true, - configurable: true - }, - getSelection: { + } + } + } + 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); + }; - /** - * - * 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: { + module.exports = Network; - /** - * - * 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: { +/***/ }, +/* 54 */ +/***/ function(module, exports, __webpack_require__) { - /** - * - * 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: { + "use strict"; + /** + * Canvas shapes used by Network + */ + if (typeof CanvasRenderingContext2D !== "undefined") { + /** + * Draw a circle shape + */ + CanvasRenderingContext2D.prototype.circle = function (x, y, r) { + this.beginPath(); + this.arc(x, y, r, 0, 2 * Math.PI, false); + }; - /** - * 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; + /** + * 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); + }; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + /** + * 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(); - // first unselect any selected node - this.unselectAll(true); + // the change in radius and the offset is here to center the shape + r *= 1.15; + y += 0.275 * r; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height - var 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: { + 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(); - /** - * 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; + }; - if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; + /** + * 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(); - // first unselect any selected node - this.unselectAll(true); + // the change in radius and the offset is here to center the shape + r *= 1.15; + y -= 0.275 * r; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + var s = r * 2; + var s2 = s / 2; + var ir = Math.sqrt(3) / 6 * s; // radius of inner circle + var h = Math.sqrt(s * s - s2 * s2); // height - var 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: { + 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(); + }; - /** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ - value: function updateSelection() { - for (var nodeId in this.selectionObj.nodes) { - if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { - if (!this.body.nodes.hasOwnProperty(nodeId)) { - delete this.selectionObj.nodes[nodeId]; - } - } - } - for (var edgeId in this.selectionObj.edges) { - if (this.selectionObj.edges.hasOwnProperty(edgeId)) { - if (!this.body.edges.hasOwnProperty(edgeId)) { - delete this.selectionObj.edges[edgeId]; - } - } - } - }, - writable: true, - configurable: true + /** + * 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(); + + // the change in radius and the offset is here to center the shape + r *= 0.82; + y += 0.1 * r; + + for (var n = 0; n < 10; n++) { + var radius = n % 2 === 0 ? r * 1.3 : r * 0.5; + this.lineTo(x + radius * Math.sin(n * 2 * Math.PI / 10), y - radius * Math.cos(n * 2 * Math.PI / 10)); } - }); - return SelectionHandler; - })(); + this.closePath(); + }; - module.exports = SelectionHandler; + /** + * Draw a Diamond shape + * @param {Number} x horizontal center + * @param {Number} y vertical center + * @param {Number} r radius, half the length of the sides of the triangle + */ + CanvasRenderingContext2D.prototype.diamond = function (x, y, r) { + // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/ + this.beginPath(); -/***/ }, -/* 60 */ -/***/ function(module, exports, __webpack_require__) { + this.lineTo(x, y + r); + this.lineTo(x + r, y); + this.lineTo(x, y - r); + this.lineTo(x - r, y); - "use strict"; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + this.closePath(); + }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * 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); + }; - /** - * Created by Alex on 3/3/2015. - */ + /** + * 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 - var util = __webpack_require__(1); + 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); + }; - var LayoutEngine = (function () { - function LayoutEngine(body) { - var _this = this; - _classCallCheck(this, LayoutEngine); - this.body = body; + /** + * 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.initialRandomSeed = Math.round(Math.random() * 1000000); - this.randomSeed = this.initialRandomSeed; - this.options = {}; - this.defaultOptions = { - randomSeed: undefined, - hierarchical: { - enabled: false, - levelSeparation: 150, - direction: "UD", // UD, DU, LR, RL - sortMethod: "hubsize" // hubsize, directed - } - }; - util.extend(this.options, this.defaultOptions); + var kappa = 0.5522848, + ox = wEllipse / 2 * kappa, + // control point offset horizontal + oy = hEllipse / 2 * kappa, + // control point offset vertical + xe = x + wEllipse, + // x-end + ye = y + hEllipse, + // y-end + xm = x + wEllipse / 2, + // x-middle + ym = y + hEllipse / 2, + // y-middle + ymb = y + (h - hEllipse / 2), + // y-midlle, bottom ellipse + yeb = y + h; // y-end, bottom ellipse - this.hierarchicalLevels = {}; + this.beginPath(); + this.moveTo(xe, ym); - this.body.emitter.on("_dataChanged", function () { - _this.setupHierarchicalLayout(); - }); - } + this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); - _prototypeProperties(LayoutEngine, null, { - setOptions: { - value: function setOptions(options, allOptions) { - if (options !== undefined) { - util.mergeOptions(this.options, options, "hierarchical"); - if (options.randomSeed !== undefined) { - this.randomSeed = options.randomSeed; - } + this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); - if (this.options.hierarchical.enabled === true) { - // make sure the level seperation is the right way up - if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "DU") { - if (this.options.hierarchical.levelSeparation > 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } else { - if (this.options.hierarchical.levelSeparation < 0) { - this.options.hierarchical.levelSeparation *= -1; - } - } + this.lineTo(xe, ymb); - // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. - return this.adaptAllOptions(allOptions); - } - } - return allOptions; - }, - writable: true, - configurable: true - }, - adaptAllOptions: { - value: function adaptAllOptions(allOptions) { - if (this.options.hierarchical.enabled === true) { - // set the physics - if (allOptions.physics === undefined || allOptions.physics === true) { - allOptions.physics = { solver: "hierarchicalRepulsion" }; - } else if (options.physics !== false) { - allOptions.physics.solver = "hierarchicalRepulsion"; - } + this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb); + this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb); - // get the type of static smooth curve in case it is required - var type = "horizontal"; - if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "LR") { - type = "vertical"; - } + this.lineTo(x, ym); + }; - // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. - if (allOptions.edges === undefined) { - allOptions.edges = { smooth: false }; - } else if (allOptions.edges.smooth === undefined) { - allOptions.edges.smooth = false; - } else { - allOptions.edges.smooth = { enabled: true, dynamic: false, type: type }; - } - // force all edges into static smooth curves. - this.body.emitter.emit("_forceDisableDynamicCurves", type); - } - return allOptions; - }, - writable: true, - configurable: true - }, - seededRandom: { - value: function seededRandom() { - var x = Math.sin(this.randomSeed++) * 10000; - return x - Math.floor(x); - }, - writable: true, - configurable: true - }, - positionInitially: { - value: function positionInitially(nodesArray) { - if (this.options.hierarchical.enabled !== true) { - for (var i = 0; i < nodesArray.length; i++) { - var node = nodesArray[i]; - if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { - var radius = 10 * 0.1 * nodesArray.length + 10; - var angle = 2 * Math.PI * this.seededRandom(); + /** + * 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 (node.options.fixed.x == false) { - node.x = radius * Math.cos(angle); - } - if (node.options.fixed.x == false) { - node.y = radius * Math.sin(angle); - } - } - } - } - }, - writable: true, - configurable: true - }, - getSeed: { - value: function getSeed() { - return this.initialRandomSeed; - }, - writable: true, - configurable: true - }, - setupHierarchicalLayout: { + // 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); - /** - * This is the main function to layout the nodes in a hierarchical way. - * It checks if the node details are supplied correctly - * - * @private - */ - value: function setupHierarchicalLayout() { - if (this.options.hierarchical.enabled == true && this.body.nodeIndices.length > 0) { - // get the size of the largest hubs and check if the user has defined a level for a node. - var node = undefined, - nodeId = undefined; - var definedLevel = false; - var undefinedLevel = false; - this.hierarchicalLevels = {}; - this.nodeSpacing = 100; + // 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); - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.options.level !== undefined) { - definedLevel = true; - this.hierarchicalLevels[nodeId] = node.options.level; - } else { - undefinedLevel = true; - } - } - } + // right + var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI); + var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI); - // if the user defined some levels but not all, alert and run without hierarchical layout - if (undefinedLevel == true && definedLevel == true) { - throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); - return; - } else { - // setup the system to use hierarchical method. - //this._changeConstants(); + this.beginPath(); + this.moveTo(x, y); + this.lineTo(xl, yl); + this.lineTo(xi, yi); + this.lineTo(xr, yr); + this.closePath(); + }; - // define levels if undefined by the users. Based on hubsize - if (undefinedLevel == true) { - if (this.options.hierarchical.sortMethod == "hubsize") { - this._determineLevelsByHubsize(); - } else if (this.options.hierarchical.sortMethod == "directed" || "direction") { - this._determineLevelsDirected(); - } - } - // check the distribution of the nodes per level. - var distribution = this._getDistribution(); + /** + * 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; + } + }; - // place the nodes on the canvas. - this._placeNodesByHierarchy(distribution); - } - } - }, - writable: true, - configurable: true - }, - _placeNodesByHierarchy: { + // TODO: add diamond shape + } - /** - * This function places the nodes on the canvas based on the hierarchial distribution. - * - * @param {Object} distribution | obtained by the function this._getDistribution() - * @private - */ - value: function _placeNodesByHierarchy(distribution) { - var nodeId = undefined, - node = undefined; - this.positionedNodes = {}; +/***/ }, +/* 55 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + /** + * 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(); + } - // start placing all the level 0 nodes first. Then recursively position their branches. - for (var level in distribution) { - if (distribution.hasOwnProperty(level)) { - for (nodeId in distribution[level].nodes) { - if (distribution[level].nodes.hasOwnProperty(nodeId)) { - node = distribution[level].nodes[nodeId]; - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - if (node.x === undefined) { - node.x = distribution[level].distance; - } - distribution[level].distance = node.x + this.nodeSpacing; - } else { - if (node.y === undefined) { - node.y = distribution[level].distance; - } - distribution[level].distance = node.y + this.nodeSpacing; - } + // token types enumeration + var TOKENTYPE = { + NULL: 0, + DELIMITER: 1, + IDENTIFIER: 2, + UNKNOWN: 3 + }; - this.positionedNodes[nodeId] = true; - this._placeBranchNodes(node.edges, node.id, distribution, level); - } - } - } - } - }, - writable: true, - configurable: true - }, - _getDistribution: { + // map with all delimiters + var DELIMITERS = { + "{": true, + "}": true, + "[": true, + "]": true, + ";": true, + "=": true, + ",": true, + "->": true, + "--": true + }; - /** - * This function get the distribution of levels based on hubsize - * - * @returns {Object} - * @private - */ - value: function _getDistribution() { - var distribution = {}; - var nodeId = undefined, - node = undefined; + 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 - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. - // the fix of X is removed after the x value has been set. - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - node.y = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; - node.options.fixed.y = true; - } else { - node.x = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; - node.options.fixed.x = true; - } - if (distribution[this.hierarchicalLevels[nodeId]] === undefined) { - distribution[this.hierarchicalLevels[nodeId]] = { amount: 0, nodes: {}, distance: 0 }; - } - distribution[this.hierarchicalLevels[nodeId]].amount += 1; - distribution[this.hierarchicalLevels[nodeId]].nodes[nodeId] = node; - } - } - return distribution; - }, - writable: true, - configurable: true - }, - _getHubSize: { + /** + * Get the 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); + } + /** + * 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); + } - /** - * Get the hubsize from all remaining unlevelled nodes. - * - * @returns {number} - * @private - */ - value: function _getHubSize() { - var hubSize = 0; - for (var nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - var node = this.body.nodes[nodeId]; - if (this.hierarchicalLevels[nodeId] === undefined) { - hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; - } - } - } - return hubSize; - }, - writable: true, - configurable: true - }, - _determineLevelsByHubsize: { + /** + * Preview the next character from the dot file. + * @return {String} cNext + */ + function nextPreview() { + return dot.charAt(index + 1); + } + /** + * Test whether given character is alphabetic or numeric + * @param {String} c + * @return {Boolean} isAlphaNumeric + */ + var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; + function isAlphaNumeric(c) { + return regexAlphaNumeric.test(c); + } - /** - * this function allocates nodes in levels based on the recursive branching from the largest hubs. - * - * @param hubsize - * @private - */ - value: function _determineLevelsByHubsize() { - var nodeId = undefined, - node = undefined; - var hubSize = 1; + /** + * Merge all options of object b into object b + * @param {Object} a + * @param {Object} b + * @return {Object} a + */ + function merge(a, b) { + if (!a) { + a = {}; + } - while (hubSize > 0) { - // determine hubs - hubSize = this._getHubSize(); - if (hubSize == 0) break; + if (b) { + for (var name in b) { + if (b.hasOwnProperty(name)) { + a[name] = b[name]; + } + } + } + return a; + } - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - if (node.edges.length == hubSize) { - this._setLevel(0, node); - } - } - } - } - }, - writable: true, - configurable: true - }, - _setLevel: { + /** + * 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; + } + } + } + /** + * 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; - /** - * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. - * - * @param level - * @param edges - * @param parentId - * @private - */ - value: function _setLevel(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) { - return; - }var childNode = undefined; - this.hierarchicalLevels[node.id] = level; - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId == node.id) { - childNode = node.edges[i].from; - } else { - childNode = node.edges[i].to; - } - this._setLevel(level + 1, childNode); - } - }, - writable: true, - configurable: true - }, - _determineLevelsDirected: { + // find root graph (in case of subgraph) + var graphs = [graph]; // list with all graphs from current graph to root graph + var root = graph; + while (root.parent) { + graphs.push(root.parent); + root = root.parent; + } + // find existing node (at root level) by its id + if (root.nodes) { + for (i = 0, len = root.nodes.length; i < len; i++) { + if (node.id === root.nodes[i].id) { + current = root.nodes[i]; + break; + } + } + } + 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 function allocates nodes in levels based on the direction of the edges - * - * @param hubsize - * @private - */ - value: function _determineLevelsDirected() { - var nodeId = undefined, - node = undefined; - var minLevel = 10000; + // add node to this (sub)graph and all its parent graphs + for (i = graphs.length - 1; i >= 0; i--) { + var g = graphs[i]; - // set first node to source - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - node = this.body.nodes[nodeId]; - this._setLevelDirected(minLevel, node); - } - } + if (!g.nodes) { + g.nodes = []; + } + if (g.nodes.indexOf(current) == -1) { + g.nodes.push(current); + } + } - // get the minimum level - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; - } - } + // merge attributes + if (node.attr) { + current.attr = merge(current.attr, node.attr); + } + } - // subtract the minimum from the set so we have a range starting from 0 - for (nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - this.hierarchicalLevels[nodeId] -= minLevel; - } - } - }, - writable: true, - configurable: true - }, - _setLevelDirected: { + /** + * 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 + } + } + /** + * 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 + }; - /** - * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction - * - * @param level - * @param edges - * @param parentId - * @private - */ - value: function _setLevelDirected(level, node) { - if (this.hierarchicalLevels[node.id] !== undefined) { - return; - }var childNode = undefined; - this.hierarchicalLevels[node.id] = level; + if (graph.edge) { + edge.attr = merge({}, graph.edge); // clone default attributes + } + edge.attr = merge(edge.attr || {}, attr); // merge attributes - for (var i = 0; i < node.edges.length; i++) { - if (node.edges[i].toId == node.id) { - childNode = node.edges[i].from; - this._setLevelDirected(level - 1, childNode); - } else { - childNode = node.edges[i].to; - this._setLevelDirected(level + 1, childNode); - } - } - }, - writable: true, - configurable: true - }, - _placeBranchNodes: { + return edge; + } + /** + * Get next token in the current dot file. + * The token and token type are available as token and tokenType + */ + function getToken() { + tokenType = TOKENTYPE.NULL; + token = ""; + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); + } - /** - * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes - * on a X position that ensures there will be no overlap. - * - * @param edges - * @param parentId - * @param distribution - * @param parentLevel - * @private - */ - value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { - for (var i = 0; i < edges.length; i++) { - var childNode = undefined; - var parentNode = undefined; - if (edges[i].toId == parentId) { - childNode = edges[i].from; - parentNode = edges[i].to; - } else { - childNode = edges[i].to; - parentNode = edges[i].from; - } - var childNodeLevel = this.hierarchicalLevels[childNode.id]; - if (this.positionedNodes[childNode.id] === undefined) { - // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. - if (childNodeLevel > parentLevel) { - if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { - if (childNode.x === undefined) { - childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); - } - distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; - this.positionedNodes[childNode.id] = true; - } else { - if (childNode.y === undefined) { - childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); - } - distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; - } - this.positionedNodes[childNode.id] = true; + do { + var isComment = false; - if (childNode.edges.length > 1) { - this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); - } - } - } + // 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(); } - }, - writable: true, - configurable: true + 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; } - }); - return LayoutEngine; - })(); + // skip over whitespaces + while (c == " " || c == "\t" || c == "\n" || c == "\r") { + // space, tab, enter + next(); + } + } while (isComment); - module.exports = LayoutEngine; + // check for end of dot file + if (c == "") { + // token is still empty + tokenType = TOKENTYPE.DELIMITER; + return; + } + + // check for delimiters consisting of 2 characters + var c2 = c + nextPreview(); + if (DELIMITERS[c2]) { + tokenType = TOKENTYPE.DELIMITER; + token = c2; + next(); + next(); + return; + } -/***/ }, -/* 61 */ -/***/ function(module, exports, __webpack_require__) { + // check for delimiters consisting of 1 character + if (DELIMITERS[c]) { + tokenType = TOKENTYPE.DELIMITER; + token = c; + next(); + return; + } - "use strict"; + // 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(); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + 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; + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // check for a string enclosed by double quotes + if (c == "\"") { + next(); + while (c != "" && (c != "\"" || c == "\"" && nextPreview() == "\"")) { + token += c; + if (c == "\"") { + // skip the escape character + next(); + } + next(); + } + if (c != "\"") { + throw newSyntaxError("End of string \" expected"); + } + next(); + tokenType = TOKENTYPE.IDENTIFIER; + return; + } - var util = __webpack_require__(1); - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(43); - var locales = __webpack_require__(77); + // 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) + "\""); + } /** - * clears the toolbar div element of children - * - * @private + * Parse a graph. + * @returns {Object} graph */ - var ManipulationSystem = (function () { - function ManipulationSystem(body, canvas, selectionHandler) { - _classCallCheck(this, ManipulationSystem); - - this.body = body; - this.canvas = canvas; - this.selectionHandler = selectionHandler; + function parseGraph() { + var graph = {}; - this.editMode = false; - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + first(); + getToken(); - this.manipulationHammers = []; - this.temporaryUIFunctions = {}; - this.temporaryEventFunctions = []; + // optional strict keyword + if (token == "strict") { + graph.strict = true; + getToken(); + } - this.touchTime = 0; - this.temporaryIds = { nodes: [], edges: [] }; - this.guiEnabled = false; - this.selectedControlNode = undefined; + // graph or digraph keyword + if (token == "graph" || token == "digraph") { + graph.type = token; + getToken(); + } - this.options = {}; - this.defaultOptions = { - enabled: false, - initiallyVisible: false, - locale: "en", - locales: locales, - functionality: { - addNode: true, - addEdge: true, - editNode: true, - editEdge: true, - deleteNode: true, - deleteEdge: true - }, - handlerFunctions: { - addNode: undefined, - addEdge: undefined, - editNode: undefined, - editEdge: undefined, - deleteNode: undefined, - deleteEdge: undefined - }, - controlNodeStyle: { - shape: "dot", - size: 6, - color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } }, - borderWidth: 2, - borderWidthSelected: 2 - } - }; - util.extend(this.options, this.defaultOptions); + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + graph.id = token; + getToken(); } - _prototypeProperties(ManipulationSystem, null, { - setOptions: { + // open angle bracket + if (token != "{") { + throw newSyntaxError("Angle bracket { expected"); + } + getToken(); + // statements + parseStatements(graph); - /** - * Set the Options - * @param options - */ - value: function setOptions(options) { - if (options !== undefined) { - if (typeof options == "boolean") { - this.options.enabled = options; - } else { - this.options.enabled = true; - for (var prop in options) { - if (options.hasOwnProperty(prop)) { - this.options[prop] = options[prop]; - } - } - } - if (this.options.initiallyVisible === true) { - this.editMode = true; - } - this._setup(); - } - }, - writable: true, - configurable: true - }, - toggleEditMode: { + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); + } + getToken(); + // end of file + if (token !== "") { + throw newSyntaxError("End of file expected"); + } + getToken(); - /** - * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. - * - * @private - */ - value: function toggleEditMode() { - this.editMode = !this.editMode; - if (this.guiEnabled === true) { - var toolbar = this.manipulationDiv; - var closeDiv = this.closeDiv; - var editModeDiv = this.editModeDiv; - if (this.editMode === true) { - toolbar.style.display = "block"; - closeDiv.style.display = "block"; - editModeDiv.style.display = "none"; - this._bindHammerToDiv(closeDiv, this.toggleEditMode.bind(this)); - this.showManipulatorToolbar(); - } else { - toolbar.style.display = "none"; - closeDiv.style.display = "none"; - editModeDiv.style.display = "block"; - this._createEditButton(); - } - } - }, - writable: true, - configurable: true - }, - showManipulatorToolbar: { + // remove temporary default options + delete graph.node; + delete graph.edge; + delete graph.graph; + return graph; + } - /** - * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. - * - * @private - */ - value: function showManipulatorToolbar() { - // restore the state of any bound functions or events, remove control nodes, restore physics - this._clean(); + /** + * Parse a list with statements. + * @param {Object} graph + */ + function parseStatements(graph) { + while (token !== "" && token != "}") { + parseStatement(graph); + if (token == ";") { + getToken(); + } + } + } - // reset global letiables - this.manipulationDOM = {}; + /** + * 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); - var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); - var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); - var selectedTotalCount = selectedNodeCount + selectedEdgeCount; - var locale = this.options.locales[this.options.locale]; - var needSeperator = false; + return; + } - if (this.options.functionality.addNode === true) { - this._createAddNodeButton(locale); - needSeperator = true; - } - if (this.options.functionality.addEdge === true) { - if (needSeperator === true) { - this._createSeperator(1); - } else { - needSeperator = true; - } - this._createAddEdgeButton(locale); - } + // parse an attribute statement + var attr = parseAttributeStatement(graph); + if (attr) { + return; + } - if (selectedNodeCount === 1 && typeof this.options.handlerFunctions.editNode === "function" && this.options.functionality.editNode === true) { - if (needSeperator === true) { - this._createSeperator(2); - } else { - needSeperator = true; - } - this._createEditNodeButton(locale); - } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.functionality.editEdge === true) { - if (needSeperator === true) { - this._createSeperator(3); - } else { - needSeperator = true; - } - this._createEditEdgeButton(locale); - } + // parse node + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Identifier expected"); + } + var id = token; // id can be a string or a number + getToken(); - // remove buttons - if (selectedTotalCount !== 0) { - if (selectedNodeCount === 1 && this.options.functionality.deleteNode === true) { - if (needSeperator === true) { - this._createSeperator(4); - } - this._createDeleteButton(locale); - } else if (selectedNodeCount === 0 && this.options.functionality.deleteEdge === true) { - if (needSeperator === true) { - this._createSeperator(4); - } - this._createDeleteButton(locale); - } - } + 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); + } + } - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + /** + * Parse a subgraph + * @param {Object} graph parent graph object + * @return {Object | null} subgraph + */ + function parseSubgraph(graph) { + var subgraph = null; - // refresh this bar based on what has been selected - this._temporaryBindEvent("select", this.showManipulatorToolbar.bind(this)); + // optional subgraph keyword + if (token == "subgraph") { + subgraph = {}; + subgraph.type = "subgraph"; + getToken(); - // redraw to show any possible changes - this.body.emitter.emit("_redraw"); - }, - writable: true, - configurable: true - }, - addNodeMode: { + // optional graph id + if (tokenType == TOKENTYPE.IDENTIFIER) { + subgraph.id = token; + getToken(); + } + } + // open angle bracket + if (token == "{") { + getToken(); - /** - * Create the toolbar for adding Nodes - * - * @private - */ - value: function addNodeMode() { - // clear the toolbar - this._clean(); + if (!subgraph) { + subgraph = {}; + } + subgraph.parent = graph; + subgraph.node = graph.node; + subgraph.edge = graph.edge; + subgraph.graph = graph.graph; - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale.addDescription); + // statements + parseStatements(subgraph); - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + // close angle bracket + if (token != "}") { + throw newSyntaxError("Angle bracket } expected"); + } + getToken(); - this._temporaryBindEvent("click", this._performAddNode.bind(this)); - }, - writable: true, - configurable: true - }, - editNode: { + // remove temporary default options + delete subgraph.node; + delete subgraph.edge; + delete subgraph.graph; + delete subgraph.parent; - /** - * call the bound function to handle the editing of the node. The node has to be selected. - * - * @private - */ - value: function editNode() { - var _this = this; - if (typeof this.options.handlerFunctions.editNode === "function") { - var node = this.selectionHandler._getSelectedNode(); - if (node.isCluster !== true) { - var data = util.deepExtend({}, node.options, true); - data.x = node.x; - data.y = node.y; + // register at the parent graph + if (!graph.subgraphs) { + graph.subgraphs = []; + } + graph.subgraphs.push(subgraph); + } - if (this.options.handlerFunctions.editNode.length == 2) { - this.options.handlerFunctions.editNode(data, function (finalizedData) { - _this.body.data.nodes.update(finalizedData); - _this.showManipulatorToolbar(); - }); - } else { - throw new Error("The function for edit does not support two arguments (data, callback)"); - } - } else { - alert(this.options.locales[this.options.locale].editClusterError); - } - } else { - throw new Error("No function has been configured to handle the editing of nodes."); - } - }, - writable: true, - configurable: true - }, - addEdgeMode: { + return subgraph; + } + /** + * 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(); - /** - * create the toolbar to connect nodes - * - * @private - */ - value: function addEdgeMode() { - // _clean the system - this._clean(); + // node attributes + graph.node = parseAttributeList(); + return "node"; + } else if (token == "edge") { + getToken(); - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale.edgeDescription); + // edge attributes + graph.edge = parseAttributeList(); + return "edge"; + } else if (token == "graph") { + getToken(); - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + // graph attributes + graph.graph = parseAttributeList(); + return "graph"; + } - // temporarily overload functions - this._temporaryBindUI("onTouch", this._handleConnect.bind(this)); - this._temporaryBindUI("onDragEnd", this._finishConnect.bind(this)); - this._temporaryBindUI("onHold", function () {}); - }, - writable: true, - configurable: true - }, - editEdgeMode: { + return null; + } - /** - * create the toolbar to edit edges - * - * @private - */ - value: function editEdgeMode() { - // clear the system - this._clean(); + /** + * 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); - if (this.guiEnabled === true) { - var locale = this.options.locales[this.options.locale]; - this.manipulationDOM = {}; - this._createBackButton(locale); - this._createSeperator(); - this._createDescription(locale.editEdgeDescription); + // edge statements + parseEdge(graph, id); + } - // bind the close button - this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - } + /** + * 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(); - this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; - var edge = this.body.edges[this.edgeBeingEditedId]; + 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(); + } - // create control nodes - var controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); - var controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); + // parse edge attributes + var attr = parseAttributeList(); - this.temporaryIds.nodes.push(controlNodeFrom.id); - this.temporaryIds.nodes.push(controlNodeTo.id); + // create edge + var edge = createEdge(graph, from, to, type, attr); + addEdge(graph, edge); - this.body.nodes[controlNodeFrom.id] = controlNodeFrom; - this.body.nodeIndices.push(controlNodeFrom.id); - this.body.nodes[controlNodeTo.id] = controlNodeTo; - this.body.nodeIndices.push(controlNodeTo.id); + from = to; + } + } - // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI - this._temporaryBindUI("onTouch", this._controlNodeTouch.bind(this)); // used to get the position - this._temporaryBindUI("onTap", function () {}); // disabled - this._temporaryBindUI("onHold", function () {}); // disabled - this._temporaryBindUI("onDragStart", this._controlNodeDragStart.bind(this)); // used to select control node - this._temporaryBindUI("onDrag", this._controlNodeDrag.bind(this)); // used to drag control node - this._temporaryBindUI("onDragEnd", this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes - this._temporaryBindUI("onMouseMove", function () {}); // disabled + /** + * Parse a set with attributes, + * for example [label="1.000", shape=solid] + * @return {Object | null} attr + */ + function parseAttributeList() { + var attr = null; - // create function to position control nodes correctly on movement - // automatically cleaned up because we use the temporary bind - this._temporaryBindEvent("beforeDrawing", function (ctx) { - var positions = edge.edgeType.findBorderPositions(ctx); - if (controlNodeFrom.selected === false) { - controlNodeFrom.x = positions.from.x; - controlNodeFrom.y = positions.from.y; - } - if (controlNodeTo.selected === false) { - controlNodeTo.x = positions.to.x; - controlNodeTo.y = positions.to.y; - } - }); + while (token == "[") { + getToken(); + attr = {}; + while (token !== "" && token != "]") { + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute name expected"); + } + var name = token; - this.body.emitter.emit("_redraw"); - }, - writable: true, - configurable: true - }, - deleteSelected: { + getToken(); + if (token != "=") { + throw newSyntaxError("Equal sign = expected"); + } + getToken(); - /** - * delete everything in the selection - * - * @private - */ - value: function deleteSelected() { - var _this = this; - var selectedNodes = this.selectionHandler.getSelectedNodes(); - var selectedEdges = this.selectionHandler.getSelectedEdges(); - var deleteFunction = undefined; - if (selectedNodes.length > 0) { - for (var i = 0; i < selectedNodes.length; i++) { - if (this.body.nodes[selectedNodes[i]].isCluster === true) { - alert(this.options.locales[this.options.locale].deleteClusterError); - return; - } - } + if (tokenType != TOKENTYPE.IDENTIFIER) { + throw newSyntaxError("Attribute value expected"); + } + var value = token; + setValue(attr, name, value); // name can be a path - if (typeof this.options.handlerFunctions.deleteNode === "function") { - deleteFunction = this.options.handlerFunctions.deleteNode; - } - } else if (selectedEdges.length > 0) { - if (typeof this.options.handlerFunctions.deleteEdge === "function") { - deleteFunction = this.options.handlerFunctions.deleteEdge; - } - } + getToken(); + if (token == ",") { + getToken(); + } + } - if (typeof deleteFunction === "function") { - var data = { nodes: selectedNodes, edges: selectedEdges }; - if (deleteFunction.length == 2) { - deleteFunction(data, function (finalizedData) { - _this.body.data.edges.remove(finalizedData.edges); - _this.body.data.nodes.remove(finalizedData.nodes); - _this.body.emitter.emit("startSimulation"); - }); - } else { - throw new Error("The function for delete does not support two arguments (data, callback)"); - } - } else { - this.body.data.edges.remove(selectedEdges); - this.body.data.nodes.remove(selectedNodes); - this.body.emitter.emit("startSimulation"); - } - }, - writable: true, - configurable: true - }, - _setup: { + if (token != "]") { + throw newSyntaxError("Bracket ] expected"); + } + getToken(); + } + return attr; + } + /** + * 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 + ")"); + } + /** + * 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) + "..."; + } - //********************************************** PRIVATE ***************************************// + /** + * 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); + } + } + } - /** - * draw or remove the DOM - * @private - */ - value: function _setup() { - if (this.options.enabled === true) { - // Enable the GUI - this.guiEnabled = true; + /** + * 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: {} + }; - // remove override - this.selectionHandler.forceSelectEdges = true; + // 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._createWrappers(); - if (this.editMode === false) { - this._createEditButton(); - } else { - this.showManipulatorToolbar(); - } - } else { - this._removeManipulationDOM(); + // 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; + }; - // disable the gui - this.guiEnabled = false; - } - }, - writable: true, - configurable: true - }, - _createWrappers: { + dotData.edges.forEach(function (dotEdge) { + var from, to; + if (dotEdge.from instanceof Object) { + from = dotEdge.from.nodes; + } else { + from = { + id: dotEdge.from + }; + } + if (dotEdge.to instanceof Object) { + to = dotEdge.to.nodes; + } else { + to = { + id: dotEdge.to + }; + } - /** - * create the div overlays that contain the DOM - * @private - */ - value: function _createWrappers() { - // load the manipulator HTML elements. All styling done in css. - if (this.manipulationDiv === undefined) { - this.manipulationDiv = document.createElement("div"); - this.manipulationDiv.className = "network-manipulationDiv"; - if (this.editMode === true) { - this.manipulationDiv.style.display = "block"; - } else { - this.manipulationDiv.style.display = "none"; - } - this.canvas.frame.appendChild(this.manipulationDiv); - } + if (dotEdge.from instanceof Object && dotEdge.from.edges) { + dotEdge.from.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } - // container for the edit button. - if (this.editModeDiv === undefined) { - this.editModeDiv = document.createElement("div"); - this.editModeDiv.className = "network-manipulation-editMode"; - if (this.editMode === true) { - this.editModeDiv.style.display = "none"; - } else { - this.editModeDiv.style.display = "block"; - } - this.canvas.frame.appendChild(this.editModeDiv); - } + 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); + }); + if (dotEdge.to instanceof Object && dotEdge.to.edges) { + dotEdge.to.edges.forEach(function (subEdge) { + var graphEdge = convertEdge(subEdge); + graphData.edges.push(graphEdge); + }); + } + }); + } - // container for the close div button - if (this.closeDiv === undefined) { - this.closeDiv = document.createElement("div"); - this.closeDiv.className = "network-manipulation-closeDiv"; - this.closeDiv.style.display = this.manipulationDiv.style.display; - this.canvas.frame.appendChild(this.closeDiv); - } - }, - writable: true, - configurable: true - }, - _getNewTargetNode: { + // copy the options + if (dotData.attr) { + graphData.options = dotData.attr; + } + return graphData; + } - /** - * generate a new target node. Used for creating new edges and editing edges - * @param x - * @param y - * @returns {*} - * @private - */ - value: function _getNewTargetNode(x, y) { - var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); + // exports + exports.parseDOT = parseDOT; + exports.DOTToGraph = DOTToGraph; - controlNodeStyle.id = "targetNode" + util.randomUUID(); - controlNodeStyle.hidden = false; - controlNodeStyle.physics = false; - controlNodeStyle.x = x; - controlNodeStyle.y = y; +/***/ }, +/* 56 */ +/***/ function(module, exports, __webpack_require__) { - return this.body.functions.createNode(controlNodeStyle); - }, - writable: true, - configurable: true + "use strict"; + + function parseGephi(gephiJSON, options) { + var edges = []; + var nodes = []; + this.options = { + edges: { + inheritColor: true }, - _createEditButton: { + nodes: { + allowedToMove: false, + parseColor: false + } + }; + if (options !== undefined) { + this.options.nodes.allowedToMove = options.allowedToMove | false; + this.options.nodes.parseColor = options.parseColor | false; + this.options.edges.inheritColor = options.inheritColor | true; + } - /** - * Create the edit button - */ - value: function _createEditButton() { - // restore everything to it's original state (if applicable) - this._clean(); + 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); + } - // reset the manipulationDOM - this.manipulationDOM = {}; + 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); + } - // empty the editModeDiv - util.recursiveDOMDelete(this.editModeDiv); + return { nodes: nodes, edges: edges }; + } - // create the contents for the editMode button - var locale = this.options.locales[this.options.locale]; - var button = this._createButton("editMode", "network-manipulationUI edit editmode", locale.edit); - this.editModeDiv.appendChild(button); + exports.parseGephi = parseGephi; - // bind a hammer listener to the button, calling the function toggleEditMode. - this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); - }, - writable: true, - configurable: true - }, - _clean: { +/***/ }, +/* 57 */ +/***/ function(module, exports, __webpack_require__) { + "use strict"; - /** - * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. - * @private - */ - value: function _clean() { - // _clean the divs - if (this.guiEnabled === true) { - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.manipulationDiv); + /** + * @class Images + * This class loads images and keeps them stored. + */ + function Images(callback) { + this.images = {}; + this.imageBroken = {}; + this.callback = callback; + } - // removes all the bindings and overloads - this._cleanManipulatorHammers(); - } + /** + * + * @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 (me.callback) { + me.images[url] = img; + me.callback(this); + } + }; - // remove temporary nodes and edges - this._cleanupTemporaryNodesAndEdges(); + img.onerror = function () { + if (brokenUrl === undefined) { + console.error("Could not load image:", url); + delete this.src; + if (me.callback) { + me.callback(this); + } + } else { + if (me.imageBroken[url] === true) { + console.error("Could not load brokenImage:", brokenUrl); + delete this.src; + if (me.callback) { + me.callback(this); + } + } else { + console.error("Could not load image:", url); + this.src = brokenUrl; + me.imageBroken[url] = true; + } + } + }; - // restore overloaded UI functions - this._unbindTemporaryUIs(); + img.src = url; + } - // remove the temporaryEventFunctions - this._unbindTemporaryEvents(); + return img; + }; - // restore the physics if required - this.body.emitter.emit("restorePhysics"); - }, - writable: true, - configurable: true - }, - _cleanManipulatorHammers: { + module.exports = Images; +/***/ }, +/* 58 */ +/***/ function(module, exports, __webpack_require__) { - /** - * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. - * @private - */ - value: function _cleanManipulatorHammers() { - // _clean hammer bindings - if (this.manipulationHammers.length != 0) { - for (var i = 0; i < this.manipulationHammers.length; i++) { - this.manipulationHammers[i].destroy(); - } - this.manipulationHammers = []; - } - }, - writable: true, - configurable: true - }, - _removeManipulationDOM: { + "use strict"; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * Remove all DOM elements created by this module. - * @private - */ - value: function _removeManipulationDOM() { - // removes all the bindings and overloads - this._clean(); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // empty the manipulation divs - util.recursiveDOMDelete(this.manipulationDiv); - util.recursiveDOMDelete(this.editModeDiv); - util.recursiveDOMDelete(this.closeDiv); + var util = __webpack_require__(1); - // remove the manipulation divs - this.canvas.frame.removeChild(this.manipulationDiv); - this.canvas.frame.removeChild(this.editModeDiv); - this.canvas.frame.removeChild(this.closeDiv); + /** + * @class Groups + * This class can store groups and options specific for groups. + */ + var Groups = (function () { + function Groups() { + _classCallCheck(this, Groups); - // set the references to undefined - this.manipulationDiv = undefined; - this.editModeDiv = undefined; - this.closeDiv = undefined; + this.clear(); + this.defaultIndex = 0; + this.groupsArray = []; + this.groupIndex = 0; - // remove override - this.selectionHandler.forceSelectEdges = false; - }, - writable: true, - configurable: true - }, - _createSeperator: { + this.defaultGroups = [{ border: "#2B7CE9", background: "#97C2FC", highlight: { border: "#2B7CE9", background: "#D2E5FF" }, hover: { border: "#2B7CE9", background: "#D2E5FF" } }, // 0: blue + { border: "#FFA500", background: "#FFFF00", highlight: { border: "#FFA500", background: "#FFFFA3" }, hover: { border: "#FFA500", background: "#FFFFA3" } }, // 1: yellow + { border: "#FA0A10", background: "#FB7E81", highlight: { border: "#FA0A10", background: "#FFAFB1" }, hover: { border: "#FA0A10", background: "#FFAFB1" } }, // 2: red + { border: "#41A906", background: "#7BE141", highlight: { border: "#41A906", background: "#A1EC76" }, hover: { border: "#41A906", background: "#A1EC76" } }, // 3: green + { border: "#E129F0", background: "#EB7DF4", highlight: { border: "#E129F0", background: "#F0B3F5" }, hover: { border: "#E129F0", background: "#F0B3F5" } }, // 4: magenta + { border: "#7C29F0", background: "#AD85E4", highlight: { border: "#7C29F0", background: "#D3BDF0" }, hover: { border: "#7C29F0", background: "#D3BDF0" } }, // 5: purple + { border: "#C37F00", background: "#FFA807", highlight: { border: "#C37F00", background: "#FFCA66" }, hover: { border: "#C37F00", background: "#FFCA66" } }, // 6: orange + { border: "#4220FB", background: "#6E6EFD", highlight: { border: "#4220FB", background: "#9B9BFD" }, hover: { border: "#4220FB", background: "#9B9BFD" } }, // 7: darkblue + { border: "#FD5A77", background: "#FFC0CB", highlight: { border: "#FD5A77", background: "#FFD1D9" }, hover: { border: "#FD5A77", background: "#FFD1D9" } }, // 8: pink + { border: "#4AD63A", background: "#C2FABC", highlight: { border: "#4AD63A", background: "#E6FFE3" }, hover: { border: "#4AD63A", background: "#E6FFE3" } }, // 9: mint + { border: "#990000", background: "#EE0000", highlight: { border: "#BB0000", background: "#FF3333" }, hover: { border: "#BB0000", background: "#FF3333" } }, // 10:bright red - /** - * create a seperator line. the index is to differentiate in the manipulation dom - * @param index - * @private - */ - value: function _createSeperator() { - var index = arguments[0] === undefined ? 1 : arguments[0]; - this.manipulationDOM["seperatorLineDiv" + index] = document.createElement("div"); - this.manipulationDOM["seperatorLineDiv" + index].className = "network-seperatorLine"; - this.manipulationDiv.appendChild(this.manipulationDOM["seperatorLineDiv" + index]); - }, - writable: true, - configurable: true - }, - _createAddNodeButton: { + { 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 - // ---------------------- DOM functions for buttons --------------------------// + { border: "#EE0000", background: "#990000", highlight: { border: "#FF3333", background: "#BB0000" }, hover: { border: "#FF3333", background: "#BB0000" } }]; - value: function _createAddNodeButton(locale) { - var button = this._createButton("addNode", "network-manipulationUI add", locale.addNode); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.addNodeMode.bind(this)); - }, - writable: true, - configurable: true - }, - _createAddEdgeButton: { - value: function _createAddEdgeButton(locale) { - var button = this._createButton("addEdge", "network-manipulationUI connect", locale.addEdge); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); - }, - writable: true, - configurable: true - }, - _createEditNodeButton: { - value: function _createEditNodeButton(locale) { - var button = this._createButton("editNode", "network-manipulationUI edit", locale.editNode); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.editNode.bind(this)); - }, - writable: true, - configurable: true - }, - _createEditEdgeButton: { - value: function _createEditEdgeButton(locale) { - var button = this._createButton("editEdge", "network-manipulationUI edit", locale.editEdge); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); - }, - writable: true, - configurable: true - }, - _createDeleteButton: { - value: function _createDeleteButton(locale) { - var button = this._createButton("delete", "network-manipulationUI delete", locale.del); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.deleteSelected.bind(this)); - }, - writable: true, - configurable: true - }, - _createBackButton: { - value: function _createBackButton(locale) { - var button = this._createButton("back", "network-manipulationUI back", locale.back); - this.manipulationDiv.appendChild(button); - this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); - }, - writable: true, - configurable: true - }, - _createButton: { - value: function _createButton(id, className, label) { - var labelClassName = arguments[3] === undefined ? "network-manipulationLabel" : arguments[3]; - this.manipulationDOM[id + "Div"] = document.createElement("div"); - this.manipulationDOM[id + "Div"].className = className; - this.manipulationDOM[id + "Label"] = document.createElement("div"); - this.manipulationDOM[id + "Label"].className = labelClassName; - this.manipulationDOM[id + "Label"].innerHTML = label; - this.manipulationDOM[id + "Div"].appendChild(this.manipulationDOM[id + "Label"]); - return this.manipulationDOM[id + "Div"]; - }, - writable: true, - configurable: true - }, - _createDescription: { - value: function _createDescription(label) { - this.manipulationDiv.appendChild(this._createButton("description", "network-manipulationUI none", label)); - }, - writable: true, - configurable: true - }, - _temporaryBindEvent: { + this.options = {}; + this.defaultOptions = { + useDefaultGroups: true + }; + util.extend(this.options, this.defaultOptions); + } - // -------------------------- End of DOM functions for buttons ------------------------------// + _prototypeProperties(Groups, null, { + setOptions: { + value: function setOptions(options) { + var optionFields = ["useDefaultGroups"]; - /** - * this binds an event until cleanup by the clean functions. - * @param event - * @param newFunction - * @private - */ - value: function _temporaryBindEvent(event, newFunction) { - this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); - this.body.emitter.on(event, newFunction); + if (options !== undefined) { + for (var groupName in options) { + if (options.hasOwnProperty(groupName)) { + if (optionFields.indexOf(groupName) == -1) { + var group = options[groupName]; + this.add(groupName, group); + } + } + } + } }, writable: true, configurable: true }, - _temporaryBindUI: { + clear: { + /** - * this overrides an UI function until cleanup by the clean function - * @param UIfunctionName - * @param newFunction - * @private + * Clear all groups */ - value: function _temporaryBindUI(UIfunctionName, newFunction) { - if (this.body.eventListeners[UIfunctionName] !== undefined) { - this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; - this.body.eventListeners[UIfunctionName] = newFunction; - } else { - throw new Error("This UI function does not exist. Typo? You tried: \"" + UIfunctionName + "\" possible are: " + JSON.stringify(Object.keys(this.body.eventListeners))); - } + value: function clear() { + this.groups = {}; + this.groupsArray = []; }, writable: true, configurable: true }, - _unbindTemporaryUIs: { + get: { /** - * Restore the overridden UI functions to their original state. - * - * @private + * get group options of a groupname. If groupname is not found, a new group + * is added. + * @param {*} groupname Can be a number, string, Date, etc. + * @return {Object} group The created group, containing all group options */ - value: function _unbindTemporaryUIs() { - for (var functionName in this.temporaryUIFunctions) { - if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { - this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; - delete this.temporaryUIFunctions[functionName]; + value: function get(groupname) { + var group = this.groups[groupname]; + if (group == undefined) { + if (this.options.useDefaultGroups === false && this.groupsArray.length > 0) { + // create new group + var index = this.groupIndex % this.groupsArray.length; + this.groupIndex++; + group = {}; + group.color = this.groups[this.groupsArray[index]]; + this.groups[groupname] = group; + } else { + // create new group + var index = this.defaultIndex % this.defaultGroups.length; + this.defaultIndex++; + group = {}; + group.color = this.defaultGroups[index]; + this.groups[groupname] = group; } } - this.temporaryUIFunctions = {}; - }, - writable: true, - configurable: true - }, - _unbindTemporaryEvents: { - /** - * Unbind the events created by _temporaryBindEvent - * @private - */ - value: function _unbindTemporaryEvents() { - for (var i = 0; i < this.temporaryEventFunctions.length; i++) { - var eventName = this.temporaryEventFunctions[i].event; - var boundFunction = this.temporaryEventFunctions[i].boundFunction; - this.body.emitter.off(eventName, boundFunction); - } - this.temporaryEventFunctions = []; + return group; }, writable: true, configurable: true }, - _bindHammerToDiv: { + add: { /** - * Bind an hammer instance to a DOM element. - * @param domElement - * @param funct + * Add a custom group style + * @param {String} groupName + * @param {Object} style An object containing borderColor, + * backgroundColor, etc. + * @return {Object} group The created group object */ - value: function _bindHammerToDiv(domElement, boundFunction) { - var hammer = new Hammer(domElement, {}); - hammerUtil.onTouch(hammer, boundFunction); - this.manipulationHammers.push(hammer); + value: function add(groupName, style) { + this.groups[groupName] = style; + this.groupsArray.push(groupName); + return style; }, writable: true, configurable: true - }, - _cleanupTemporaryNodesAndEdges: { + } + }); + return Groups; + })(); - /** - * Neatly clean up temporary edges and nodes - * @private - */ - value: function _cleanupTemporaryNodesAndEdges() { - // _clean temporary edges - for (var i = 0; i < this.temporaryIds.edges.length; i++) { - this.body.edges[this.temporaryIds.edges[i]].disconnect(); - delete this.body.edges[this.temporaryIds.edges[i]]; - var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); - if (indexTempEdge !== -1) { - this.body.edgeIndices.splice(indexTempEdge, 1); - } - } + module.exports = Groups; + // 20:bright red - // _clean temporary nodes - for (var i = 0; i < this.temporaryIds.nodes.length; i++) { - delete this.body.nodes[this.temporaryIds.nodes[i]]; - var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); - if (indexTempNode !== -1) { - this.body.nodeIndices.splice(indexTempNode, 1); - } - } +/***/ }, +/* 59 */ +/***/ function(module, exports, __webpack_require__) { - this.temporaryIds = { nodes: [], edges: [] }; - }, - writable: true, - configurable: true - }, - _controlNodeTouch: { + "use strict"; - // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - /** - * the touch is used to get the position of the initial click - * @param event - * @private - */ - value: function _controlNodeTouch(event) { - this.lastTouch = this.body.functions.getPointer(event.center); - this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object - }, - writable: true, - configurable: true - }, - _controlNodeDragStart: { + 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"); } }; - /** - * the drag start is used to mark one of the control nodes as selected. - * @param event - * @private - */ - value: function _controlNodeDragStart(event) { - var pointer = this.lastTouch; - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - var to = this.body.nodes[this.temporaryIds.nodes[1]]; - var edge = this.body.edges[this.edgeBeingEditedId]; - this.selectedControlNode = undefined; + /** + * Created by Alex on 3/4/2015. + */ - var fromSelect = from.isOverlappingWith(pointerObj); - var toSelect = to.isOverlappingWith(pointerObj); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); - if (fromSelect === true) { - this.selectedControlNode = from; - edge.edgeType.from = from; - } else if (toSelect === true) { - this.selectedControlNode = to; - edge.edgeType.to = to; + var Node = _interopRequire(__webpack_require__(60)); + + var NodesHandler = (function () { + function NodesHandler(body, images, groups, layoutEngine) { + var _this = this; + _classCallCheck(this, NodesHandler); + + this.body = body; + this.images = images; + this.groups = groups; + this.layoutEngine = layoutEngine; + + // create the node API in the body container + this.body.functions.createNode = this.create.bind(this); + + this.nodesListeners = { + add: function (event, params) { + _this.add(params.items); + }, + update: function (event, params) { + _this.update(params.items, params.data); + }, + remove: function (event, params) { + _this.remove(params.items); + } + }; + + + this.options = {}; + this.defaultOptions = { + borderWidth: 1, + borderWidthSelected: undefined, + color: { + border: "#2B7CE9", + background: "#97C2FC", + highlight: { + border: "#2B7CE9", + background: "#D2E5FF" + }, + hover: { + border: "#2B7CE9", + background: "#D2E5FF" + } + }, + fixed: { + x: false, + y: false + }, + font: { + color: "#343434", + size: 14, // px + face: "arial", + background: "none", + stroke: 0, // px + strokeColor: "white", + align: "horizontal" + }, + group: undefined, + hidden: false, + icon: { + face: undefined, //'FontAwesome', + code: undefined, //'\uf007', + size: undefined, //50, + color: undefined //'#aa00ff' + }, + image: undefined, // --> URL + label: undefined, + level: undefined, + mass: 1, + physics: true, + scaling: { + min: 10, + max: 30, + label: { + enabled: true, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 3 + }, + customScalingFunction: function (min, max, total, value) { + if (max == min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + } + }, + shape: "ellipse", + size: 25, + value: 1, + x: undefined, + y: undefined + }; + util.extend(this.options, this.defaultOptions); + } + + _prototypeProperties(NodesHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options) { + util.selectiveNotDeepExtend(["color"], this.options, options); + if (options.color) { + this.options.color = util.parseColor(options.color); + } } - - this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - _controlNodeDrag: { + setData: { /** - * dragging the control nodes or the canvas - * @param event + * Set a data set with nodes for the network + * @param {Array | DataSet | DataView} nodes The data containing the nodes. * @private */ - value: function _controlNodeDrag(event) { - this.body.emitter.emit("disablePhysics"); - var pointer = this.body.functions.getPointer(event.center); - var pos = this.canvas.DOMtoCanvas(pointer); + value: function setData(nodes) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var oldNodesData = this.body.data.nodes; - if (this.selectedControlNode !== undefined) { - this.selectedControlNode.x = pos.x; - this.selectedControlNode.y = pos.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 { - // if the drag was not started properly because the click started outside the network div, start it now. - var diffX = pointer.x - this.lastTouch.x; - var diffY = pointer.y - this.lastTouch.y; - this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; + throw new TypeError("Array or DataSet expected"); } - this.body.emitter.emit("_redraw"); - }, - writable: true, - configurable: true - }, - _controlNodeDragEnd: { + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.off(event, callback); + }); + } - /** - * connecting or restoring the control nodes. - * @param event - * @private - */ - value: function _controlNodeDragEnd(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var edge = this.body.edges[this.edgeBeingEditedId]; + // remove drawn nodes + this.body.nodes = {}; - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - if (overlappingNodeIds[i] !== this.selectedControlNode.id) { - node = this.body.nodes[overlappingNodeIds[i]]; - break; - } + if (this.body.data.nodes) { + // subscribe to new dataset + var me = this; + util.forEach(this.nodesListeners, function (callback, event) { + me.body.data.nodes.on(event, callback); + }); + + // draw all new nodes + var ids = this.body.data.nodes.getIds(); + this.add(ids, true); } - // perform the connection - if (node !== undefined && this.selectedControlNode !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale].createEdgeError); - } else { - var from = this.body.nodes[this.temporaryIds.nodes[0]]; - if (this.selectedControlNode.id == from.id) { - this._performEditEdge(node.id, edge.to.id); - } else { - this._performEditEdge(edge.from.id, node.id); - } - } - } else { - edge.updateEdgeType(); - this.body.emitter.emit("restorePhysics"); + if (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); } - this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - _handleConnect: { - - // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// - + add: { - // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// /** - * the function bound to the selection event. It checks if you want to connect a cluster and changes the description - * to walk the user through the process. - * + * Add nodes + * @param {Number[] | String[]} ids * @private */ - value: function _handleConnect(event) { - var _this = this; - // check to avoid double fireing of this function. - if (new Date().valueOf() - this.touchTime > 100) { - var pointer = this.body.functions.getPointer(event.center); - var node = this.selectionHandler.getNodeAt(pointer); - - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale].createEdgeError); - } else { - (function () { - // create a node the temporary line can look at - var targetNode = _this._getNewTargetNode(node.x, node.y); - var targetNodeId = targetNode.id; - _this.body.nodes[targetNode.id] = targetNode; - _this.body.nodeIndices.push(targetNode.id); - - // create a temporary edge - var connectionEdge = _this.body.functions.createEdge({ - id: "connectionEdge" + util.randomUUID(), - from: node.id, - to: targetNode.id, - physics: false, - smooth: { - enabled: true, - dynamic: false, - type: "continuous", - roundness: 0.5 - } - }); - _this.body.edges[connectionEdge.id] = connectionEdge; - _this.body.edgeIndices.push(connectionEdge.id); - - _this.temporaryIds.nodes.push(targetNode.id); - _this.temporaryIds.edges.push(connectionEdge.id); + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var id; + var newNodes = []; + for (var i = 0; i < ids.length; i++) { + id = ids[i]; + var properties = this.body.data.nodes.get(id); + var node = this.create(properties);; + newNodes.push(node); + this.body.nodes[id] = node; // note: this may replace an existing node + } - _this.temporaryUIFunctions.onDrag = _this.body.eventListeners.onDrag; - _this.body.eventListeners.onDrag = function (event) { - var pointer = _this.body.functions.getPointer(event.center); - var targetNode = _this.body.nodes[targetNodeId]; - targetNode.x = _this.canvas._XconvertDOMtoCanvas(pointer.x); - targetNode.y = _this.canvas._YconvertDOMtoCanvas(pointer.y); - _this.body.emitter.emit("_redraw"); - }; - })(); - } - } - this.touchTime = new Date().valueOf(); + this.layoutEngine.positionInitially(newNodes); - // do the original touch events - this.temporaryUIFunctions.onTouch(event); + if (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); } }, writable: true, configurable: true }, - _finishConnect: { + update: { /** - * Connect the new edge to the target if one exists, otherwise remove temp line - * @param event + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids * @private */ - value: function _finishConnect(event) { - var pointer = this.body.functions.getPointer(event.center); - var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - - // remember the edge id - var connectFromId = undefined; - if (this.temporaryIds.edges[0] !== undefined) { - connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; - } - - //restore the drag function - if (this.temporaryUIFunctions.onDrag !== undefined) { - this.body.eventListeners.onDrag = this.temporaryUIFunctions.onDrag; - delete this.temporaryUIFunctions.onDrag; - } - - // get the overlapping node but NOT the temporary node; - var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); - var node = undefined; - for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { - if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) !== -1) { - node = this.body.nodes[overlappingNodeIds[i]]; - break; - } - } - - // clean temporary nodes and edges. - this._cleanupTemporaryNodesAndEdges(); - - // perform the connection - if (node !== undefined) { - if (node.isCluster === true) { - alert(this.options.locales[this.options.locale].createEdgeError); + value: function update(ids, changedData) { + var nodes = this.body.nodes; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = changedData[i]; + if (node !== undefined) { + // update node + node.setOptions(data, this.constants); } else { - if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { - this._performCreateEdge(connectFromId, node.id); - } + dataChanged = true; + // create node + node = this.create(properties); + nodes[id] = node; } } - this.body.emitter.emit("_redraw"); - }, - writable: true, - configurable: true - }, - _performAddNode: { - - // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// - - - // ------------------------------ Performing all the actual data manipulation ------------------------// - - /** - * Adds a node on the specified location - */ - value: function _performAddNode(clickData) { - var _this = this; - var defaultData = { - id: util.randomUUID(), - x: clickData.pointer.canvas.x, - y: clickData.pointer.canvas.y, - label: "new" - }; - if (typeof this.options.handlerFunctions.addNode === "function") { - if (this.options.handlerFunctions.addNode.length == 2) { - this.options.handlerFunctions.addNode(defaultData, function (finalizedData) { - _this.body.data.nodes.add(finalizedData); - _this.showManipulatorToolbar(); - }); - } else { - throw new Error("The function for add does not support two arguments (data,callback)"); - this.showManipulatorToolbar(); - } + if (dataChanged === true) { + this.body.emitter.emit("_dataChanged"); } else { - this.body.data.nodes.add(defaultData); - this.showManipulatorToolbar(); + this.body.emitter.emit("_dataUpdated"); } }, writable: true, configurable: true }, - _performCreateEdge: { - + remove: { /** - * connect two nodes with a new edge. - * + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids * @private */ - value: function _performCreateEdge(sourceNodeId, targetNodeId) { - var _this = this; - var defaultData = { from: sourceNodeId, to: targetNodeId }; - if (this.options.handlerFunctions.addEdge) { - if (this.options.handlerFunctions.addEdge.length == 2) { - this.options.handlerFunctions.addEdge(defaultData, function (finalizedData) { - _this.body.data.edges.add(finalizedData); - _this.selectionHandler.unselectAll(); - _this.showManipulatorToolbar(); - }); - } else { - throw new Error("The function for connect does not support two arguments (data,callback)"); - } - } else { - this.body.data.edges.add(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); + value: function remove(ids) { + var nodes = this.body.nodes; + + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + delete nodes[id]; } + + this.body.emitter.emit("_dataChanged"); }, writable: true, configurable: true }, - _performEditEdge: { - - /** - * connect two nodes with a new edge. - * - * @private - */ - value: function _performEditEdge(sourceNodeId, targetNodeId) { - var _this = this; - var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; - if (this.options.handlerFunctions.editEdge) { - if (this.options.handlerFunctions.editEdge.length == 2) { - this.options.handlerFunctions.editEdge(defaultData, function (finalizedData) { - _this.body.data.edges.update(finalizedData); - _this.selectionHandler.unselectAll(); - _this.showManipulatorToolbar(); - }); - } else { - throw new Error("The function for edit does not support two arguments (data, callback)"); - } - } else { - this.body.data.edges.update(defaultData); - this.selectionHandler.unselectAll(); - this.showManipulatorToolbar(); - } + create: { + value: function create(properties) { + var constructorClass = arguments[1] === undefined ? Node : arguments[1]; + return new constructorClass(properties, this.body, this.images, this.groups, this.options); }, writable: true, configurable: true } }); - return ManipulationSystem; + return NodesHandler; })(); - module.exports = ManipulationSystem; + module.exports = NodesHandler; /***/ }, -/* 62 */ +/* 60 */ /***/ function(module, exports, __webpack_require__) { - - /** - * Expose `Emitter`. - */ - - module.exports = Emitter; - - /** - * Initialize a new `Emitter`. - * - * @api public - */ - - function Emitter(obj) { - if (obj) return mixin(obj); - }; - - /** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ - - function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; - } - return obj; - } - - /** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - - Emitter.prototype.on = - Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks[event] = this._callbacks[event] || []) - .push(fn); - return this; - }; - - /** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - - Emitter.prototype.once = function(event, fn){ - var self = this; - this._callbacks = this._callbacks || {}; - - function on() { - self.off(event, on); - fn.apply(this, arguments); - } - - on.fn = fn; - this.on(event, on); - return this; - }; - - /** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - - Emitter.prototype.off = - Emitter.prototype.removeListener = - Emitter.prototype.removeAllListeners = - Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } - - // specific event - var callbacks = this._callbacks[event]; - if (!callbacks) return this; - - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks[event]; - return this; - } - - // remove specific handler - var cb; - for (var i = 0; i < callbacks.length; i++) { - cb = callbacks[i]; - if (cb === fn || cb.fn === fn) { - callbacks.splice(i, 1); - break; - } - } - return this; - }; - - /** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} - */ - - Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks[event]; - - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); - } - } - - return this; - }; - - /** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ - - Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks[event] || []; - }; - - /** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public - */ + "use strict"; - Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; - }; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; -/***/ }, -/* 63 */ -/***/ function(module, exports, __webpack_require__) { + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - 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); - (function (undefined) { - /************************************ - Constants - ************************************/ + var Label = _interopRequire(__webpack_require__(61)); - 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, + var Box = _interopRequire(__webpack_require__(62)); - YEAR = 0, - MONTH = 1, - DATE = 2, - HOUR = 3, - MINUTE = 4, - SECOND = 5, - MILLISECOND = 6, + var Circle = _interopRequire(__webpack_require__(64)); - // internal storage for locale config files - locales = {}, + var CircularImage = _interopRequire(__webpack_require__(66)); - // extra moment internal properties (plugins register props here) - momentProperties = [], + var Database = _interopRequire(__webpack_require__(67)); - // check for nodeJS - hasModule = (typeof module !== 'undefined' && module && module.exports), + var Diamond = _interopRequire(__webpack_require__(68)); - // ASP.NET json date format regex - aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, - aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, + var Dot = _interopRequire(__webpack_require__(70)); - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, + var Ellipse = _interopRequire(__webpack_require__(71)); - // format tokens - formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g, - localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, + var Icon = _interopRequire(__webpack_require__(72)); - // 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 + var Image = _interopRequire(__webpack_require__(73)); - //strict parsing regexes - parseTokenOneDigit = /\d/, // 0 - 9 - parseTokenTwoDigits = /\d\d/, // 00 - 99 - parseTokenThreeDigits = /\d{3}/, // 000 - 999 - parseTokenFourDigits = /\d{4}/, // 0000 - 9999 - parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 - parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf + var Square = _interopRequire(__webpack_require__(74)); - // 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)?)?$/, + var Star = _interopRequire(__webpack_require__(75)); - isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + var Text = _interopRequire(__webpack_require__(76)); - 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}/] - ], + var Triangle = _interopRequire(__webpack_require__(77)); - // 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 TriangleDown = _interopRequire(__webpack_require__(78)); - // timezone chunker '+10:00' > ['10', '00'] or '-1530' > ['-', '15', '30'] - parseTimezoneChunker = /([\+\-]|\d\d)/gi, + /** + * @class Node + * A node. A node can be connected to other nodes via one or multiple edges. + * @param {object} options An object containing options for the node. All + * options are optional, except for the id. + * {number} id Id of the node. Required + * {string} label Text label for the node + * {number} x Horizontal position of the node + * {number} y Vertical position of the node + * {string} shape Node shape, available: + * "database", "circle", "ellipse", + * "box", "image", "text", "dot", + * "star", "triangle", "triangleDown", + * "square", "icon" + * {string} image An image url + * {string} title An title text, can be HTML + * {anytype} group A group name or number + * @param {Network.Images} imagelist A list with images. Only needed + * when the node has an image + * @param {Network.Groups} grouplist A list with groups. Needed for + * retrieving group options + * @param {Object} constants An object with default values for + * example for the color + * + */ + var Node = (function () { + function Node(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Node); - // 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 - }, + this.options = util.bridgeObject(globalOptions); + this.body = body; - 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' - }, + this.edges = []; // all edges connected to this node - camelFunctions = { - dayofyear : 'dayOfYear', - isoweekday : 'isoWeekday', - isoweek : 'isoWeek', - weekyear : 'weekYear', - isoweekyear : 'isoWeekYear' - }, + // set defaults for the options + this.id = undefined; + this.imagelist = imagelist; + this.grouplist = grouplist; - // format function strings - formatFunctions = {}, + // state options + this.x = undefined; + this.y = undefined; + this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate + this.selected = false; + this.hover = false; - // 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.labelModule = new Label(this.body, this.options); + this.setOptions(options); + } - // tokens to ordinalize and pad - ordinalizeTokens = 'DDD w W M D d'.split(' '), - paddedTokens = 'M D H h m s w W'.split(' '), + _prototypeProperties(Node, null, { + attachEdge: { - formatTokenFunctions = { - M : function () { - return this.month() + 1; - }, - MMM : function (format) { - return this.localeData().monthsShort(this, format); - }, - MMMM : function (format) { - return this.localeData().months(this, format); - }, - D : function () { - return this.date(); - }, - DDD : function () { - return this.dayOfYear(); - }, - d : function () { - return this.day(); - }, - dd : function (format) { - return this.localeData().weekdaysMin(this, format); - }, - ddd : function (format) { - return this.localeData().weekdaysShort(this, format); - }, - dddd : function (format) { - return this.localeData().weekdays(this, format); - }, - w : function () { - return this.week(); - }, - W : function () { - return this.isoWeek(); - }, - YY : function () { - return leftZeroFill(this.year() % 100, 2); - }, - YYYY : function () { - return leftZeroFill(this.year(), 4); - }, - YYYYY : function () { - return leftZeroFill(this.year(), 5); - }, - YYYYYY : function () { - var y = this.year(), sign = y >= 0 ? '+' : '-'; - return sign + leftZeroFill(Math.abs(y), 6); - }, - gg : function () { - return leftZeroFill(this.weekYear() % 100, 2); - }, - gggg : function () { - return leftZeroFill(this.weekYear(), 4); - }, - ggggg : function () { - return leftZeroFill(this.weekYear(), 5); - }, - GG : function () { - return leftZeroFill(this.isoWeekYear() % 100, 2); - }, - GGGG : function () { - return leftZeroFill(this.isoWeekYear(), 4); - }, - GGGGG : function () { - return leftZeroFill(this.isoWeekYear(), 5); - }, - e : function () { - return this.weekday(); - }, - E : function () { - return this.isoWeekday(); - }, - a : function () { - return this.localeData().meridiem(this.hours(), this.minutes(), true); - }, - A : function () { - return this.localeData().meridiem(this.hours(), this.minutes(), false); - }, - H : function () { - return this.hours(); - }, - h : function () { - return this.hours() % 12 || 12; - }, - m : function () { - return this.minutes(); - }, - s : function () { - return this.seconds(); - }, - S : function () { - return toInt(this.milliseconds() / 100); - }, - SS : function () { - return leftZeroFill(toInt(this.milliseconds() / 10), 2); - }, - SSS : function () { - return leftZeroFill(this.milliseconds(), 3); - }, - SSSS : function () { - return leftZeroFill(this.milliseconds(), 3); - }, - Z : function () { - var a = this.utcOffset(), - b = '+'; - if (a < 0) { - a = -a; - b = '-'; - } - return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2); - }, - ZZ : function () { - var a = this.utcOffset(), - b = '+'; - if (a < 0) { - a = -a; - b = '-'; - } - return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); - }, - z : function () { - return this.zoneAbbr(); - }, - zz : function () { - return this.zoneName(); - }, - x : function () { - return this.valueOf(); - }, - X : function () { - return this.unix(); - }, - Q : function () { - return this.quarter(); - } - }, - deprecations = {}, + /** + * Attach a edge to the node + * @param {Edge} edge + */ + value: function attachEdge(edge) { + if (this.edges.indexOf(edge) == -1) { + this.edges.push(edge); + } + }, + writable: true, + configurable: true + }, + detachEdge: { - lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'], - updateInProgress = false; + /** + * Detach a edge from the node + * @param {Edge} edge + */ + value: function detachEdge(edge) { + var index = this.edges.indexOf(edge); + if (index != -1) { + this.edges.splice(index, 1); + } + }, + writable: true, + configurable: true + }, + togglePhysics: { - // 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'); + /** + * Enable or disable the physics. + * @param status + */ + value: function togglePhysics(status) { + this.options.physics = status; + }, + writable: true, + configurable: true + }, + setOptions: { + + + /** + * Set or overwrite options for the node + * @param {Object} options an object with options + * @param {Object} constants and object with default, global options + */ + value: function setOptions(options) { + if (!options) { + return; } - } - function hasOwnProp(a, b) { - return hasOwnProperty.call(a, b); - } + var fields = ["borderWidth", "borderWidthSelected", "brokenImage", "customScalingFunction", "font", "hidden", "icon", "id", "image", "label", "level", "physics", "shape", "size", "title", "value", "x", "y"]; + util.selectiveDeepExtend(fields, this.options, options); - 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 - }; - } + // basic options + if (options.id !== undefined) { + this.id = options.id; + } - function printMsg(msg) { - if (moment.suppressDeprecationWarnings === false && - typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); + if (this.id === undefined) { + throw "Node must have an id"; } - } - function deprecate(msg, fn) { - var firstTime = true; - return extend(function () { - if (firstTime) { - printMsg(msg); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); - } + if (options.x !== undefined) { + this.x = options.x;this.predefinedPosition = true; + } + if (options.y !== undefined) { + this.y = options.y;this.predefinedPosition = true; + } + if (options.value !== undefined) { + this.value = options.value; + } - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - printMsg(msg); - deprecations[name] = true; + // copy group options + if (typeof options.group === "number" || typeof options.group === "string" && options.group != "") { + var groupObj = this.grouplist.get(options.group); + util.deepExtend(this.options, groupObj); + // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. + this.options.color = util.parseColor(this.options.color); + } + // individual shape options + if (options.color !== undefined) { + this.options.color = util.parseColor(options.color); } - } - 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); - }; - } + 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"; + } + } - function monthDiff(a, b) { - // difference in months - var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), - // b is in (anchor - 1 month, anchor + 1 month) - anchor = a.clone().add(wholeMonthDiff, 'months'), - anchor2, adjust; + if (options.fixed !== undefined) { + if (typeof options.fixed == "boolean") { + this.options.fixed.x = true; + this.options.fixed.y = true; + } else { + if (options.fixed.x !== undefined && typeof options.fixed.x == "boolean") { + this.options.fixed.x = options.fixed.x; + } + if (options.fixed.y !== undefined && typeof options.fixed.y == "boolean") { + this.options.fixed.y = options.fixed.y; + } + } + } - 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); + // choose draw method depending on the shape + switch (this.options.shape) { + + case "box": + this.shape = new Box(this.options, this.body, this.labelModule); + break; + case "circle": + this.shape = new Circle(this.options, this.body, this.labelModule); + break; + case "circularImage": + this.shape = new CircularImage(this.options, this.body, this.labelModule, this.imageObj); + break; + case "database": + this.shape = new Database(this.options, this.body, this.labelModule); + break; + case "diamond": + this.shape = new Diamond(this.options, this.body, this.labelModule); + break; + case "dot": + this.shape = new Dot(this.options, this.body, this.labelModule); + break; + case "ellipse": + this.shape = new Ellipse(this.options, this.body, this.labelModule); + break; + case "icon": + this.shape = new Icon(this.options, this.body, this.labelModule); + break; + case "image": + this.shape = new Image(this.options, this.body, this.labelModule, this.imageObj); + break; + case "square": + this.shape = new Square(this.options, this.body, this.labelModule); + break; + case "star": + this.shape = new Star(this.options, this.body, this.labelModule); + break; + case "text": + this.shape = new Text(this.options, this.body, this.labelModule); + break; + case "triangle": + this.shape = new Triangle(this.options, this.body, this.labelModule); + break; + case "triangleDown": + this.shape = new TriangleDown(this.options, this.body, this.labelModule); + break; + default: + this.shape = new Ellipse(this.options, this.body, this.labelModule); + break; } - return -(wholeMonthDiff + adjust); - } + this.labelModule.setOptions(this.options, options); + + // reset the size of the node, this can be changed + this._reset(); + }, + writable: true, + configurable: true + }, + select: { + - while (ordinalizeTokens.length) { - i = ordinalizeTokens.pop(); - formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); - } - while (paddedTokens.length) { - i = paddedTokens.pop(); - formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); - } - formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); + /** + * select this node + */ + value: function select() { + this.selected = true; + this._reset(); + }, + writable: true, + configurable: true + }, + unselect: { - function meridiemFixWrap(locale, hour, meridiem) { - var isPm; + /** + * unselect this node + */ + value: function unselect() { + this.selected = false; + this._reset(); + }, + writable: true, + configurable: true + }, + _reset: { - 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; - } - } - /************************************ - Constructors - ************************************/ - function Locale() { - } + /** + * Reset the calculated size of the node, forces it to recalculate its size + * @private + */ + value: function _reset() { + this.shape.width = undefined; + this.shape.height = undefined; + }, + writable: true, + configurable: true + }, + getTitle: { - // 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; - } - } - // 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; + /** + * get the title of this node. + * @return {string} title The title of the node, or undefined when no title + * has been set. + */ + value: function getTitle() { + return typeof this.options.title === "function" ? this.options.title() : this.options.title; + }, + writable: true, + configurable: true + }, + distanceToBorder: { - // 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; - this._data = {}; + /** + * Calculate the distance to the border of the Node + * @param {CanvasRenderingContext2D} ctx + * @param {Number} angle Angle in radians + * @returns {number} distance Distance to the border in pixels + */ + value: function distanceToBorder(ctx, angle) { + return this.shape.distanceToBorder(ctx, angle); + }, + writable: true, + configurable: true + }, + isFixed: { - this._locale = moment.localeData(); - this._bubble(); - } + /** + * Check if this node has a fixed x and y position + * @return {boolean} true if fixed, false if not + */ + value: function isFixed() { + return this.options.fixed.x && this.options.fixed.y; + }, + writable: true, + configurable: true + }, + isSelected: { - /************************************ - Helpers - ************************************/ + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + }, + writable: true, + configurable: true + }, + getValue: { - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } - } - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } + /** + * Retrieve the value of the node. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.value; + }, + writable: true, + configurable: true + }, + setValueRange: { - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; + + /** + * Adjust the value range of the node. The node will adjust it's size + * based on its value. + * @param {Number} min + * @param {Number} max + */ + value: function setValueRange(min, max, total) { + if (this.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); + var sizeDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled == true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + } + this.options.size = this.options.scaling.min + scale * sizeDiff; } + }, + writable: true, + configurable: true + }, + draw: { - return a; - } - function copyConfig(to, from) { - var i, prop, val; + /** + * Draw this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function draw(ctx) { + this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); + }, + writable: true, + configurable: true + }, + resize: { - 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; - } - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } - } + /** + * Recalculate the size of this node in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function resize(ctx) { + this.shape.resize(ctx); + }, + writable: true, + configurable: true + }, + isOverlappingWith: { - return to; - } - function absRound(number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); - } + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top, right, bottom + * @return {boolean} True if location is located on node + */ + value: function isOverlappingWith(obj) { + return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; + }, + writable: true, + configurable: true } + }); - // 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; + return Node; + })(); - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } + module.exports = Node; - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; +/***/ }, +/* 61 */ +/***/ function(module, exports, __webpack_require__) { - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; - } + "use strict"; - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); + var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; - return res; - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - function momentsDifference(base, other) { - var res; - other = makeAs(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - return res; - } + var util = __webpack_require__(1); - // 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; - } + /** + * Created by Alex on 3/17/2015. + */ - val = typeof val === 'string' ? +val : val; - dur = moment.duration(val, period); - addOrSubtractDurationFromMoment(this, dur, direction); - return this; - }; - } + var Label = (function () { + function Label(body, options) { + _classCallCheck(this, Label); - function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; + this.body = body; - 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); + this.fontOptions = {}; + this.defaultOptions = { + color: "#343434", + size: 14, // px + face: "arial", + background: "none", + stroke: 0, // px + strokeColor: "white", + align: "horizontal" + }; + util.extend(this.fontOptions, this.defaultOptions); + + this.setOptions(options); + this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + } + + _prototypeProperties(Label, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + if (options.label !== undefined) { + this.labelDirty = true; } - if (updateOffset) { - moment.updateOffset(mom, days || months); + if (options.font) { + if (typeof options.font === "string") { + var optionsArray = options.font.split(" "); + this.fontOptions.size = optionsArray[0].replace("px", ""); + this.fontOptions.face = optionsArray[1]; + this.fontOptions.color = optionsArray[2]; + } else if (typeof options.font == "object") { + util.protoExtend(this.fontOptions, options.font); + } + this.fontOptions.size = Number(this.fontOptions.size); } - } + }, + writable: true, + configurable: true + }, + draw: { - // check if is an array - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; - } - function isDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || - input instanceof Date; - } + /** + * Main function. This is called from anything that wants to draw a label. + * @param ctx + * @param x + * @param y + * @param selected + * @param baseline + */ + value: function draw(ctx, x, y, selected) { + var baseline = arguments[4] === undefined ? "middle" : arguments[4]; + // if no label, return + if (this.options.label === undefined) { + return; + } // check if we have to render the label + var viewFontSize = this.fontOptions.size * this.body.view.scale; + if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) { + return; + } // update the size cache if required + this.calculateLabelSize(ctx, selected, x, y, baseline); - // 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++; - } + // create the fontfill background + this._drawBackground(ctx); + // draw text + this._drawText(ctx, selected, x, y, baseline); + }, + writable: true, + configurable: true + }, + _drawBackground: { + + /** + * Draws the label background + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _drawBackground(ctx) { + if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") { + ctx.fillStyle = this.fontOptions.background; + + var lineMargin = 2; + + switch (this.fontOptions.align) { + case "middle": + ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); + break; + case "top": + ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); + break; + case "bottom": + ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); + break; + default: + ctx.fillRect(this.size.left, this.size.top, this.size.width, this.size.height); + break; + } + } + }, + writable: true, + configurable: true + }, + _drawText: { + + + /** + * + * @param ctx + * @param x + * @param baseline + * @private + */ + value: function _drawText(ctx, selected, x, y) { + var baseline = arguments[4] === undefined ? "middle" : arguments[4]; + var fontSize = this.fontOptions.size; + var viewFontSize = fontSize * this.body.view.scale; + // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) + if (viewFontSize >= this.options.scaling.label.maxVisible) { + fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; } - return diffs + lengthDiff; - } - function normalizeUnits(units) { - if (units) { - var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); - units = unitAliases[units] || camelFunctions[lowered] || lowered; - } - return units; - } + var yLine = this.size.yLine; + var _getColor = this._getColor(viewFontSize); - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; + var _getColor2 = _slicedToArray(_getColor, 2); - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } + var fontColor = _getColor2[0]; + var strokeColor = _getColor2[1]; + var _ref = this._setAlignment(ctx, x, yLine, baseline); - return normalizedInput; - } + var _ref2 = _slicedToArray(_ref, 2); - function makeList(field) { - var count, setter; + x = _ref2[0]; + yLine = _ref2[1]; - if (field.indexOf('week') === 0) { - count = 7; - setter = 'day'; + + // configure context for drawing the text + ctx.font = (selected ? "bold " : "") + fontSize + "px " + this.fontOptions.face; + ctx.fillStyle = fontColor; + ctx.textAlign = "center"; + + // set the strokeWidth + if (this.fontOptions.stroke > 0) { + ctx.lineWidth = this.fontOptions.stroke; + ctx.strokeStyle = strokeColor; + ctx.lineJoin = "round"; } - else if (field.indexOf('month') === 0) { - count = 12; - setter = 'month'; + + // draw the text + for (var i = 0; i < this.lineCount; i++) { + if (this.fontOptions.stroke > 0) { + ctx.strokeText(this.lines[i], x, yLine); + } + ctx.fillText(this.lines[i], x, yLine); + yLine += fontSize; } - else { - return; + }, + writable: true, + configurable: true + }, + _setAlignment: { + value: function _setAlignment(ctx, x, yLine, baseline) { + // check for label alignment (for edges) + // TODO: make alignment for nodes + if (this.fontOptions.align !== "horizontal") { + x = 0; + yLine = 0; + + var lineMargin = 2; + if (this.fontOptions.align === "top") { + ctx.textBaseline = "alphabetic"; + yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers + } else if (this.fontOptions.align === "bottom") { + ctx.textBaseline = "hanging"; + yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers + } else { + ctx.textBaseline = "middle"; + } + } else { + ctx.textBaseline = baseline; } - moment[field] = function (format, index) { - var i, getter, - method = moment._locale[field], - results = []; + return [x, yLine]; + }, + writable: true, + configurable: true + }, + _getColor: { - if (typeof format === 'number') { - index = format; - format = undefined; - } + /** + * fade in when relative scale is between threshold and threshold - 1. + * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. + * + * @param viewFontSize + * @returns {*[]} + * @private + */ + value: function _getColor(viewFontSize) { + var fontColor = this.fontOptions.color || "#000000"; + var strokeColor = this.fontOptions.strokeColor || "#ffffff"; + if (viewFontSize <= this.options.scaling.label.drawThreshold) { + var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); + fontColor = util.overrideOpacity(fontColor, opacity); + strokeColor = util.overrideOpacity(strokeColor, opacity); + } + return [fontColor, strokeColor]; + }, + writable: true, + configurable: true + }, + getTextSize: { - getter = function (i) { - var m = moment().utc().set(setter, i); - return method.call(moment._locale, m, format || ''); - }; - if (index != null) { - return getter(index); - } - else { - for (i = 0; i < count; i++) { - results.push(getter(i)); - } - return results; - } + /** + * + * @param ctx + * @param selected + * @returns {{width: number, height: number}} + */ + value: function getTextSize(ctx) { + var selected = arguments[1] === undefined ? false : arguments[1]; + var size = { + width: this._processLabel(ctx, selected), + height: this.fontOptions.size * this.lineCount }; - } + return size; + }, + writable: true, + configurable: true + }, + calculateLabelSize: { - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } + /** + * + * @param ctx + * @param selected + * @param x + * @param y + * @param baseline + */ + value: function calculateLabelSize(ctx, selected) { + var x = arguments[2] === undefined ? 0 : arguments[2]; + var y = arguments[3] === undefined ? 0 : arguments[3]; + var baseline = arguments[4] === undefined ? "middle" : arguments[4]; + if (this.labelDirty === true) { + this.size.width = this._processLabel(ctx, selected); + } + this.size.height = this.fontOptions.size * this.lineCount; + this.size.left = x - this.size.width * 0.5; + this.size.top = y - this.size.height * 0.5; + this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; + if (baseline == "hanging") { + this.size.top += 0.5 * this.fontOptions.size; + this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers + this.size.yLine += 4; // distance from node } - return value; - } - - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } + this.labelDirty = false; + }, + writable: true, + configurable: true + }, + _processLabel: { - function weeksInYear(year, dow, doy) { - return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; - } - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } + /** + * This calculates the width as well as explodes the label string and calculates the amount of lines. + * @param ctx + * @param selected + * @returns {number} + * @private + */ + value: function _processLabel(ctx, selected) { + var width = 0; + var lines = [""]; + var lineCount = 0; + if (this.options.label !== undefined) { + lines = String(this.options.label).split("\n"); + lineCount = lines.length; + ctx.font = (selected ? "bold " : "") + this.fontOptions.size + "px " + this.fontOptions.face; + width = ctx.measureText(lines[0]).width; + for (var i = 1; i < lineCount; i++) { + var lineWidth = ctx.measureText(lines[i]).width; + width = lineWidth > width ? lineWidth : width; + } + } + this.lines = lines; + this.lineCount = lineCount; - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + return width; + }, + writable: true, + configurable: true } + }); - function checkOverflow(m) { - var overflow; - if (m._a && m._pf.overflow === -2) { - overflow = - m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : - m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : - m._a[HOUR] < 0 || m._a[HOUR] > 24 || - (m._a[HOUR] === 24 && (m._a[MINUTE] !== 0 || - m._a[SECOND] !== 0 || - m._a[MILLISECOND] !== 0)) ? HOUR : - m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : - m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : - m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : - -1; + return Label; + })(); - if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } + module.exports = Label; - m._pf.overflow = overflow; - } - } +/***/ }, +/* 62 */ +/***/ function(module, exports, __webpack_require__) { - 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; + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - if (m._strict) { - m._isValid = m._isValid && - m._pf.charsLeftOver === 0 && - m._pf.unusedTokens.length === 0 && - m._pf.bigHour === undefined; - } - } - return m._isValid; - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // 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 _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - while (i < names.length) { - split = normalizeLocale(names[i]).split('-'); - j = split.length; - next = normalizeLocale(names[i + 1]); - next = next ? next.split('-') : null; - while (j > 0) { - locale = loadLocale(split.slice(0, j).join('-')); - if (locale) { - return locale; - } - if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { - //the next array item is better than a shallower substring of this one - break; - } - j--; - } - i++; - } - return null; - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - function 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 _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // 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(); - } - } + var NodeBase = _interopRequire(__webpack_require__(63)); - /************************************ - Locale - ************************************/ + var Box = (function (NodeBase) { + function Box(options, body, labelModule) { + _classCallCheck(this, Box); + _get(Object.getPrototypeOf(Box.prototype), "constructor", this).call(this, options, body, labelModule); + } - extend(Locale.prototype, { + _inherits(Box, NodeBase); - 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); - }, + _prototypeProperties(Box, null, { + resize: { + value: function resize(ctx) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, this.selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), - months : function (m) { - return this._months[m.month()]; - }, + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), - monthsShort : function (m) { - return this._monthsShort[m.month()]; - }, + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - monthsParse : function (monthName, format, strict) { - var i, mom, regex; + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } + ctx.roundRect(this.left, this.top, this.width, this.height, this.options.size); + ctx.fill(); + ctx.stroke(); - 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; - } - } - }, + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), - weekdays : function (m) { - return this._weekdays[m.day()]; - }, + this.labelModule.draw(ctx, x, y, selected); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + }, + writable: true, + configurable: true + } + }); - _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), - weekdaysShort : function (m) { - return this._weekdaysShort[m.day()]; - }, + return Box; + })(NodeBase); - _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), - weekdaysMin : function (m) { - return this._weekdaysMin[m.day()]; - }, + module.exports = Box; - weekdaysParse : function (weekdayName) { - var i, mom, regex; +/***/ }, +/* 63 */ +/***/ function(module, exports, __webpack_require__) { - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } + "use strict"; - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = moment([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } - } - }, + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - _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; - }, + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - 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'); - }, + /** + * Created by Alex on 3/19/2015. + */ - _meridiemParse : /[ap]\.?m?\.?/i, - meridiem : function (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } - }, + var NodeBase = (function () { + function NodeBase(options, body, labelModule) { + _classCallCheck(this, NodeBase); + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.top = undefined; + this.left = undefined; + this.height = undefined; + this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; + } - _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; - }, + _prototypeProperties(NodeBase, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + _distanceToBorder: { + value: function _distanceToBorder(angle) { + var borderWidth = 1; + return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + }, + writable: true, + configurable: true + } + }); - _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' - }, + return NodeBase; + })(); - 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); - }, + module.exports = NodeBase; - pastFuture : function (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - }, +/***/ }, +/* 64 */ +/***/ function(module, exports, __webpack_require__) { - ordinal : function (number) { - return this._ordinal.replace('%d', number); - }, - _ordinal : '%d', - _ordinalParse : /\d{1,2}/, + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - preparse : function (string) { - return string; - }, + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - postformat : function (string) { - return string; - }, + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - week : function (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - }, + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - _week : { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. - }, + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - firstDayOfWeek : function () { - return this._week.dow; - }, + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - firstDayOfYear : function () { - return this._week.doy; - }, + var CircleImageBase = _interopRequire(__webpack_require__(65)); - _invalidDate: 'Invalid date', - invalidDate: function () { - return this._invalidDate; - } - }); + var Circle = (function (CircleImageBase) { + function Circle(options, body, labelModule) { + _classCallCheck(this, Circle); - /************************************ - Formatting - ************************************/ + _get(Object.getPrototypeOf(Circle.prototype), "constructor", this).call(this, options, body, labelModule); + } + _inherits(Circle, CircleImageBase); - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); + _prototypeProperties(Circle, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; + this.options.size = diameter / 2; + + this.width = diameter; + this.height = diameter; } - return input.replace(/\\/g, ''); - } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; + this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } - } + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; - return function (mom) { - var output = ''; - for (i = 0; i < length; i++) { - output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; - } - return output; - }; + this.labelModule.draw(ctx, x, y, selected); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + }, + writable: true, + configurable: true } + }); - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } + return Circle; + })(CircleImageBase); - format = expandFormat(format, m.localeData()); + module.exports = Circle; - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } +/***/ }, +/* 65 */ +/***/ function(module, exports, __webpack_require__) { - return formatFunctions[format](m); - } + "use strict"; - function expandFormat(format, locale) { - var i = 5; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - return format; - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - /************************************ - Parsing - ************************************/ + /** + * Created by Alex on 3/19/2015. + */ + var NodeBase = _interopRequire(__webpack_require__(63)); + var CircleImageBase = (function (NodeBase) { + function CircleImageBase(options, body, labelModule) { + _classCallCheck(this, CircleImageBase); - // 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; - } - } + _get(Object.getPrototypeOf(CircleImageBase.prototype), "constructor", this).call(this, options, body, labelModule); + } - 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]); + _inherits(CircleImageBase, NodeBase); - return parts[0] === '+' ? minutes : -minutes; - } + _prototypeProperties(CircleImageBase, null, { + _drawRawCircle: { + value: function _drawRawCircle(ctx, x, y, selected, hover, size) { + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - // function to convert string input to date - function addTimeToArrayFromToken(token, input, config) { - var a, datePartArray = config._a; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - 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); - } + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - 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.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.circle(x, y, size); + ctx.fill(); + ctx.stroke(); + }, + writable: true, + configurable: true + }, + _drawImageAtPosition: { + value: function _drawImageAtPosition(ctx) { + if (this.imageObj.width != 0) { + // draw the image + ctx.globalAlpha = 1; + ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); + } + }, + writable: true, + configurable: true + }, + _drawImageLabel: { + value: function _drawImageLabel(ctx, x, y, selected) { + var yLabel; + var offset = 0; + + if (this.height !== undefined) { + offset = this.height * 0.5; + var labelDimensions = this.labelModule.getTextSize(ctx); + + if (labelDimensions.lineCount >= 1) { + offset += labelDimensions.height / 2; + offset += 3; + } } + + yLabel = y + offset; + this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); + }, + writable: true, + configurable: true } + }); - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; + return CircleImageBase; + })(NodeBase); - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; + module.exports = CircleImageBase; - // 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; +/***/ }, +/* 66 */ +/***/ function(module, exports, __webpack_require__) { - weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); - week = dfl(w.w, 1); + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - 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); - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // convert an array to a date. - // the array should mirror the parameters below - // note: all values past the year are optional and will default to the lowest possible value. - // [year, month, day , hour, minute, second, millisecond] - function dateFromConfig(config) { - var i, date, input = [], currentDate, yearToUse; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - if (config._d) { - return; - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - currentDate = currentDateArray(config); + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); + var CircleImageBase = _interopRequire(__webpack_require__(65)); - if (config._dayOfYear > daysInYear(yearToUse)) { - config._pf._overflowDayOfYear = true; - } + var CircularImage = (function (CircleImageBase) { + function CircularImage(options, body, labelModule, imageObj) { + _classCallCheck(this, CircularImage); - date = makeUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } + _get(Object.getPrototypeOf(CircularImage.prototype), "constructor", this).call(this, options, body, labelModule); + this.imageObj = imageObj; + } - // 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]; - } + _inherits(CircularImage, CircleImageBase); - // 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]; + _prototypeProperties(CircularImage, null, { + resize: { + value: function resize(ctx) { + if (this.imageObj.src !== undefined || this.imageObj.width !== undefined || this.imageObj.height !== undefined) { + if (!this.width) { + var diameter = this.options.size * 2; + this.width = diameter; + this.height = diameter; + this._swapToImageResizeWhenImageLoaded = true; + } + } else { + if (this._swapToImageResizeWhenImageLoaded) { + this.width = 0; + this.height = 0; + delete this._swapToImageResizeWhenImageLoaded; + } + this._resizeImage(ctx); } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); - // 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.left = x - this.width / 2; + this.top = y - this.height / 2; - config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); - // Apply timezone offset from input. The actual utcOffset can be changed - // with parseZone. - if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); - } + var size = Math.abs(this.height / 2); + this._drawRawCircle(ctx, x, y, selected, hover, size); - if (config._nextDay) { - config._a[HOUR] = 24; - } - } + ctx.save(); + ctx.circle(x, y, size); + ctx.stroke(); + ctx.clip(); - function dateFromObject(config) { - var normalizedInput; + this._drawImageAtPosition(ctx); - if (config._d) { - return; - } + ctx.restore(); - 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 = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; - dateFromConfig(config); - } + this._drawImageLabel(ctx, x, y, selected); - 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()]; - } + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true } + }); - // date from string and format string - function makeDateFromStringAndFormat(config) { - if (config._f === moment.ISO_8601) { - parseISO(config); - return; - } + return CircularImage; + })(CircleImageBase); - config._a = []; - config._pf.empty = true; + module.exports = CircularImage; - // 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; +/***/ }, +/* 67 */ +/***/ function(module, exports, __webpack_require__) { - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - 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); - } - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // add remaining unparsed input length to the string - config._pf.charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - config._pf.unusedInput.push(string); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // 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); - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - function unescapeFormat(s) { - return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }); - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function regexpEscape(s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // date from string and array of format strings - function makeDateFromStringAndArray(config) { - var tempConfig, - bestMoment, + var NodeBase = _interopRequire(__webpack_require__(63)); - scoreToBeat, - i, - currentScore; + var Database = (function (NodeBase) { + function Database(options, body, labelModule) { + _classCallCheck(this, Database); - if (config._f.length === 0) { - config._pf.invalidFormat = true; - config._d = new Date(NaN); - return; - } + _get(Object.getPrototypeOf(Database.prototype), "constructor", this).call(this, options, body, labelModule); + } - 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); + _inherits(Database, NodeBase); - if (!isValid(tempConfig)) { - continue; - } + _prototypeProperties(Database, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + var size = textSize.width + 2 * margin; + this.width = size; + this.height = size; + } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - // if there is any input that was not parsed add a penalty for that format - currentScore += tempConfig._pf.charsLeftOver; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - //or tokens - currentScore += tempConfig._pf.unusedTokens.length * 10; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; + ctx.lineWidth *= this.networkScaleInv; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - tempConfig._pf.score = currentScore; + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); + ctx.fill(); + ctx.stroke(); - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } - } + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; - extend(config, bestMoment || tempConfig); + this.labelModule.draw(ctx, x, y, selected); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + }, + writable: true, + configurable: true } + }); - // date from iso format - function parseISO(config) { - var i, l, - string = config._i, - match = isoRegex.exec(string); + return Database; + })(NodeBase); - 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; - } - } + module.exports = Database; - // date from iso format or fallback - function makeDateFromString(config) { - parseISO(config); - if (config._isValid === false) { - delete config._isValid; - moment.createFromInputFallback(config); - } - } +/***/ }, +/* 68 */ +/***/ function(module, exports, __webpack_require__) { - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - 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); - } - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - 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); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - function makeUTCDate(y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var ShapeBase = _interopRequire(__webpack_require__(69)); + + var Diamond = (function (ShapeBase) { + function Diamond(options, body, labelModule) { + _classCallCheck(this, Diamond); + + _get(Object.getPrototypeOf(Diamond.prototype), "constructor", this).call(this, options, body, labelModule); + } + + _inherits(Diamond, ShapeBase); + + _prototypeProperties(Diamond, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "diamond", 4, x, y, selected, hover); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true } + }); - /************************************ - Relative Time - ************************************/ + return Diamond; + })(ShapeBase); + module.exports = Diamond; - // 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); - } +/***/ }, +/* 69 */ +/***/ function(module, exports, __webpack_require__) { - 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')), + "use strict"; - args = seconds < relativeTimeThresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < relativeTimeThresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < relativeTimeThresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < relativeTimeThresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < relativeTimeThresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - args[2] = withoutSuffix; - args[3] = +posNegDuration > 0; - args[4] = locale; - return substituteTimeAgo.apply({}, args); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - /************************************ - Week of Year - ************************************/ + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // 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; + /** + * Created by Alex on 3/19/2015. + */ + var NodeBase = _interopRequire(__webpack_require__(63)); + var ShapeBase = (function (NodeBase) { + function ShapeBase(options, body, labelModule) { + _classCallCheck(this, ShapeBase); - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; - } + _get(Object.getPrototypeOf(ShapeBase.prototype), "constructor", this).call(this, options, body, labelModule); + } - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; + _inherits(ShapeBase, NodeBase); + + _prototypeProperties(ShapeBase, null, { + _resizeShape: { + value: function _resizeShape() { + if (this.width === undefined) { + var size = 2 * this.options.size; + this.width = size; + this.height = size; } + }, + writable: true, + configurable: true + }, + _drawShape: { + value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { + this._resizeShape(); - adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; - } + this.left = x - this.width / 2; + this.top = y - this.height / 2; - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - 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.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx[shape](x, y, this.options.size); + ctx.fill(); + ctx.stroke(); - return { - year: dayOfYear > 0 ? year : year - 1, - dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; + this.boundingBox.top = y - this.options.size; + this.boundingBox.left = x - this.options.size; + this.boundingBox.right = x + this.options.size; + this.boundingBox.bottom = y + this.options.size; + + if (this.options.label !== undefined) { + var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. + this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + } + }, + writable: true, + configurable: true } + }); - /************************************ - Top Level Functions - ************************************/ + return ShapeBase; + })(NodeBase); - function makeMoment(config) { - var input = config._i, - format = config._f, - res; + module.exports = ShapeBase; - config._locale = config._locale || moment.localeData(config._l); +/***/ }, +/* 70 */ +/***/ function(module, exports, __webpack_require__) { - if (input === null || (format === undefined && input === '')) { - return moment.invalid({nullInput: true}); - } + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - if (typeof input === 'string') { - config._i = input = config._locale.preparse(input); - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - if (moment.isMoment(input)) { - return new Moment(input, true); - } else if (format) { - if (isArray(format)) { - makeDateFromStringAndArray(config); - } else { - makeDateFromStringAndFormat(config); - } - } else { - makeDateFromInput(config); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - res = new Moment(config); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - return res; - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - moment = function (input, format, locale, strict) { - var c; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - if (typeof(locale) === 'boolean') { - strict = locale; - locale = undefined; - } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c = {}; - c._isAMomentObject = true; - c._i = input; - c._f = format; - c._l = locale; - c._strict = strict; - c._isUTC = false; - c._pf = defaultParsingFlags(); + var ShapeBase = _interopRequire(__webpack_require__(69)); - return makeMoment(c); - }; + var Dot = (function (ShapeBase) { + function Dot(options, body, labelModule) { + _classCallCheck(this, Dot); - moment.suppressDeprecationWarnings = false; + _get(Object.getPrototypeOf(Dot.prototype), "constructor", this).call(this, options, body, labelModule); + } - 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' : '')); - } - ); + _inherits(Dot, ShapeBase); - // 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; + _prototypeProperties(Dot, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "circle", 2, x, y, selected, hover); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this.options.size + this.options.borderWidth; + }, + writable: true, + configurable: true } + }); - moment.min = function () { - var args = [].slice.call(arguments, 0); - - return pickBy('isBefore', args); - }; - - moment.max = function () { - var args = [].slice.call(arguments, 0); - - return pickBy('isAfter', args); - }; + return Dot; + })(ShapeBase); - // creating with utc - moment.utc = function (input, format, locale, strict) { - var c; + module.exports = Dot; - 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(); +/***/ }, +/* 71 */ +/***/ function(module, exports, __webpack_require__) { - return makeMoment(c).utc(); - }; + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); - }; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // 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 _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - if (moment.isDuration(input)) { - duration = { - ms: input._milliseconds, - d: input._days, - M: input._months - }; - } else if (typeof input === 'number') { - duration = {}; - if (key) { - duration[key] = input; - } else { - duration.milliseconds = input; - } - } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y: 0, - d: toInt(match[DATE]) * sign, - h: toInt(match[HOUR]) * sign, - m: toInt(match[MINUTE]) * sign, - s: toInt(match[SECOND]) * sign, - ms: toInt(match[MILLISECOND]) * sign - }; - } else if (!!(match = isoDurationRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - parseIso = function (inp) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; - }; - duration = { - y: parseIso(match[2]), - M: parseIso(match[3]), - d: parseIso(match[4]), - h: parseIso(match[5]), - m: parseIso(match[6]), - s: parseIso(match[7]), - w: parseIso(match[8]) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && - ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(moment(duration.from), moment(duration.to)); + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - ret = new Duration(duration); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - if (moment.isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; - } + var NodeBase = _interopRequire(__webpack_require__(63)); - return ret; - }; + var Ellipse = (function (NodeBase) { + function Ellipse(options, body, labelModule) { + _classCallCheck(this, Ellipse); - // version number - moment.version = VERSION; + _get(Object.getPrototypeOf(Ellipse.prototype), "constructor", this).call(this, options, body, labelModule); + } - // default format - moment.defaultFormat = isoFormat; + _inherits(Ellipse, NodeBase); - // constant that refers to the ISO standard - moment.ISO_8601 = function () {}; + _prototypeProperties(Ellipse, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var textSize = this.labelModule.getTextSize(ctx, selected); - // Plugins that add properties should also add the key here (null value), - // so we can properly clone ourselves. - moment.momentProperties = momentProperties; + this.width = textSize.width * 1.5; + this.height = textSize.height * 2; + if (this.width < this.height) { + this.width = this.height; + } + } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - // 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 () {}; + var borderWidth = this.options.borderWidth; + var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - // 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; - }; + ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - moment.lang = deprecate( - 'moment.lang is deprecated. Use moment.locale instead.', - function (key, value) { - return moment.locale(key, value); - } - ); + ctx.lineWidth = selected ? selectionLineWidth : borderWidth; + ctx.lineWidth /= this.body.view.scale; + ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - // 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); - } + ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + ctx.ellipse(this.left, this.top, this.width, this.height); + ctx.fill(); + ctx.stroke(); - if (data) { - moment.duration._locale = moment._locale = data; - } - } + this.boundingBox.left = this.left; + this.boundingBox.top = this.top; + this.boundingBox.bottom = this.top + this.height; + this.boundingBox.right = this.left + this.width; - return moment._locale._abbr; - }; - moment.defineLocale = function (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + this.labelModule.draw(ctx, x, y, selected); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + }, + writable: true, + configurable: true + } + }); - // backwards compat for now: also set the locale - moment.locale(name); + return Ellipse; + })(NodeBase); - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } - }; + module.exports = Ellipse; - moment.langData = deprecate( - 'moment.langData is deprecated. Use moment.localeData instead.', - function (key) { - return moment.localeData(key); - } - ); +/***/ }, +/* 72 */ +/***/ function(module, exports, __webpack_require__) { - // returns locale data - moment.localeData = function (key) { - var locale; + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - if (!key) { - return moment._locale; - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - return chooseLocale(key); - }; + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - // compare moment object - moment.isMoment = function (obj) { - return obj instanceof Moment || - (obj != null && hasOwnProp(obj, '_isAMomentObject')); - }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - // for typechecking Duration objects - moment.isDuration = function (obj) { - return obj instanceof Duration; - }; + var NodeBase = _interopRequire(__webpack_require__(63)); - for (i = lists.length - 1; i >= 0; --i) { - makeList(lists[i]); - } + var Icon = (function (NodeBase) { + function Icon(options, body, labelModule) { + _classCallCheck(this, Icon); - moment.normalizeUnits = function (units) { - return normalizeUnits(units); - }; + _get(Object.getPrototypeOf(Icon.prototype), "constructor", this).call(this, options, body, labelModule); + } - moment.invalid = function (flags) { - var m = moment.utc(NaN); - if (flags != null) { - extend(m._pf, flags); - } - else { - m._pf.userInvalidated = true; + _inherits(Icon, NodeBase); + + _prototypeProperties(Icon, null, { + resize: { + value: function resize(ctx) { + if (this.width === undefined) { + var margin = 5; + var iconSize = { + width: Number(this.options.icon.size), + height: Number(this.options.icon.size) + }; + this.width = iconSize.width + 2 * margin; + this.height = iconSize.height + 2 * margin; } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.options.icon.size = this.options.icon.size || 50; - return m; - }; + this.left = x - this.width * 0.5; + this.top = y - this.height * 0.5; + this._icon(ctx, x, y, selected); - moment.parseZone = function () { - return moment.apply(null, arguments).parseZone(); - }; - moment.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; + this.boundingBox.top = y - this.options.icon.size * 0.5; + this.boundingBox.left = x - this.options.icon.size * 0.5; + this.boundingBox.right = x + this.options.icon.size * 0.5; + this.boundingBox.bottom = y + this.options.icon.size * 0.5; - moment.isDate = isDate; + if (this.options.label !== undefined) { + var iconTextSpacing = 5; + this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + } + }, + writable: true, + configurable: true + }, + _icon: { + value: function _icon(ctx, x, y, selected) { + var iconSize = Number(this.options.icon.size); + var relativeIconSize = iconSize * this.body.view.scale; - /************************************ - Moment Prototype - ************************************/ + if (this.options.icon.code && relativeIconSize > this.options.scaling.label.drawThreshold - 1) { + ctx.font = (selected ? "bold " : "") + iconSize + "px " + this.options.icon.face; + // draw icon + ctx.fillStyle = this.options.icon.color || "black"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(this.options.icon.code, x, y); + } + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + this._distanceToBorder(angle); + }, + writable: true, + configurable: true + } + }); - extend(moment.fn = Moment.prototype, { + return Icon; + })(NodeBase); - clone : function () { - return moment(this); - }, + module.exports = Icon; - valueOf : function () { - return +this._d - ((this._offset || 0) * 60000); - }, +/***/ }, +/* 73 */ +/***/ function(module, exports, __webpack_require__) { - unix : function () { - return Math.floor(+this / 1000); - }, + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - toString : function () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); - }, + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - toDate : function () { - return this._offset ? new Date(+this) : this._d; - }, + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - 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]'); - } - }, + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - toArray : function () { - var m = this; - return [ - m.year(), - m.month(), - m.date(), - m.hours(), - m.minutes(), - m.seconds(), - m.milliseconds() - ]; - }, + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - isValid : function () { - return isValid(this); - }, + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - isDSTShifted : function () { - if (this._a) { - return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; - } + var CircleImageBase = _interopRequire(__webpack_require__(65)); - return false; - }, + var Image = (function (CircleImageBase) { + function Image(options, body, labelModule, imageObj) { + _classCallCheck(this, Image); - parsingFlags : function () { - return extend({}, this._pf); - }, + _get(Object.getPrototypeOf(Image.prototype), "constructor", this).call(this, options, body, labelModule); + this.imageObj = imageObj; + } - invalidAt: function () { - return this._pf.overflow; - }, + _inherits(Image, CircleImageBase); - utc : function (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - }, + _prototypeProperties(Image, null, { + resize: { + value: function resize() { + if (!this.width || !this.height) { + // undefined or 0 + var width, height; + if (this.value) { + var scale = this.imageObj.height / this.imageObj.width; + if (scale !== undefined) { + width = this.options.size || this.imageObj.width; + height = this.options.size * scale || this.imageObj.height; + } else { + width = 0; + height = 0; + } + } else { + width = this.imageObj.width; + height = this.imageObj.height; + } + this.width = width; + this.height = height; + } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - local : function (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; + this._drawImageAtPosition(ctx); - if (keepLocalTime) { - this.subtract(this._dateUtcOffset(), 'm'); - } - } - return 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; - format : function (inputString) { - var output = formatMoment(this, inputString || moment.defaultFormat); - return this.localeData().postformat(output); - }, + this._drawImageLabel(ctx, x, y, selected || hover); + this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); + this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); + this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + var a = this.width / 2; + var b = this.height / 2; + var w = Math.sin(angle) * a; + var h = Math.cos(angle) * b; + return a * b / Math.sqrt(w * w + h * h); + }, + writable: true, + configurable: true + } + }); - add : createAdder(1, 'add'), + return Image; + })(CircleImageBase); - subtract : createAdder(-1, 'subtract'), + module.exports = Image; - diff : function (input, units, asFloat) { - var that = makeAs(input, this), - zoneDiff = (that.utcOffset() - this.utcOffset()) * 6e4, - anchor, diff, output, daysAdjust; +/***/ }, +/* 74 */ +/***/ function(module, exports, __webpack_require__) { - units = normalizeUnits(units); + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - 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); - }, + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - from : function (time, withoutSuffix) { - return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - }, + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - fromNow : function (withoutSuffix) { - return this.from(moment(), withoutSuffix); - }, + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - 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))); - }, + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - isLeapYear : function () { - return isLeapYear(this.year()); - }, + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - isDST : function () { - return (this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset()); - }, + var ShapeBase = _interopRequire(__webpack_require__(69)); - 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; - } - }, + var Square = (function (ShapeBase) { + function Square(options, body, labelModule) { + _classCallCheck(this, Square); - month : makeAccessor('Month', true), + _get(Object.getPrototypeOf(Square.prototype), "constructor", this).call(this, options, body, labelModule); + } - 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 */ - } + _inherits(Square, ShapeBase); - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } + _prototypeProperties(Square, null, { + resize: { + value: function resize() { + this._resizeShape(); + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "square", 2, x, y, selected, hover); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true + } + }); - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } + return Square; + })(ShapeBase); - return this; - }, + module.exports = Square; - 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'); - }, +/***/ }, +/* 75 */ +/***/ function(module, exports, __webpack_require__) { - 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); - } - }, + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - isBefore: function (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = moment.isMoment(input) ? input : moment(input); - return +this < +input; - } else { - inputMs = moment.isMoment(input) ? +input : +moment(input); - return +this.clone().endOf(units) < inputMs; - } - }, + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - isBetween: function (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - }, + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - 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)); - } - }, + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - min: deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function (other) { - other = moment.apply(null, arguments); - return other < this ? this : other; - } - ), + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - 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; - } - ), + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - 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; - } + var ShapeBase = _interopRequire(__webpack_require__(69)); - this.utcOffset(input, keepLocalTime); + var Star = (function (ShapeBase) { + function Star(options, body, labelModule) { + _classCallCheck(this, Star); - return this; - } else { - return -this.utcOffset(); - } - } - ), + _get(Object.getPrototypeOf(Star.prototype), "constructor", this).call(this, options, body, labelModule); + } - // 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; - } - } + _inherits(Star, ShapeBase); - return this; - } else { - return this._isUTC ? offset : this._dateUtcOffset(); - } - }, + _prototypeProperties(Star, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "star", 4, x, y, selected, hover); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true + } + }); - isLocal : function () { - return !this._isUTC; - }, + return Star; + })(ShapeBase); - isUtcOffset : function () { - return this._isUTC; - }, + module.exports = Star; - isUtc : function () { - return this._isUTC && this._offset === 0; - }, +/***/ }, +/* 76 */ +/***/ function(module, exports, __webpack_require__) { - zoneAbbr : function () { - return this._isUTC ? 'UTC' : ''; - }, + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - zoneName : function () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - }, + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - parseZone : function () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(utcOffsetFromString(this._i)); - } - return this; - }, + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - hasAlignedHourOffset : function (input) { - if (!input) { - input = 0; - } - else { - input = moment(input).utcOffset(); - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - return (this.utcOffset() - input) % 60 === 0; - }, + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - daysInMonth : function () { - return daysInMonth(this.year(), this.month()); - }, + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - 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'); - }, + var NodeBase = _interopRequire(__webpack_require__(63)); - quarter : function (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - }, + var Text = (function (NodeBase) { + function Text(options, body, labelModule) { + _classCallCheck(this, Text); - 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'); - }, + _get(Object.getPrototypeOf(Text.prototype), "constructor", this).call(this, options, body, labelModule); + } - isoWeekYear : function (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - }, + _inherits(Text, NodeBase); - week : function (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - }, + _prototypeProperties(Text, null, { + resize: { + value: function resize(ctx, selected) { + if (this.width === undefined) { + var margin = 5; + var textSize = this.labelModule.getTextSize(ctx, selected); + this.width = textSize.width + 2 * margin; + this.height = textSize.height + 2 * margin; + } + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this.resize(ctx, selected || hover); + this.left = x - this.width / 2; + this.top = y - this.height / 2; - isoWeek : function (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - }, + this.labelModule.draw(ctx, x, y, selected || hover); - weekday : function (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - }, + this.boundingBox.top = this.top; + this.boundingBox.left = this.left; + this.boundingBox.right = this.left + this.width; + this.boundingBox.bottom = this.top + this.height; + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + this.resize(ctx); + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true + } + }); - isoWeekday : function (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); - }, + return Text; + })(NodeBase); - isoWeeksInYear : function () { - return weeksInYear(this.year(), 1, 4); - }, + module.exports = Text; - weeksInYear : function () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - }, +/***/ }, +/* 77 */ +/***/ function(module, exports, __webpack_require__) { - get : function (units) { - units = normalizeUnits(units); - return this[units](); - }, + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - set : function (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } - } - else { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - this[units](value); - } - } - return this; - }, + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // If passed a locale key, it will set the locale for this - // instance. Otherwise, it will return the locale configuration - // variables for this instance. - locale : function (key) { - var newLocaleData; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = moment.localeData(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } - }, + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - lang : deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - return this.locale(key); - } - } - ), + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - localeData : function () { - return this._locale; - }, + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - _dateUtcOffset : function () { - // On Firefox.24 Date#getTimezoneOffset returns a floating point. - // https://github.com/moment/moment/pull/1871 - return -Math.round(this._d.getTimezoneOffset() / 15) * 15; - } + var ShapeBase = _interopRequire(__webpack_require__(69)); - }); + var Triangle = (function (ShapeBase) { + function Triangle(options, body, labelModule) { + _classCallCheck(this, Triangle); - function rawMonthSetter(mom, value) { - var dayOfMonth; + _get(Object.getPrototypeOf(Triangle.prototype), "constructor", this).call(this, options, body, labelModule); + } - // 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; - } - } + _inherits(Triangle, ShapeBase); - dayOfMonth = Math.min(mom.date(), - daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; + _prototypeProperties(Triangle, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "triangle", 3, x, y, selected, hover); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true } + }); - function rawGetter(mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } + return Triangle; + })(ShapeBase); - function rawSetter(mom, unit, value) { - if (unit === 'Month') { - return rawMonthSetter(mom, value); - } else { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } - } + module.exports = Triangle; - 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); - } - }; - } +/***/ }, +/* 78 */ +/***/ function(module, exports, __webpack_require__) { - 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)); + /** + * Created by Alex on 3/18/2015. + */ + "use strict"; - // 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; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // add aliased format methods - moment.fn.toJSON = moment.fn.toISOString; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // alias isUtc for dev-friendliness - moment.fn.isUTC = moment.fn.isUtc; + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - /************************************ - Duration Prototype - ************************************/ + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; - } + var ShapeBase = _interopRequire(__webpack_require__(69)); - function yearsToDays (years) { - // years * 365 + absRound(years / 4) - - // absRound(years / 100) + absRound(years / 400); - return years * 146097 / 400; - } + var TriangleDown = (function (ShapeBase) { + function TriangleDown(options, body, labelModule) { + _classCallCheck(this, TriangleDown); - extend(moment.duration.fn = Duration.prototype, { + _get(Object.getPrototypeOf(TriangleDown.prototype), "constructor", this).call(this, options, body, labelModule); + } - _bubble : function () { - var milliseconds = this._milliseconds, - days = this._days, - months = this._months, - data = this._data, - seconds, minutes, hours, years = 0; + _inherits(TriangleDown, ShapeBase); - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + _prototypeProperties(TriangleDown, null, { + resize: { + value: function resize(ctx) { + this._resizeShape(); + }, + writable: true, + configurable: true + }, + draw: { + value: function draw(ctx, x, y, selected, hover) { + this._drawShape(ctx, "triangleDown", 3, x, y, selected, hover); + }, + writable: true, + configurable: true + }, + distanceToBorder: { + value: function distanceToBorder(ctx, angle) { + return this._distanceToBorder(angle); + }, + writable: true, + configurable: true + } + }); - seconds = absRound(milliseconds / 1000); - data.seconds = seconds % 60; + return TriangleDown; + })(ShapeBase); - minutes = absRound(seconds / 60); - data.minutes = minutes % 60; + module.exports = TriangleDown; - hours = absRound(minutes / 60); - data.hours = hours % 24; +/***/ }, +/* 79 */ +/***/ function(module, exports, __webpack_require__) { - days += absRound(hours / 24); + "use strict"; - // Accurately convert days to years, assume start from year 0. - years = absRound(daysToYears(days)); - days -= absRound(yearsToDays(years)); + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absRound(days / 30); - days %= 30; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - // 12 months -> 1 year - years += absRound(months / 12); - months %= 12; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - data.days = days; - data.months = months; - data.years = years; - }, + /** + * Created by Alex on 3/4/2015. + */ - abs : function () { - this._milliseconds = Math.abs(this._milliseconds); - this._days = Math.abs(this._days); - this._months = Math.abs(this._months); - this._data.milliseconds = Math.abs(this._data.milliseconds); - this._data.seconds = Math.abs(this._data.seconds); - this._data.minutes = Math.abs(this._data.minutes); - this._data.hours = Math.abs(this._data.hours); - this._data.months = Math.abs(this._data.months); - this._data.years = Math.abs(this._data.years); + var util = __webpack_require__(1); + var DataSet = __webpack_require__(7); + var DataView = __webpack_require__(9); - return this; - }, + var Edge = _interopRequire(__webpack_require__(80)); - weeks : function () { - return absRound(this.days() / 7); - }, + var EdgesHandler = (function () { + function EdgesHandler(body, images, groups) { + var _this = this; + _classCallCheck(this, EdgesHandler); - valueOf : function () { - return this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6; - }, + this.body = body; + this.images = images; + this.groups = groups; - humanize : function (withSuffix) { - var output = relativeTime(this, !withSuffix, this.localeData()); + // create the edge API in the body container + this.body.functions.createEdge = this.create.bind(this); - if (withSuffix) { - output = this.localeData().pastFuture(+this, output); - } + this.edgesListeners = { + add: function (event, params) { + _this.add(params.items); + }, + update: function (event, params) { + _this.update(params.items); + }, + remove: function (event, params) { + _this.remove(params.items); + } + }; - return this.localeData().postformat(output); + this.options = {}; + this.defaultOptions = { + arrows: { + to: { enabled: false, scaleFactor: 1 }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: { enabled: false, scaleFactor: 1 }, + from: { enabled: false, scaleFactor: 1 } + }, + color: { + color: "#848484", + highlight: "#848484", + hover: "#848484", + inherit: { + enabled: true, + source: "from", // from / true + useGradients: false // release in 4.0 }, - - add : function (input, val) { - // supports only 2.0-style add(1, 's') or add(moment) - var dur = moment.duration(input, val); - - this._milliseconds += dur._milliseconds; - this._days += dur._days; - this._months += dur._months; - - this._bubble(); - - return this; + opacity: 1 + }, + dashes: { + enabled: false, + preset: "dotted", + length: 10, + gap: 5, + altLength: undefined + }, + font: { + color: "#343434", + size: 14, // px + face: "arial", + background: "none", + stroke: 1, // px + strokeColor: "white", + align: "horizontal" + }, + hidden: false, + hoverWidth: 1.5, + label: undefined, + length: undefined, + physics: true, + scaling: { + min: 1, + max: 15, + label: { + enabled: true, + min: 14, + max: 30, + maxVisible: 30, + drawThreshold: 3 }, + customScalingFunction: function (min, max, total, value) { + if (max == min) { + return 0.5; + } else { + var scale = 1 / (max - min); + return Math.max(0, (value - min) * scale); + } + } + }, + selfReferenceSize: 20, + smooth: { + enabled: true, + dynamic: true, + type: "continuous", + roundness: 0.5 + }, + title: undefined, + width: 1, + widthSelectionMultiplier: 2, + value: 1 + }; - subtract : function (input, val) { - var dur = moment.duration(input, val); - - this._milliseconds -= dur._milliseconds; - this._days -= dur._days; - this._months -= dur._months; + util.extend(this.options, this.defaultOptions); - this._bubble(); - return this; - }, + // this allows external modules to force all dynamic curves to turn static. + this.body.emitter.on("_forceDisableDynamicCurves", function (type) { + var emitChange = false; + for (var edgeId in _this.body.edges) { + if (_this.body.edges.hasOwnProperty(edgeId)) { + var edgeOptions = _this.body.edges[edgeId].options.smooth; + if (edgeOptions.enabled === true && edgeOptions.dynamic === true) { + if (type === undefined) { + edge.setOptions({ smooth: false }); + } else { + edge.setOptions({ smooth: { dynamic: false, type: type } }); + } + emitChange = true; + } + } + } + if (emitChange === true) { + _this.body.emitter.emit("_dataChanged"); + } + }); - get : function (units) { - units = normalizeUnits(units); - return this[units.toLowerCase() + 's'](); - }, + // this is called when options of EXISTING nodes or edges have changed. + this.body.emitter.on("_dataUpdated", function () { + _this.reconnectEdges(); + _this.markAllEdgesAsDirty(); + }); + } - as : function (units) { - var days, months; - units = normalizeUnits(units); + _prototypeProperties(EdgesHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.mergeOptions(this.options, options, "smooth"); + util.mergeOptions(this.options, options, "dashes"); - if (units === 'month' || units === 'year') { - days = this._days + this._milliseconds / 864e5; - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; + // hanlde multiple input cases for arrows + if (options.arrows !== undefined) { + if (typeof options.arrows === "string") { + var arrows = options.arrows.toLowerCase(); + if (arrows.indexOf("to") != -1) { + this.options.arrows.to.enabled = true; + } + if (arrows.indexOf("middle") != -1) { + this.options.arrows.middle.enabled = true; + } + if (arrows.indexOf("from") != -1) { + this.options.arrows.from.enabled = true; + } + } else if (typeof options.arrows === "object") { + util.mergeOptions(this.options.arrows, options.arrows, "to"); + util.mergeOptions(this.options.arrows, options.arrows, "middle"); + util.mergeOptions(this.options.arrows, options.arrows, "from"); } else { - // 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); - } + throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); } - }, - - lang : moment.fn.lang, - locale : moment.fn.locale, + } - toIsoString : deprecate( - 'toIsoString() is deprecated. Please use toISOString() instead ' + - '(notice the capitals)', - function () { - return this.toISOString(); + // hanlde multiple input cases for color + if (options.color !== undefined) { + if (util.isString(options.color)) { + util.assignAllKeys(this.options.color, options.color); + this.options.color.inherit.enabled = false; + } else { + util.extend(this.options.color, options.color); + if (options.color.inherit === undefined) { + this.options.color.inherit.enabled = false; + } } - ), + util.mergeOptions(this.options.color, options.color, "inherit"); + } - 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); + // font cases are handled by the Label class + } + }, + writable: true, + configurable: true + }, + setData: { - 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' : ''); - }, + /** + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private + * @private + */ + value: function setData(edges) { + var _this = this; + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var oldEdgesData = this.body.data.edges; - localeData : function () { - return this._locale; - }, + 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"); + } - toJSON : function () { - return this.toISOString(); + // TODO: is this null or undefined or false? + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.off(event, callback); + }); } - }); - moment.duration.fn.toString = moment.duration.fn.toISOString; + // remove drawn edges + this.body.edges = {}; - function makeDurationGetter(name) { - moment.duration.fn[name] = function () { - return this._data[name]; - }; - } + // TODO: is this null or undefined or false? + if (this.body.data.edges) { + // subscribe to new dataset + util.forEach(this.edgesListeners, function (callback, event) { + _this.body.data.edges.on(event, callback); + }); - for (i in unitMillisecondFactors) { - if (hasOwnProp(unitMillisecondFactors, i)) { - makeDurationGetter(i.toLowerCase()); + // draw all new nodes + var ids = this.body.data.edges.getIds(); + this.add(ids, true); } - } - - 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 (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + add: { - // 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; - } - }); + /** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ + value: function add(ids) { + var doNotEmit = arguments[1] === undefined ? false : arguments[1]; + var edges = this.body.edges; + var edgesData = this.body.data.edges; - /* EMBED_LOCALES */ + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; - /************************************ - Exposing Moment - ************************************/ + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } - 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; + var data = edgesData.get(id, { showInternalIds: true }); + edges[id] = this.create(data); } - } - // 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; - } + if (doNotEmit === false) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + update: { - return moment; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - makeGlobal(true); - } else { - makeGlobal(); - } - }).call(this); - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()), __webpack_require__(98)(module))) -/***/ }, -/* 64 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ + value: function update(ids) { + var edges = this.body.edges; + var edgesData = this.body.data.edges; + var dataChanged = false; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var data = edgesData.get(id); + var edge = edges[id]; + if (edge === null) { + // update edge + edge.disconnect(); + dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed. + edge.connect(); + } else { + // create edge + this.body.edges[id] = this.create(data); + dataChanged = true; + } + } - var __WEBPACK_AMD_DEFINE_RESULT__;/*! Hammer.JS - v2.0.4 - 2014-09-28 - * http://hammerjs.github.io/ - * - * Copyright (c) 2014 Jorik Tangelder; - * Licensed under the MIT license */ - (function(window, document, exportName, undefined) { - 'use strict'; + if (dataChanged === true) { + this.body.emitter.emit("_dataChanged"); + } else { + this.body.emitter.emit("_dataUpdated"); + } + }, + writable: true, + configurable: true + }, + remove: { - var VENDOR_PREFIXES = ['', 'webkit', 'moz', 'MS', 'ms', 'o']; - var TEST_ELEMENT = document.createElement('div'); - var TYPE_FUNCTION = 'function'; - var round = Math.round; - var abs = Math.abs; - var now = Date.now; + /** + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids + * @private + */ + value: function remove(ids) { + var edges = this.body.edges; + for (var i = 0; i < ids.length; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge !== undefined) { + if (edge.via != null) { + delete this.body.supportNodes[edge.via.id]; + } + edge.disconnect(); + delete edges[id]; + } + } - /** - * 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); - } + this.body.emitter.emit("_dataChanged"); + }, + writable: true, + configurable: true + }, + create: { + value: function create(properties) { + return new Edge(properties, this.body, this.options); + }, + writable: true, + configurable: true + }, + markAllEdgesAsDirty: { + value: function markAllEdgesAsDirty() { + for (var edgeId in this.body.edges) { + this.body.edges[edgeId].colorDirty = true; + } + }, + writable: true, + configurable: true + }, + reconnectEdges: { - /** - * if the argument is an array, we want to execute the fn on each entry - * if it aint an array we don't want to do a thing. - * this is used by all the methods that accept a single and array argument. - * @param {*|Array} arg - * @param {String} fn - * @param {Object} [context] - * @returns {Boolean} - */ - function invokeArrayArg(arg, fn, context) { - if (Array.isArray(arg)) { - each(arg, context[fn], context); - return true; - } - return false; - } - /** - * walk objects and arrays - * @param {Object} obj - * @param {Function} iterator - * @param {Object} context - */ - function each(obj, iterator, context) { - var i; - if (!obj) { - return; - } + /** + * Reconnect all edges + * @private + */ + value: function reconnectEdges() { + var id; + var nodes = this.body.nodes; + var edges = this.body.edges; - 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 (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } } - } - } - /** - * extend object. - * means that properties in dest will be overwritten by the ones in src. - * @param {Object} dest - * @param {Object} src - * @param {Boolean} [merge] - * @returns {Object} dest - */ - function extend(dest, src, merge) { - var keys = Object.keys(src); - var i = 0; - while (i < keys.length) { - if (!merge || (merge && dest[keys[i]] === undefined)) { - dest[keys[i]] = src[keys[i]]; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } } - i++; + }, + writable: true, + configurable: true } - return dest; - } - - /** - * merge the values from src in the dest. - * means that properties that exist in dest will not be overwritten by src - * @param {Object} dest - * @param {Object} src - * @returns {Object} dest - */ - function merge(dest, src) { - return extend(dest, src, true); - } + }); - /** - * simple class inheritance - * @param {Function} child - * @param {Function} base - * @param {Object} [properties] - */ - function inherit(child, base, properties) { - var baseP = base.prototype, - childP; + return EdgesHandler; + })(); - childP = child.prototype = Object.create(baseP); - childP.constructor = child; - childP._super = baseP; + module.exports = EdgesHandler; - if (properties) { - extend(childP, properties); - } - } +/***/ }, +/* 80 */ +/***/ function(module, exports, __webpack_require__) { - /** - * simple function bind - * @param {Function} fn - * @param {Object} context - * @returns {Function} - */ - function bindFn(fn, context) { - return function boundFn() { - return fn.apply(context, arguments); - }; - } + "use strict"; - /** - * let a boolean value also be a function that must return a boolean - * this first item in args will be used as the context - * @param {Boolean|Function} val - * @param {Array} [args] - * @returns {Boolean} - */ - function boolOrFn(val, args) { - if (typeof val == TYPE_FUNCTION) { - return val.apply(args ? args[0] || undefined : undefined, args); - } - return val; - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - /** - * use the val2 when val1 is undefined - * @param {*} val1 - * @param {*} val2 - * @returns {*} - */ - function ifUndefined(val1, val2) { - return (val1 === undefined) ? val2 : val1; - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * 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); - }); - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - /** - * removeEventListener with multiple events at once - * @param {EventTarget} target - * @param {String} types - * @param {Function} handler - */ - function removeEventListeners(target, types, handler) { - each(splitStr(types), function(type) { - target.removeEventListener(type, handler, false); - }); - } + var util = __webpack_require__(1); - /** - * find if a node is in the given parent - * @method hasParent - * @param {HTMLElement} node - * @param {HTMLElement} parent - * @return {Boolean} found - */ - function hasParent(node, parent) { - while (node) { - if (node == parent) { - return true; - } - node = node.parentNode; - } - return false; - } - /** - * small indexOf wrapper - * @param {String} str - * @param {String} find - * @returns {Boolean} found - */ - function inStr(str, find) { - return str.indexOf(find) > -1; - } + var Label = _interopRequire(__webpack_require__(61)); - /** - * split string on whitespace - * @param {String} str - * @returns {Array} words - */ - function splitStr(str) { - return str.trim().split(/\s+/g); - } + var BezierEdgeDynamic = _interopRequire(__webpack_require__(81)); - /** - * find if a array contains the object using indexOf or a simple polyFill - * @param {Array} src - * @param {String} find - * @param {String} [findByKey] - * @return {Boolean|Number} false when not found, or the index - */ - function inArray(src, find, findByKey) { - if (src.indexOf && !findByKey) { - return src.indexOf(find); - } else { - var i = 0; - while (i < src.length) { - if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) { - return i; - } - i++; - } - return -1; - } - } + var BezierEdgeStatic = _interopRequire(__webpack_require__(84)); - /** - * convert array-like objects to real arrays - * @param {Object} obj - * @returns {Array} - */ - function toArray(obj) { - return Array.prototype.slice.call(obj, 0); - } + var StraightEdge = _interopRequire(__webpack_require__(85)); /** - * 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}] + * @class Edge + * + * A edge connects two nodes + * @param {Object} properties Object with options. Must contain + * At least options from and to. + * Available options: from (number), + * to (number), label (string, color (string), + * width (number), style (string), + * length (number), title (string) + * @param {Network} network A Network object, used to find and edge to + * nodes. + * @param {Object} constants An object with default values for + * example for the color */ - function uniqueArray(src, key, sort) { - var results = []; - var values = []; - var i = 0; + var Edge = (function () { + function Edge(options, body, globalOptions) { + _classCallCheck(this, Edge); - while (i < src.length) { - var val = key ? src[i][key] : src[i]; - if (inArray(values, val) < 0) { - results.push(src[i]); - } - values[i] = val; - i++; + if (body === undefined) { + throw "No body provided"; } + this.options = util.bridgeObject(globalOptions); + this.body = body; - if (sort) { - if (!key) { - results = results.sort(); - } else { - results = results.sort(function sortUniqueArray(a, b) { - return a[key] > b[key]; - }); - } - } + // initialize variables + this.id = undefined; + this.fromId = undefined; + this.toId = undefined; + this.value = undefined; + this.selected = false; + this.hover = false; + this.labelDirty = true; + this.colorDirty = true; - return results; - } + this.from = undefined; // a node + this.to = undefined; // a node - /** - * get the prefixed property - * @param {Object} obj - * @param {String} property - * @returns {String|Undefined} prefixed - */ - function prefixed(obj, property) { - var prefix, prop; - var camelProp = property[0].toUpperCase() + property.slice(1); + this.edgeType = undefined; - var i = 0; - while (i < VENDOR_PREFIXES.length) { - prefix = VENDOR_PREFIXES[i]; - prop = (prefix) ? prefix + camelProp : property; + this.connected = false; - if (prop in obj) { - return prop; - } - i++; - } - return undefined; - } + this.labelModule = new Label(this.body, this.options); - /** - * get a unique id - * @returns {number} uniqueId - */ - var _uniqueId = 1; - function uniqueId() { - return _uniqueId++; - } + this.setOptions(options); - /** - * get the window object of an element - * @param {HTMLElement} element - * @returns {DocumentView|Window} - */ - function getWindowForElement(element) { - var doc = element.ownerDocument; - return (doc.defaultView || doc.parentWindow); - } + this.controlNodesEnabled = false; + this.controlNodes = { from: undefined, to: undefined, positions: {} }; + this.connectedNode = undefined; + } - var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; + _prototypeProperties(Edge, null, { + setOptions: { - 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'; + /** + * Set or overwrite options for the edge + * @param {Object} options an object with options + * @param doNotEmit + */ + value: function setOptions(options) { + if (!options) { + return; + } + this.colorDirty = true; - var COMPUTE_INTERVAL = 25; + var fields = ["id", "font", "from", "hidden", "hoverWidth", "label", "length", "line", "opacity", "physics", "scaling", "selfReferenceSize", "to", "title", "value", "width", "widthMin", "widthMax", "widthSelectionMultiplier"]; + util.selectiveDeepExtend(fields, this.options, options); - var INPUT_START = 1; - var INPUT_MOVE = 2; - var INPUT_END = 4; - var INPUT_CANCEL = 8; + util.mergeOptions(this.options, options, "smooth"); + util.mergeOptions(this.options, options, "dashes"); - var DIRECTION_NONE = 1; - var DIRECTION_LEFT = 2; - var DIRECTION_RIGHT = 4; - var DIRECTION_UP = 8; - var DIRECTION_DOWN = 16; + if (options.id !== undefined) { + this.id = options.id; + } + if (options.from !== undefined) { + this.fromId = options.from; + } + if (options.to !== undefined) { + this.toId = options.to; + } + if (options.title !== undefined) { + this.title = options.title; + } + if (options.value !== undefined) { + this.value = options.value; + } - var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; - var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; - var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + // hanlde multiple input cases for arrows + if (options.arrows !== undefined) { + if (typeof options.arrows === "string") { + var arrows = options.arrows.toLowerCase(); + if (arrows.indexOf("to") != -1) { + this.options.arrows.to.enabled = true; + } + if (arrows.indexOf("middle") != -1) { + this.options.arrows.middle.enabled = true; + } + if (arrows.indexOf("from") != -1) { + this.options.arrows.from.enabled = true; + } + } else if (typeof options.arrows === "object") { + util.mergeOptions(this.options.arrows, options.arrows, "to"); + util.mergeOptions(this.options.arrows, options.arrows, "middle"); + util.mergeOptions(this.options.arrows, options.arrows, "from"); + } else { + throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); + } + } - var PROPS_XY = ['x', 'y']; - var PROPS_CLIENT_XY = ['clientX', 'clientY']; + // hanlde multiple input cases for color + if (options.color !== undefined) { + if (util.isString(options.color)) { + util.assignAllKeys(this.options.color, options.color); + this.options.color.inherit.enabled = false; + } else { + util.extend(this.options.color, options.color); + if (options.color.inherit === undefined) { + this.options.color.inherit.enabled = false; + } + } + util.mergeOptions(this.options.color, options.color, "inherit"); + } - /** - * create new input type manager - * @param {Manager} manager - * @param {Function} callback - * @returns {Input} - * @constructor - */ - function Input(manager, callback) { - var self = this; - this.manager = manager; - this.callback = callback; - this.element = manager.element; - this.target = manager.options.inputTarget; + // A node is connected when it has a from and to node that both exist in the network.body.nodes. + this.connect(); - // 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.labelModule.setOptions(this.options); - this.init(); + var dataChanged = this.updateEdgeType(); + return dataChanged; + }, + writable: true, + configurable: true + }, + updateEdgeType: { + value: function updateEdgeType() { + var dataChanged = false; + var changeInType = true; + if (this.edgeType !== undefined) { + if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { + changeInType = false; + } + if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false) { + changeInType = false; + } + if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) { + changeInType = false; + } - } + if (changeInType == true) { + dataChanged = this.edgeType.cleanup(); + } + } - Input.prototype = { - /** - * should handle the inputEvent data and trigger the callback - * @virtual - */ - handler: function() { }, + if (changeInType === true) { + if (this.options.smooth.enabled === true) { + if (this.options.smooth.dynamic === true) { + dataChanged = true; + this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); + } else { + this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); + } + } else { + this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); + } + } else { + // if nothing changes, we just set the options. + this.edgeType.setOptions(this.options); + } - /** - * 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); + return dataChanged; + }, + writable: true, + configurable: true }, + togglePhysics: { - /** - * unbind the events - */ - destroy: function() { - this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler); - this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler); - this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler); - } - }; - - /** - * create new input type manager - * called by the Manager constructor - * @param {Hammer} manager - * @returns {Input} - */ - function createInputInstance(manager) { - var Type; - var inputClass = manager.options.inputClass; - if (inputClass) { - Type = inputClass; - } else if (SUPPORT_POINTER_EVENTS) { - Type = PointerEventInput; - } else if (SUPPORT_ONLY_TOUCH) { - Type = TouchInput; - } else if (!SUPPORT_TOUCH) { - Type = MouseInput; - } else { - Type = TouchMouseInput; - } - return new (Type)(manager, inputHandler); - } + /** + * Enable or disable the physics. + * @param status + */ + value: function togglePhysics(status) { + if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { + if (this.via === undefined) { + this.via.pptions.physics = status; + } + } + this.options.physics = status; + }, + writable: true, + configurable: true + }, + connect: { - /** - * 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)); + /** + * Connect an edge to its nodes + */ + value: function connect() { + this.disconnect(); - input.isFirst = !!isFirst; - input.isFinal = !!isFinal; + this.from = this.body.nodes[this.fromId] || undefined; + this.to = this.body.nodes[this.toId] || undefined; + this.connected = this.from !== undefined && this.to !== undefined; - if (isFirst) { - manager.session = {}; - } + if (this.connected === true) { + this.from.attachEdge(this); + this.to.attachEdge(this); + } else { + if (this.from) { + this.from.detachEdge(this); + } + if (this.to) { + this.to.detachEdge(this); + } + } + }, + writable: true, + configurable: true + }, + disconnect: { - // source event is the normalized value of the domEvents - // like 'touchstart, mouseup, pointerdown' - input.eventType = eventType; - // compute scale, rotation etc - computeInputData(manager, input); + /** + * Disconnect an edge from its nodes + */ + value: function disconnect() { + if (this.from) { + this.from.detachEdge(this); + this.from = undefined; + } + if (this.to) { + this.to.detachEdge(this); + this.to = undefined; + } - // emit secret event - manager.emit('hammer.input', input); + this.connected = false; + }, + writable: true, + configurable: true + }, + getTitle: { - manager.recognize(input); - manager.session.prevInput = input; - } - /** - * extend the data with some usable properties like scale, rotate, velocity etc - * @param {Object} manager - * @param {Object} input - */ - function computeInputData(manager, input) { - var session = manager.session; - var pointers = input.pointers; - var pointersLength = pointers.length; + /** + * get the title of this edge. + * @return {string} title The title of the edge, or undefined when no title + * has been set. + */ + value: function getTitle() { + return typeof this.title === "function" ? this.title() : this.title; + }, + writable: true, + configurable: true + }, + isSelected: { - // store the first input to calculate the distance and direction - if (!session.firstInput) { - session.firstInput = simpleCloneInputData(input); - } - // to compute scale and rotation we need to store the multiple touches - if (pointersLength > 1 && !session.firstMultiple) { - session.firstMultiple = simpleCloneInputData(input); - } else if (pointersLength === 1) { - session.firstMultiple = false; - } + /** + * check if this node is selecte + * @return {boolean} selected True if node is selected, else false + */ + value: function isSelected() { + return this.selected; + }, + writable: true, + configurable: true + }, + getValue: { - var firstInput = session.firstInput; - var firstMultiple = session.firstMultiple; - var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center; - var center = input.center = getCenter(pointers); - input.timeStamp = now(); - input.deltaTime = input.timeStamp - firstInput.timeStamp; - input.angle = getAngle(offsetCenter, center); - input.distance = getDistance(offsetCenter, center); + /** + * Retrieve the value of the edge. Can be undefined + * @return {Number} value + */ + value: function getValue() { + return this.value; + }, + writable: true, + configurable: true + }, + setValueRange: { - computeDeltaXY(session, input); - input.offsetDirection = getDirection(input.deltaX, input.deltaY); - input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1; - input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0; + /** + * Adjust the value range of the edge. The edge will adjust it's width + * based on its value. + * @param {Number} min + * @param {Number} max + * @param total + */ + value: function setValueRange(min, max, total) { + if (this.value !== undefined) { + var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); + var widthDiff = this.options.scaling.max - this.options.scaling.min; + if (this.options.scaling.label.enabled == true) { + var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; + this.options.font.size = this.options.scaling.label.min + scale * fontDiff; + } + this.options.width = this.options.scaling.min + scale * widthDiff; + } + }, + writable: true, + configurable: true + }, + draw: { - computeIntervalInputData(session, input); - // find the correct target - var target = manager.element; - if (hasParent(input.srcEvent.target, target)) { - target = input.srcEvent.target; - } - input.target = target; - } + /** + * Redraw a edge + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + */ + value: function draw(ctx) { + var via = this.edgeType.drawLine(ctx, this.selected, this.hover); + this.drawArrows(ctx, via); + this.drawLabel(ctx, via); + }, + writable: true, + configurable: true + }, + drawArrows: { + value: function drawArrows(ctx, viaNode) { + if (this.options.arrows.from.enabled === true) { + this.edgeType.drawArrowHead(ctx, "from", viaNode, this.selected, this.hover); + } + if (this.options.arrows.middle.enabled === true) { + this.edgeType.drawArrowHead(ctx, "middle", viaNode, this.selected, this.hover); + } + if (this.options.arrows.to.enabled === true) { + this.edgeType.drawArrowHead(ctx, "to", viaNode, this.selected, this.hover); + } + }, + writable: true, + configurable: true + }, + drawLabel: { + value: function drawLabel(ctx, viaNode) { + if (this.options.label !== undefined) { + // set style + var node1 = this.from; + var node2 = this.to; + var selected = this.from.selected || this.to.selected || this.selected; + if (node1.id != node2.id) { + var point = this.edgeType.getPoint(0.5, viaNode); + ctx.save(); - function computeDeltaXY(session, input) { - var center = input.center; - var offset = session.offsetDelta || {}; - var prevDelta = session.prevDelta || {}; - var prevInput = session.prevInput || {}; + // if the label has to be rotated: + if (this.options.font.align !== "horizontal") { + this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); + ctx.translate(point.x, this.labelModule.size.yLine); + this._rotateForLabelAlignment(ctx); + } - if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) { - prevDelta = session.prevDelta = { - x: prevInput.deltaX || 0, - y: prevInput.deltaY || 0 - }; + // draw the label + this.labelModule.draw(ctx, point.x, point.y, selected); + ctx.restore(); + } else { + var x, y; + var radius = this.options.selfReferenceSize; + if (node1.width > node1.height) { + x = node1.x + node1.width * 0.5; + y = node1.y - radius; + } else { + x = node1.x + radius; + y = node1.y - node1.height * 0.5; + } + point = this._pointOnCircle(x, y, radius, 0.125); - offset = session.offsetDelta = { - x: center.x, - y: center.y - }; - } + this.labelModule.draw(ctx, point.x, point.y, selected); + } + } + }, + writable: true, + configurable: true + }, + isOverlappingWith: { - input.deltaX = prevDelta.x + (center.x - offset.x); - input.deltaY = prevDelta.y + (center.y - offset.y); - } - /** - * velocity is calculated every x ms - * @param {Object} session - * @param {Object} input - */ - function computeIntervalInputData(session, input) { - var last = session.lastInterval || input, - deltaTime = input.timeStamp - last.timeStamp, - velocity, velocityX, velocityY, direction; + /** + * Check if this object is overlapping with the provided object + * @param {Object} obj an object with parameters left, top + * @return {boolean} True if location is located on the edge + */ + value: function isOverlappingWith(obj) { + if (this.connected) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; - if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) { - var deltaX = last.deltaX - input.deltaX; - var deltaY = last.deltaY - input.deltaY; + var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - var v = getVelocity(deltaTime, deltaX, deltaY); - velocityX = v.x; - velocityY = v.y; - velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y; - direction = getDirection(deltaX, deltaY); + return dist < distMax; + } else { + return false; + } + }, + writable: true, + configurable: true + }, + _rotateForLabelAlignment: { - 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; - } + /** + * Rotates the canvas so the text is most readable + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _rotateForLabelAlignment(ctx) { + var dy = this.from.y - this.to.y; + var dx = this.from.x - this.to.x; + var angleInDegrees = Math.atan2(dy, dx); - /** - * 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++; - } + // rotate so label it is readable + if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { + angleInDegrees = angleInDegrees + Math.PI; + } - return { - timeStamp: now(), - pointers: pointers, - center: getCenter(pointers), - deltaX: input.deltaX, - deltaY: input.deltaY - }; - } + ctx.rotate(angleInDegrees); + }, + writable: true, + configurable: true + }, + _pointOnCircle: { - /** - * get the center of all the pointers - * @param {Array} pointers - * @return {Object} center contains `x` and `y` properties - */ - function getCenter(pointers) { - var pointersLength = pointers.length; - // no need to loop when only one touch - if (pointersLength === 1) { + /** + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; return { - x: round(pointers[0].clientX), - y: round(pointers[0].clientY) + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) }; + }, + writable: true, + configurable: true + }, + select: { + value: function select() { + this.selected = true; + }, + writable: true, + configurable: true + }, + unselect: { + value: function unselect() { + this.selected = false; + }, + writable: true, + configurable: true } + }); - var x = 0, y = 0, i = 0; - while (i < pointersLength) { - x += pointers[i].clientX; - y += pointers[i].clientY; - i++; - } - - return { - x: round(x / pointersLength), - y: round(y / pointersLength) - }; - } - - /** - * calculate the velocity between two points. unit is in px per ms. - * @param {Number} deltaTime - * @param {Number} x - * @param {Number} y - * @return {Object} velocity `x` and `y` - */ - function getVelocity(deltaTime, x, y) { - return { - x: x / deltaTime || 0, - y: y / deltaTime || 0 - }; - } - - /** - * get the direction between two points - * @param {Number} x - * @param {Number} y - * @return {Number} direction - */ - function getDirection(x, y) { - if (x === y) { - return DIRECTION_NONE; - } + return Edge; + })(); - if (abs(x) >= abs(y)) { - return x > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT; - } - return y > 0 ? DIRECTION_UP : DIRECTION_DOWN; - } + module.exports = Edge; - /** - * calculate the absolute distance between two points - * @param {Object} p1 {x, y} - * @param {Object} p2 {x, y} - * @param {Array} [props] containing x and y keys - * @return {Number} distance - */ - function getDistance(p1, p2, props) { - if (!props) { - props = PROPS_XY; - } - var x = p2[props[0]] - p1[props[0]], - y = p2[props[1]] - p1[props[1]]; +/***/ }, +/* 81 */ +/***/ function(module, exports, __webpack_require__) { - return Math.sqrt((x * x) + (y * y)); - } + "use strict"; - /** - * 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; - } + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - /** - * 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); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * calculate the scale factor between two pointersets - * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out - * @param {Array} start array of pointers - * @param {Array} end array of pointers - * @return {Number} scale - */ - function getScale(start, end) { - return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY); - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - var MOUSE_INPUT_MAP = { - mousedown: INPUT_START, - mousemove: INPUT_MOVE, - mouseup: INPUT_END - }; + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var MOUSE_ELEMENT_EVENTS = 'mousedown'; - var MOUSE_WINDOW_EVENTS = 'mousemove mouseup'; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Mouse events input - * @constructor - * @extends Input + * Created by Alex on 3/20/2015. */ - 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 + var BezierEdgeBase = _interopRequire(__webpack_require__(82)); - Input.apply(this, arguments); - } + var BezierEdgeDynamic = (function (BezierEdgeBase) { + function BezierEdgeDynamic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeDynamic); - inherit(MouseInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function MEhandler(ev) { - var eventType = MOUSE_INPUT_MAP[ev.type]; + this.via = undefined; + _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); // --> this calls the setOptions below + } - // on start we want to have the left mouse button down - if (eventType & INPUT_START && ev.button === 0) { - this.pressed = true; - } + _inherits(BezierEdgeDynamic, BezierEdgeBase); - if (eventType & INPUT_MOVE && ev.which !== 1) { - eventType = INPUT_END; + _prototypeProperties(BezierEdgeDynamic, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; + this.setupSupportNode(); + }, + writable: true, + configurable: true + }, + cleanup: { + value: function cleanup() { + if (this.via !== undefined) { + delete this.body.nodes[this.via.id]; + this.via = undefined; + return true; } + return false; + }, + writable: true, + configurable: true + }, + setupSupportNode: { - // mouse must be down, and mouse events are allowed (see the TouchMouse input) - if (!this.pressed || !this.allow) { - return; + /** + * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but + * are used for the force calculation. + * + * The changed data is not called, if needed, it is returned by the main edge constructor. + * @private + */ + value: function setupSupportNode() { + if (this.via === undefined) { + var nodeId = "edgeId:" + this.id; + var node = this.body.functions.createNode({ + id: nodeId, + mass: 1, + shape: "circle", + image: "", + physics: true, + hidden: true + }); + this.body.nodes[nodeId] = node; + this.via = node; + this.via.parentEdgeId = this.id; + this.positionBezierNode(); } - - if (eventType & INPUT_END) { - this.pressed = false; + }, + writable: true, + configurable: true + }, + positionBezierNode: { + value: function positionBezierNode() { + if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { + this.via.x = 0.5 * (this.from.x + this.to.x); + this.via.y = 0.5 * (this.from.y + this.to.y); + } else if (this.via !== undefined) { + this.via.x = 0; + this.via.y = 0; } + }, + writable: true, + configurable: true + }, + _line: { - this.callback(this.manager, eventType, { - pointers: [ev], - changedPointers: [ev], - pointerType: INPUT_TYPE_MOUSE, - srcEvent: ev - }); + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); + ctx.stroke(); + return this.via; + }, + writable: true, + configurable: true + }, + getPoint: { + + + /** + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} + * @private + */ + value: function getPoint(percentage) { + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + + return { x: x, y: y }; + }, + writable: true, + configurable: true + }, + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + return this._findBorderPositionBezier(nearNode, ctx, this.via); + }, + writable: true, + configurable: true + }, + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); + }, + writable: true, + configurable: true } - }); + }); - var POINTER_INPUT_MAP = { - pointerdown: INPUT_START, - pointermove: INPUT_MOVE, - pointerup: INPUT_END, - pointercancel: INPUT_CANCEL, - pointerout: INPUT_CANCEL - }; + return BezierEdgeDynamic; + })(BezierEdgeBase); - // 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 - }; + module.exports = BezierEdgeDynamic; - var POINTER_ELEMENT_EVENTS = 'pointerdown'; - var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel'; +/***/ }, +/* 82 */ +/***/ function(module, exports, __webpack_require__) { - // IE10 has prefixed support, and case-sensitive - if (window.MSPointerEvent) { - POINTER_ELEMENT_EVENTS = 'MSPointerDown'; - POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel'; - } + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Pointer events input - * @constructor - * @extends Input + * Created by Alex on 3/20/2015. */ - function PointerEventInput() { - this.evEl = POINTER_ELEMENT_EVENTS; - this.evWin = POINTER_WINDOW_EVENTS; - Input.apply(this, arguments); + var EdgeBase = _interopRequire(__webpack_require__(83)); - this.store = (this.manager.session.pointerEvents = []); - } + var BezierEdgeBase = (function (EdgeBase) { + function BezierEdgeBase(options, body, labelModule) { + _classCallCheck(this, BezierEdgeBase); - inherit(PointerEventInput, Input, { - /** - * handle mouse events - * @param {Object} ev - */ - handler: function PEhandler(ev) { - var store = this.store; - var removePointer = false; + _get(Object.getPrototypeOf(BezierEdgeBase.prototype), "constructor", this).call(this, options, body, labelModule); + } - var eventTypeNormalized = ev.type.toLowerCase().replace('ms', ''); - var eventType = POINTER_INPUT_MAP[eventTypeNormalized]; - var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType; + _inherits(BezierEdgeBase, EdgeBase); - var isTouch = (pointerType == INPUT_TYPE_TOUCH); + _prototypeProperties(BezierEdgeBase, null, { + _findBorderPositionBezier: { - // get index of the event in the store - var storeIndex = inArray(store, ev.pointerId, 'pointerId'); + /** + * This function uses binary search to look for the point where the bezier curve crosses the border of the node. + * + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + * @param nearNode + * @param ctx + * @param viaNode + */ + value: function _findBorderPositionBezier(nearNode, ctx) { + var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; + var maxIterations = 10; + var iteration = 0; + var low = 0; + var high = 1; + var pos, angle, distanceToBorder, distanceToPoint, difference; + var threshold = 0.2; + var node = this.to; + var from = false; + if (nearNode.id === this.from.id) { + node = this.from; + from = true; + } - // start and mouse must be down - if (eventType & INPUT_START && (ev.button === 0 || isTouch)) { - if (storeIndex < 0) { - store.push(ev); - storeIndex = store.length - 1; + while (low <= high && iteration < maxIterations) { + var middle = (low + high) * 0.5; + + pos = this.getPoint(middle, viaNode); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + if (Math.abs(difference) < threshold) { + break; // found + } else if (difference < 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (from == false) { + low = middle; + } else { + high = middle; } - } else if (eventType & (INPUT_END | INPUT_CANCEL)) { - removePointer = true; - } + } else { + if (from == false) { + high = middle; + } else { + low = middle; + } + } - // it not found, so the pointer hasn't been down (so it's probably a hover) - if (storeIndex < 0) { - return; + iteration++; } + pos.t = middle; - // update the event in the store - store[storeIndex] = ev; + return pos; + }, + writable: true, + configurable: true + }, + _getDistanceToBezierEdge: { - this.callback(this.manager, eventType, { - pointers: store, - changedPointers: [ev], - pointerType: pointerType, - srcEvent: ev - }); - if (removePointer) { - // remove from the store - store.splice(storeIndex, 1); + + /** + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + * @private + */ + value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var xVia = undefined, + yVia = undefined; + xVia = via.x; + yVia = via.y; + var minDistance = 1000000000; + var distance = undefined; + var i = undefined, + t = undefined, + x = undefined, + y = undefined; + var lastX = x1; + var lastY = y1; + for (i = 1; i < 10; i++) { + t = 0.1 * i; + x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; + y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; + if (i > 0) { + distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); + minDistance = distance < minDistance ? distance : minDistance; + } + lastX = x; + lastY = y; } + + return minDistance; + }, + writable: true, + configurable: true } - }); + }); - var SINGLE_TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; + return BezierEdgeBase; + })(EdgeBase); - var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart'; - var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel'; + module.exports = BezierEdgeBase; + +/***/ }, +/* 83 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; + + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Touch events input - * @constructor - * @extends Input + * Created by Alex on 3/20/2015. */ - function SingleTouchInput() { - this.evTarget = SINGLE_TOUCH_TARGET_EVENTS; - this.evWin = SINGLE_TOUCH_WINDOW_EVENTS; - this.started = false; + var util = __webpack_require__(1); - Input.apply(this, arguments); - } + var EdgeBase = (function () { + function EdgeBase(options, body, labelModule) { + _classCallCheck(this, EdgeBase); - inherit(SingleTouchInput, Input, { - handler: function TEhandler(ev) { - var type = SINGLE_TOUCH_INPUT_MAP[ev.type]; + this.body = body; + this.labelModule = labelModule; + this.setOptions(options); + this.colorDirty = true; + } - // should we handle the touch events? - if (type === INPUT_START) { - this.started = true; - } + _prototypeProperties(EdgeBase, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + this.from = this.body.nodes[this.options.from]; + this.to = this.body.nodes[this.options.to]; + this.id = this.options.id; + }, + writable: true, + configurable: true + }, + drawLine: { - if (!this.started) { - return; - } + /** + * Redraw a edge as a line + * Draw this edge in the given canvas + * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function drawLine(ctx, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.lineWidth = this.getLineWidth(selected, hover); + var via = undefined; + if (this.from != this.to) { + // draw line + if (this.options.dashes.enabled == true) { + via = this._drawDashedLine(ctx); + } else { + via = this._line(ctx); + } + } else { + var _getCircleData = this._getCircleData(); - var touches = normalizeSingleTouches.call(this, ev, type); + var _getCircleData2 = _slicedToArray(_getCircleData, 3); - // when done, reset the started state - if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) { - this.started = false; + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; + this._circle(ctx, x, y, radius); } - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); - - /** - * @this {TouchInput} - * @param {Object} ev - * @param {Number} type flag - * @returns {undefined|Array} [all, changed] - */ - function normalizeSingleTouches(ev, type) { - var all = toArray(ev.touches); - var changed = toArray(ev.changedTouches); + return via; + }, + writable: true, + configurable: true + }, + _drawDashedLine: { + value: function _drawDashedLine(ctx) { + var via = undefined; + // only firefox and chrome support this method, else we use the legacy one. + if (ctx.setLineDash !== undefined && this.options.dashes.altLength === undefined) { + ctx.save(); + // configure the dash pattern + var pattern = [0]; + if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) { + pattern = [this.options.dashes.length, this.options.dashes.gap]; + } else { + pattern = [5, 5]; + } - if (type & (INPUT_END | INPUT_CANCEL)) { - all = uniqueArray(all.concat(changed), 'identifier', true); - } + // set dash settings for chrome or firefox + ctx.setLineDash(pattern); + ctx.lineDashOffset = 0; - return [all, changed]; - } + // draw the line + via = this._line(ctx); - var TOUCH_INPUT_MAP = { - touchstart: INPUT_START, - touchmove: INPUT_MOVE, - touchend: INPUT_END, - touchcancel: INPUT_CANCEL - }; + // restore the dash settings. + ctx.setLineDash([0]); + ctx.lineDashOffset = 0; + ctx.restore(); + } else { + // unsupporting smooth lines + // draw dashes line + ctx.beginPath(); + ctx.lineCap = "round"; + if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value + { + ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]); + } else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value + { + ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap]); + } else //If all else fails draw a line + { + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + } + ctx.stroke(); + } + return via; + }, + writable: true, + configurable: true + }, + findBorderPosition: { + value: function findBorderPosition(nearNode, ctx, options) { + if (this.from != this.to) { + return this._findBorderPosition(nearNode, ctx, options); + } else { + return this._findBorderPositionCircle(nearNode, ctx, options); + } + }, + writable: true, + configurable: true + }, + findBorderPositions: { + value: function findBorderPositions(ctx) { + var from = {}; + var to = {}; + if (this.from != this.to) { + from = this._findBorderPosition(this.from, ctx); + to = this._findBorderPosition(this.to, ctx); + } else { + var _getCircleData = this._getCircleData(); - var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel'; + var _getCircleData2 = _slicedToArray(_getCircleData, 3); - /** - * Multi-user touch events input - * @constructor - * @extends Input - */ - function TouchInput() { - this.evTarget = TOUCH_TARGET_EVENTS; - this.targetIds = {}; + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; - 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; + from = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); + to = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.6, high: 0.8, direction: 1 }); } + return { from: from, to: to }; + }, + writable: true, + configurable: true + }, + _getCircleData: { + value: function _getCircleData() { + var x = undefined, + y = undefined; + var node = this.from; + var radius = this.options.selfReferenceSize; - this.callback(this.manager, type, { - pointers: touches[0], - changedPointers: touches[1], - pointerType: INPUT_TYPE_TOUCH, - srcEvent: ev - }); - } - }); + // get circle coordinates + if (node.shape.width > node.shape.height) { + x = node.x + node.shape.width * 0.5; + y = node.y - radius; + } else { + x = node.x + radius; + y = node.y - node.shape.height * 0.5; + } + return [x, y, radius]; + }, + writable: true, + configurable: true + }, + _pointOnCircle: { - /** - * @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; + /** + * Get a point on a circle + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} percentage. Value between 0 (line start) and 1 (line end) + * @return {Object} point + * @private + */ + value: function _pointOnCircle(x, y, radius, percentage) { + var angle = percentage * 2 * Math.PI; + return { + x: x + radius * Math.cos(angle), + y: y - radius * Math.sin(angle) + }; + }, + writable: true, + configurable: true + }, + _findBorderPositionCircle: { - // 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]; - } + /** + * This function uses binary search to look for the point where the circle crosses the border of the node. + * @param node + * @param ctx + * @param options + * @returns {*} + * @private + */ + value: function _findBorderPositionCircle(node, ctx, options) { + var x = options.x; + var y = options.y; + var low = options.low; + var high = options.high; + var direction = options.direction; - var i, - targetTouches, - changedTouches = toArray(ev.changedTouches), - changedTargetTouches = [], - target = this.target; + var maxIterations = 10; + var iteration = 0; + var radius = this.options.selfReferenceSize; + var pos = undefined, + angle = undefined, + distanceToBorder = undefined, + distanceToPoint = undefined, + difference = undefined; + var threshold = 0.05; + var middle = (low + high) * 0.5; - // get target touches from touches - targetTouches = allTouches.filter(function(touch) { - return hasParent(touch.target, target); - }); + while (low <= high && iteration < maxIterations) { + middle = (low + high) * 0.5; - // collect touches - if (type === INPUT_START) { - i = 0; - while (i < targetTouches.length) { - targetIds[targetTouches[i].identifier] = true; - i++; + pos = this._pointOnCircle(x, y, radius, middle); + angle = Math.atan2(node.y - pos.y, node.x - pos.x); + distanceToBorder = node.distanceToBorder(ctx, angle); + distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); + difference = distanceToBorder - distanceToPoint; + if (Math.abs(difference) < threshold) { + break; // found + } else if (difference > 0) { + // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. + if (direction > 0) { + low = middle; + } else { + high = middle; + } + } else { + if (direction > 0) { + high = middle; + } else { + low = middle; + } + } + iteration++; } - } + pos.t = middle; - // 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]); - } + return pos; + }, + writable: true, + configurable: true + }, + getLineWidth: { - // cleanup removed touches - if (type & (INPUT_END | INPUT_CANCEL)) { - delete targetIds[changedTouches[i].identifier]; + /** + * Get the line width of the edge. Depends on width and whether one of the + * connected nodes is selected. + * @return {Number} width + * @private + */ + value: function getLineWidth(selected, hover) { + if (selected == true) { + return Math.max(Math.min(this.options.widthSelectionMultiplier * this.options.width, this.options.scaling.max), 0.3 / this.body.view.scale); + } else { + if (hover == true) { + return Math.max(Math.min(this.options.hoverWidth, this.options.scaling.max), 0.3 / this.body.view.scale); + } else { + return Math.max(this.options.width, 0.3 / this.body.view.scale); + } } - 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 - ]; - } + }, + writable: true, + configurable: true + }, + getColor: { + value: function getColor(ctx) { + var colorObj = this.options.color; - /** - * 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); + if (colorObj.inherit.enabled === true) { + if (colorObj.inherit.useGradients == true) { + var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); + var fromColor, toColor; + fromColor = this.from.options.color.highlight.border; + toColor = this.to.options.color.highlight.border; - var handler = bindFn(this.handler, this); - this.touch = new TouchInput(this.manager, handler); - this.mouse = new MouseInput(this.manager, handler); - } + if (this.from.selected == false && this.to.selected == false) { + fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); + toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); + } else if (this.from.selected == true && this.to.selected == false) { + toColor = this.to.options.color.border; + } else if (this.from.selected == false && this.to.selected == true) { + fromColor = this.from.options.color.border; + } + grd.addColorStop(0, fromColor); + grd.addColorStop(1, toColor); - 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 returns -------------------- // + return grd; + } - // 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; + if (this.colorDirty === true) { + if (colorObj.inherit.source == "to") { + colorObj.highlight = this.to.options.color.highlight.border; + colorObj.hover = this.to.options.color.hover.border; + colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); + } else { + // (this.options.color.inherit.source == "from") { + colorObj.highlight = this.from.options.color.highlight.border; + colorObj.hover = this.from.options.color.hover.border; + colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); + } + } } - // reset the allowMouse when we're done - if (inputEvent & (INPUT_END | INPUT_CANCEL)) { - this.mouse.allow = true; - } + // if color inherit is on and gradients are used, the function has already returned by now. + this.colorDirty = false; - this.callback(manager, inputEvent, inputData); + if (this.selected == true) { + return colorObj.highlight; + } else if (this.hover == true) { + return colorObj.hover; + } else { + return colorObj.color; + } + }, + writable: true, + configurable: true }, + _circle: { - /** - * remove the event listeners - */ - destroy: function destroy() { - this.touch.destroy(); - this.mouse.destroy(); - } - }); + /** + * Draw a line from a node to itself, a circle + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @private + */ + value: function _circle(ctx, x, y, radius) { + // draw a circle + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI, false); + ctx.stroke(); + }, + writable: true, + configurable: true + }, + getDistanceToEdge: { - var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); - var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; - // magical touchAction value - var TOUCH_ACTION_COMPUTE = 'compute'; - var TOUCH_ACTION_AUTO = 'auto'; - var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented - var TOUCH_ACTION_NONE = 'none'; - var TOUCH_ACTION_PAN_X = 'pan-x'; - var TOUCH_ACTION_PAN_Y = 'pan-y'; + /** + * Calculate the distance between a point (x3,y3) and a line segment from + * (x1,y1) to (x2,y2). + * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment + * @param {number} x1 + * @param {number} y1 + * @param {number} x2 + * @param {number} y2 + * @param {number} x3 + * @param {number} y3 + * @private + */ + value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { + // x3,y3 is the point + var returnValue = 0; + if (this.from != this.to) { + returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); + } else { + var _getCircleData = this._getCircleData(); - /** - * Touch Action - * sets the touchAction property or uses the js alternative - * @param {Manager} manager - * @param {String} value - * @constructor - */ - function TouchAction(manager, value) { - this.manager = manager; - this.set(value); - } + var _getCircleData2 = _slicedToArray(_getCircleData, 3); - 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(); + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; + var dx = x - x3; + var dy = y - y3; + returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); } - if (NATIVE_TOUCH_ACTION) { - this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; + if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { + return 0; + } else { + return returnValue; } - 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(' ')); + }, + writable: true, + configurable: true }, + _getDistanceToLine: { + value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { + var px = x2 - x1; + var py = y2 - y1; + var something = px * px + py * py; + var u = ((x3 - x1) * px + (y3 - y1) * py) / something; - /** - * 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; + if (u > 1) { + u = 1; + } else if (u < 0) { + u = 0; } - 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 x = x1 + u * px; + var y = y1 + u * py; + var dx = x - x3; + var dy = y - y3; - 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); + //# 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 - if (hasNone || - (hasPanY && direction & DIRECTION_HORIZONTAL) || - (hasPanX && direction & DIRECTION_VERTICAL)) { - return this.preventSrc(srcEvent); - } + return Math.sqrt(dx * dx + dy * dy); + }, + writable: true, + configurable: true }, + drawArrowHead: { - /** - * call preventDefault to prevent the browser's default behavior (scrolling in most cases) - * @param {Object} srcEvent - */ - preventSrc: function(srcEvent) { - this.manager.session.prevented = true; - srcEvent.preventDefault(); - } - }; - - /** - * when the touchActions are collected they are not a valid value, so we need to clean things up. * - * @param {String} actions - * @returns {*} - */ - function cleanTouchActions(actions) { - // none - if (inStr(actions, TOUCH_ACTION_NONE)) { - return TOUCH_ACTION_NONE; - } + /** + * + * @param ctx + * @param position + * @param viaNode + */ + value: function drawArrowHead(ctx, position, viaNode, selected, hover) { + // set style + ctx.strokeStyle = this.getColor(ctx); + ctx.fillStyle = ctx.strokeStyle; + ctx.lineWidth = this.getLineWidth(selected, hover); - var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); - var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); + // set lets + var angle = undefined; + var length = undefined; + var arrowPos = undefined; + var node1 = undefined; + var node2 = undefined; + var guideOffset = undefined; + var scaleFactor = undefined; - // pan-x and pan-y can be combined - if (hasPanX && hasPanY) { - return TOUCH_ACTION_PAN_X + ' ' + TOUCH_ACTION_PAN_Y; - } + if (position == "from") { + node1 = this.from; + node2 = this.to; + guideOffset = 0.1; + scaleFactor = this.options.arrows.from.scaleFactor; + } else if (position == "to") { + node1 = this.to; + node2 = this.from; + guideOffset = -0.1; + scaleFactor = this.options.arrows.to.scaleFactor; + } else { + node1 = this.to; + node2 = this.from; + scaleFactor = this.options.arrows.middle.scaleFactor; + } - // pan-x OR pan-y - if (hasPanX || hasPanY) { - return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y; - } + // if not connected to itself + if (node1 != node2) { + if (position !== "middle") { + // draw arrow head + if (this.options.smooth.enabled == true) { + arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); + var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); + angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + } else { + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.findBorderPosition(node1, ctx); + } + } else { + angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. + } + // draw arrow at the end of the line + length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(arrowPos.x, arrowPos.y, angle, length); + ctx.fill(); + ctx.stroke(); + } else { + // draw circle + var _angle = undefined, + point = undefined; + var _getCircleData = this._getCircleData(); - // manipulation - if (inStr(actions, TOUCH_ACTION_MANIPULATION)) { - return TOUCH_ACTION_MANIPULATION; - } + var _getCircleData2 = _slicedToArray(_getCircleData, 3); - return TOUCH_ACTION_AUTO; - } + var x = _getCircleData2[0]; + var y = _getCircleData2[1]; + var radius = _getCircleData2[2]; - /** - * Recognizer flow explained; * - * All recognizers have the initial state of POSSIBLE when a input session starts. - * The definition of a input session is from the first input until the last input, with all it's movement in it. * - * Example session for mouse-input: mousedown -> mousemove -> mouseup - * - * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed - * which determines with state it should be. - * - * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to - * POSSIBLE to give it another change on the next cycle. - * - * Possible - * | - * +-----+---------------+ - * | | - * +-----+-----+ | - * | | | - * Failed Cancelled | - * +-------+------+ - * | | - * Recognized Began - * | - * Changed - * | - * Ended/Recognized - */ - var STATE_POSSIBLE = 1; - var STATE_BEGAN = 2; - var STATE_CHANGED = 4; - var STATE_ENDED = 8; - var STATE_RECOGNIZED = STATE_ENDED; - var STATE_CANCELLED = 16; - var STATE_FAILED = 32; - /** - * Recognizer - * Every recognizer needs to extend from this class. - * @constructor - * @param {Object} options - */ - function Recognizer(options) { - this.id = uniqueId(); + if (position == "from") { + point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; + } else if (position == "to") { + point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.6, high: 1, direction: 1 }); + _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; + } else { + point = this._pointOnCircle(x, y, radius, 0.175); + _angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; + } - this.manager = null; - this.options = merge(options || {}, this.defaults); + // draw the arrowhead + var _length = (10 + 5 * this.options.width) * scaleFactor; + ctx.arrow(point.x, point.y, _angle, _length); + ctx.fill(); + ctx.stroke(); + } + }, + writable: true, + configurable: true + } + }); - // default is enable true - this.options.enable = ifUndefined(this.options.enable, true); + return EdgeBase; + })(); - this.state = STATE_POSSIBLE; + module.exports = EdgeBase; - this.simultaneous = {}; - this.requireFail = []; - } +/***/ }, +/* 84 */ +/***/ function(module, exports, __webpack_require__) { - Recognizer.prototype = { - /** - * @virtual - * @type {Object} - */ - defaults: {}, + "use strict"; - /** - * set options - * @param {Object} options - * @return {Recognizer} - */ - set: function(options) { - extend(this.options, options); + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - // also update the touchAction, in case something changed about the directions/enabled state - this.manager && this.manager.touchAction.update(); - return this; - }, + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * recognize simultaneous with an other recognizer. - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - recognizeWith: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) { - return this; - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - var simultaneous = this.simultaneous; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (!simultaneous[otherRecognizer.id]) { - simultaneous[otherRecognizer.id] = otherRecognizer; - otherRecognizer.recognizeWith(this); - } - return this; - }, + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - /** - * 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; - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - delete this.simultaneous[otherRecognizer.id]; - return this; - }, + /** + * Created by Alex on 3/20/2015. + */ - /** - * recognizer can only run when an other is failing - * @param {Recognizer} otherRecognizer - * @returns {Recognizer} this - */ - requireFailure: function(otherRecognizer) { - if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) { - return this; - } + var BezierEdgeBase = _interopRequire(__webpack_require__(82)); - var requireFail = this.requireFail; - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - if (inArray(requireFail, otherRecognizer) === -1) { - requireFail.push(otherRecognizer); - otherRecognizer.requireFailure(this); - } - return this; - }, + var BezierEdgeStatic = (function (BezierEdgeBase) { + function BezierEdgeStatic(options, body, labelModule) { + _classCallCheck(this, BezierEdgeStatic); - /** - * 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; - } + _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); + } - otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this); - var index = inArray(this.requireFail, otherRecognizer); - if (index > -1) { - this.requireFail.splice(index, 1); - } - return this; - }, + _inherits(BezierEdgeStatic, BezierEdgeBase); - /** - * has require failures boolean - * @returns {boolean} - */ - hasRequireFailures: function() { - return this.requireFail.length > 0; + _prototypeProperties(BezierEdgeStatic, null, { + cleanup: { + value: function cleanup() { + return false; + }, + writable: true, + configurable: true }, + _line: { + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + var via = this._getViaCoordinates(); - /** - * if the recognizer can recognize simultaneous with an other recognizer - * @param {Recognizer} otherRecognizer - * @returns {Boolean} - */ - canRecognizeWith: function(otherRecognizer) { - return !!this.simultaneous[otherRecognizer.id]; + // fallback to normal straight edges + if (via.x === undefined) { + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return undefined; + } else { + ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); + ctx.stroke(); + return via; + } + }, + writable: true, + configurable: true }, + _getViaCoordinates: { + value: function _getViaCoordinates() { + var xVia = undefined; + var yVia = undefined; + var factor = this.options.smooth.roundness; + var type = this.options.smooth.type; + var dx = Math.abs(this.from.x - this.to.x); + var dy = Math.abs(this.from.y - this.to.y); + if (type == "discrete" || type == "diagonalCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y - factor * dy; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y - factor * dy; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dy; + yVia = this.from.y + factor * dy; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dy; + yVia = this.from.y + factor * dy; + } + } + if (type == "discrete") { + xVia = dx < factor * dy ? this.from.x : xVia; + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + if (this.from.y > this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y - factor * dx; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y - factor * dx; + } + } else if (this.from.y < this.to.y) { + if (this.from.x < this.to.x) { + xVia = this.from.x + factor * dx; + yVia = this.from.y + factor * dx; + } else if (this.from.x > this.to.x) { + xVia = this.from.x - factor * dx; + yVia = this.from.y + factor * dx; + } + } + if (type == "discrete") { + yVia = dy < factor * dx ? this.from.y : yVia; + } + } + } else if (type == "straightCross") { + if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { + // up - down + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { + // left - right + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } + } else if (type == "horizontal") { + if (this.from.x < this.to.x) { + xVia = this.to.x - (1 - factor) * dx; + } else { + xVia = this.to.x + (1 - factor) * dx; + } + yVia = this.from.y; + } else if (type == "vertical") { + xVia = this.from.x; + if (this.from.y < this.to.y) { + yVia = this.to.y - (1 - factor) * dy; + } else { + yVia = this.to.y + (1 - factor) * dy; + } + } else if (type == "curvedCW") { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - /** - * You should use `tryEmit` instead of `emit` directly to check - * that all the needed recognizers has failed before emitting. - * @param {Object} input - */ - emit: function(input) { - var self = this; - var state = this.state; - - function emit(withState) { - self.manager.emit(self.options.event + (withState ? stateStr(state) : ''), input); - } + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (factor * 0.5 + 0.5) * pi) % (2 * pi); - // 'panstart' and 'panmove' - if (state < STATE_ENDED) { - emit(true); - } + xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle); + yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle); + } else if (type == "curvedCCW") { + dx = this.to.x - this.from.x; + dy = this.from.y - this.to.y; + var radius = Math.sqrt(dx * dx + dy * dy); + var pi = Math.PI; - emit(); // simple 'eventName' events + var originalAngle = Math.atan2(dy, dx); + var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); - // panend and pancancel - if (state >= STATE_ENDED) { - emit(true); + 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; + } + } + } } + return { x: xVia, y: yVia }; + }, + writable: true, + configurable: true }, - - /** - * Check that all the require failure recognizers has failed, - * if true, it emits a gesture event, - * otherwise, setup the state to FAILED. - * @param {Object} input - */ - tryEmit: function(input) { - if (this.canEmit()) { - return this.emit(input); - } - // it's failing anyway - this.state = STATE_FAILED; + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + var options = arguments[2] === undefined ? {} : arguments[2]; + return this._findBorderPositionBezier(nearNode, ctx, options.via); + }, + writable: true, + configurable: true }, - - /** - * 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; + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; + // x3,y3 is the point + return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); + }, + writable: true, + configurable: true }, + getPoint: { - /** - * 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); + /** + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} + * @private + */ + value: function getPoint(percentage) { + var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; + var t = percentage; + var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; + var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; - // is is enabled and allow recognizing? - if (!boolOrFn(this.options.enable, [this, inputDataClone])) { - this.reset(); - this.state = STATE_FAILED; - return; - } + return { x: x, y: y }; + }, + writable: true, + configurable: true + } + }); - // reset when we've reached the end - if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) { - this.state = STATE_POSSIBLE; - } + return BezierEdgeStatic; + })(BezierEdgeBase); - this.state = this.process(inputDataClone); + module.exports = BezierEdgeStatic; - // the recognizer has recognized a gesture - // so trigger an event - if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) { - this.tryEmit(inputDataClone); - } - }, +/***/ }, +/* 85 */ +/***/ 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 _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - /** - * called when the gesture isn't allowed to recognize - * like when another is being recognized or it is disabled - * @virtual - */ - reset: function() { } - }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * get a usable string, used as event postfix - * @param {Const} state - * @returns {String} state - */ - function stateStr(state) { - if (state & STATE_CANCELLED) { - return 'cancel'; - } else if (state & STATE_ENDED) { - return 'end'; - } else if (state & STATE_CHANGED) { - return 'move'; - } else if (state & STATE_BEGAN) { - return 'start'; - } - return ''; - } + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - /** - * direction cons to string - * @param {Const} direction - * @returns {String} - */ - function directionStr(direction) { - if (direction == DIRECTION_DOWN) { - return 'down'; - } else if (direction == DIRECTION_UP) { - return 'up'; - } else if (direction == DIRECTION_LEFT) { - return 'left'; - } else if (direction == DIRECTION_RIGHT) { - return 'right'; - } - return ''; - } + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - /** - * get a recognizer by name if it is bound to a manager - * @param {Recognizer|String} otherRecognizer - * @param {Recognizer} recognizer - * @returns {Recognizer} - */ - function getRecognizerByNameIfManager(otherRecognizer, recognizer) { - var manager = recognizer.manager; - if (manager) { - return manager.get(otherRecognizer); - } - return otherRecognizer; - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * This recognizer is just used as a base for the simple attribute recognizers. - * @constructor - * @extends Recognizer + * Created by Alex on 3/20/2015. */ - function AttrRecognizer() { - Recognizer.apply(this, arguments); - } - inherit(AttrRecognizer, Recognizer, { - /** - * @namespace - * @memberof AttrRecognizer - */ - defaults: { - /** - * @type {Number} - * @default 1 - */ - pointers: 1 - }, + var EdgeBase = _interopRequire(__webpack_require__(83)); - /** - * 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; - }, + var StraightEdge = (function (EdgeBase) { + function StraightEdge(options, body, labelModule) { + _classCallCheck(this, StraightEdge); - /** - * 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; + _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); + } - var isRecognized = state & (STATE_BEGAN | STATE_CHANGED); - var isValid = this.attrTest(input); + _inherits(StraightEdge, EdgeBase); - // 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; + _prototypeProperties(StraightEdge, null, { + cleanup: { + value: function cleanup() { + return false; + }, + writable: true, + configurable: true + }, + _line: { + /** + * Draw a line between two nodes + * @param {CanvasRenderingContext2D} ctx + * @private + */ + value: function _line(ctx) { + // draw a straight line + ctx.beginPath(); + ctx.moveTo(this.from.x, this.from.y); + ctx.lineTo(this.to.x, this.to.y); + ctx.stroke(); + return undefined; + }, + writable: true, + configurable: true + }, + getPoint: { + + /** + * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way + * @param percentage + * @param via + * @returns {{x: number, y: number}} + * @private + */ + value: function getPoint(percentage) { + return { + x: (1 - percentage) * this.from.x + percentage * this.to.x, + y: (1 - percentage) * this.from.y + percentage * this.to.y + }; + }, + writable: true, + configurable: true + }, + _findBorderPosition: { + value: function _findBorderPosition(nearNode, ctx) { + var node1 = this.to; + var node2 = this.from; + if (nearNode.id === this.from.id) { + node1 = this.from; + node2 = this.to; } - return STATE_FAILED; - } - }); - /** - * Pan - * Recognized when the pointer is down and moved in the allowed direction. - * @constructor - * @extends AttrRecognizer - */ - function PanRecognizer() { - AttrRecognizer.apply(this, arguments); + var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); + var dx = node1.x - node2.x; + var dy = node1.y - node2.y; + var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); + var toBorderDist = nearNode.distanceToBorder(ctx, angle); + var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; - this.pX = null; - this.pY = null; - } + var borderPos = {}; + borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; + borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; - inherit(PanRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PanRecognizer - */ - defaults: { - event: 'pan', - threshold: 10, - pointers: 1, - direction: DIRECTION_ALL + return borderPos; + }, + writable: true, + configurable: true }, + _getDistanceToEdge: { + value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { + // x3,y3 is the point + return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); + }, + writable: true, + configurable: true + } + }); - 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; - }, + return StraightEdge; + })(EdgeBase); - 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; + module.exports = StraightEdge; - // 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; - }, +/***/ }, +/* 86 */ +/***/ function(module, exports, __webpack_require__) { - attrTest: function(input) { - return AttrRecognizer.prototype.attrTest.call(this, input) && - (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input))); - }, + "use strict"; - emit: function(input) { - this.pX = input.deltaX; - this.pY = input.deltaY; + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - this._super.emit.call(this, input); - } - }); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * Pinch - * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out). - * @constructor - * @extends AttrRecognizer + * Created by Alex on 2/23/2015. */ - function PinchRecognizer() { - AttrRecognizer.apply(this, arguments); - } - inherit(PinchRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof PinchRecognizer - */ - defaults: { - event: 'pinch', - threshold: 0, - pointers: 2 - }, + var BarnesHutSolver = _interopRequire(__webpack_require__(87)); - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; - }, + var Repulsion = _interopRequire(__webpack_require__(88)); - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN); - }, + var HierarchicalRepulsion = _interopRequire(__webpack_require__(89)); - 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); - } - } - }); + var SpringSolver = _interopRequire(__webpack_require__(90)); - /** - * Press - * Recognized when the pointer is down for x ms without any movement. - * @constructor - * @extends Recognizer - */ - function PressRecognizer() { - Recognizer.apply(this, arguments); + var HierarchicalSpringSolver = _interopRequire(__webpack_require__(91)); - this._timer = null; - this._input = null; - } + var CentralGravitySolver = _interopRequire(__webpack_require__(92)); - 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 util = __webpack_require__(1); - 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; + var PhysicsEngine = (function () { + function PhysicsEngine(body) { + var _this = this; + _classCallCheck(this, PhysicsEngine); - this._input = input; + this.body = body; + this.physicsBody = { physicsNodeIndices: [], physicsEdgeIndices: [], forces: {}, velocities: {} }; - // 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; + this.physicsEnabled = true; + this.simulationInterval = 1000 / 60; + this.requiresTimeout = true; + this.previousStates = {}; + this.freezeCache = {}; + this.renderTimer == undefined; + + this.stabilized = false; + this.stabilizationIterations = 0; + this.ready = false; // will be set to true if the stabilize + + // default options + this.options = {}; + this.defaultOptions = { + barnesHut: { + theta: 0.5, // inverted to save time during calculation + gravitationalConstant: -2000, + centralGravity: 0.3, + springLength: 95, + springConstant: 0.04, + damping: 0.09 + }, + repulsion: { + centralGravity: 0.2, + springLength: 200, + springConstant: 0.05, + nodeDistance: 100, + damping: 0.09 + }, + hierarchicalRepulsion: { + centralGravity: 0, + springLength: 100, + springConstant: 0.01, + nodeDistance: 120, + damping: 0.09 + }, + maxVelocity: 50, + minVelocity: 0.1, // px/s + solver: "BarnesHut", + stabilization: { + enabled: true, + iterations: 1000, // maximum number of iteration to stabilize + updateInterval: 100, + onlyDynamicEdges: false, + zoomExtent: true + }, + timestep: 0.5 + }; + util.extend(this.options, this.defaultOptions); + + this.body.emitter.on("initPhysics", function () { + _this.initPhysics(); + }); + this.body.emitter.on("resetPhysics", function () { + _this.stopSimulation();_this.ready = false; + }); + this.body.emitter.on("disablePhysics", function () { + _this.physicsEnabled = false;_this.stopSimulation(); + }); + this.body.emitter.on("restorePhysics", function () { + _this.setOptions(_this.options); + if (_this.ready === true) { + _this.stabilized = false; + _this.runSimulation(); + } + }); + this.body.emitter.on("startSimulation", function () { + if (_this.ready === true) { + _this.stabilized = false; + _this.runSimulation(); + } + }); + this.body.emitter.on("stopSimulation", function () { + _this.stopSimulation(); + }); + } + + _prototypeProperties(PhysicsEngine, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + if (options === false) { + this.physicsEnabled = false; + this.stopSimulation(); + } else { + this.physicsEnabled = true; + if (options !== undefined) { + util.selectiveNotDeepExtend(["stabilization"], this.options, options); + util.mergeOptions(this.options, options, "stabilization"); + } + this.init(); + } + } + }, + writable: true, + configurable: true + }, + init: { + value: function init() { + var options; + if (this.options.solver == "repulsion") { + options = this.options.repulsion; + this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } else if (this.options.solver == "hierarchicalRepulsion") { + options = this.options.hierarchicalRepulsion; + this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); + this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options); + } else { + // barnesHut + options = this.options.barnesHut; + this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options); + this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); + } + + this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options); + this.modelOptions = options; + }, + writable: true, + configurable: true + }, + initPhysics: { + value: function initPhysics() { + if (this.physicsEnabled === true) { + this.stabilized = false; + if (this.options.stabilization.enabled === true) { + this.stabilize(); + } else { + this.ready = true; + this.body.emitter.emit("zoomExtent", { duration: 0 }, true); + this.runSimulation(); + } + } else { + this.ready = true; + this.body.emitter.emit("_redraw"); } - return STATE_FAILED; - }, - - reset: function() { - clearTimeout(this._timer); + }, + writable: true, + configurable: true }, - - emit: function(input) { - if (this.state !== STATE_RECOGNIZED) { - return; + stopSimulation: { + value: function stopSimulation() { + this.stabilized = true; + if (this.viewFunction !== undefined) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.viewFunction = undefined; + this.body.emitter.emit("_stopRendering"); } - - if (input && (input.eventType & INPUT_END)) { - this.manager.emit(this.options.event + 'up', input); + }, + writable: true, + configurable: true + }, + runSimulation: { + value: function runSimulation() { + if (this.physicsEnabled === true) { + if (this.viewFunction === undefined) { + this.viewFunction = this.simulationStep.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } } else { - this._input.timeStamp = now(); - this.manager.emit(this.options.event, this._input); + this.body.emitter.emit("_redraw"); } - } - }); - - /** - * Rotate - * Recognized when two or more pointer are moving in a circular motion. - * @constructor - * @extends AttrRecognizer - */ - function RotateRecognizer() { - AttrRecognizer.apply(this, arguments); - } - - inherit(RotateRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof RotateRecognizer - */ - defaults: { - event: 'rotate', - threshold: 0, - pointers: 2 - }, - - getTouchAction: function() { - return [TOUCH_ACTION_NONE]; + }, + 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; - attrTest: function(input) { - return this._super.attrTest.call(this, input) && - (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN); - } - }); + // run double speed if it is a little graph + if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { + this.physicsTick(); - /** - * 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 makes sure there is no jitter. The decision is taken once to run it at double speed. + this.runDoubleSpeed = true; + } - inherit(SwipeRecognizer, AttrRecognizer, { - /** - * @namespace - * @memberof SwipeRecognizer - */ - defaults: { - event: 'swipe', - threshold: 10, - velocity: 0.65, - direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL, - pointers: 1 + if (this.stabilized === true) { + if (this.stabilizationIterations > 1) { + // trigger the "stabilized" event. + // The event is triggered on the next tick, to prevent the case that + // it is fired while initializing the Network, in which case you would not + // be able to catch it + var me = this; + var params = { + iterations: this.stabilizationIterations + }; + this.stabilizationIterations = 0; + this.startedStabilization = false; + setTimeout(function () { + me.body.emitter.emit("stabilized", params); + }, 0); + } else { + this.stabilizationIterations = 0; + } + this.stopSimulation(); + } + }, + writable: true, + configurable: true }, + physicsTick: { - getTouchAction: function() { - return PanRecognizer.prototype.getTouchAction.call(this); - }, + /** + * A single simulation step (or "tick") in the physics simulation + * + * @private + */ + value: function physicsTick() { + if (this.stabilized === false) { + this.calculateForces(); + this.stabilized = this.moveNodes(); - attrTest: function(input) { - var direction = this.options.direction; - var velocity; + // determine if the network has stabilzied + if (this.stabilized === true) { + this.revert(); + } else { + // this is here to ensure that there is no start event when the network is already stable. + if (this.startedStabilization == false) { + this.body.emitter.emit("startStabilizing"); + this.startedStabilization = true; + } + } - if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) { - velocity = input.velocity; - } else if (direction & DIRECTION_HORIZONTAL) { - velocity = input.velocityX; - } else if (direction & DIRECTION_VERTICAL) { - velocity = input.velocityY; + this.stabilizationIterations++; } - - return this._super.attrTest.call(this, input) && - direction & input.direction && - input.distance > this.options.threshold && - abs(velocity) > this.options.velocity && input.eventType & INPUT_END; + }, + writable: true, + configurable: true }, + updatePhysicsIndices: { - emit: function(input) { - var direction = directionStr(input.direction); - if (direction) { - this.manager.emit(this.options.event + direction, input); - } + /** + * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also + * handled in the calculateForces function. We then use a quadratic curve with the center node as control. + * This function joins the datanodes and invisible (called support) nodes into one object. + * We do this so we do not contaminate this.body.nodes with the support nodes. + * + * @private + */ + value: function updatePhysicsIndices() { + this.physicsBody.forces = {}; + this.physicsBody.physicsNodeIndices = []; + this.physicsBody.physicsEdgeIndices = []; + var nodes = this.body.nodes; + var edges = this.body.edges; - this.manager.emit(this.options.event, input); - } - }); + // get node indices for physics + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].options.physics === true) { + this.physicsBody.physicsNodeIndices.push(nodeId); + } + } + } - /** - * 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); + // get edge indices for physics + for (var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + if (edges[edgeId].options.physics === true) { + this.physicsBody.physicsEdgeIndices.push(edgeId); + } + } + } - // previous time and center, - // used for tap counting - this.pTime = false; - this.pCenter = false; + // get the velocity and the forces vector + for (var i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) { + var nodeId = this.physicsBody.physicsNodeIndices[i]; + this.physicsBody.forces[nodeId] = { x: 0, y: 0 }; - this._timer = null; - this._input = null; - this.count = 0; - } + // 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 }; + } + } - 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 + // clean deleted nodes from the velocity vector + for (var nodeId in this.physicsBody.velocities) { + if (nodes[nodeId] === undefined) { + delete this.physicsBody.velocities[nodeId]; + } + } + }, + writable: true, + configurable: true }, + revert: { + value: function revert() { + var nodeIds = Object.keys(this.previousStates); + var nodes = this.body.nodes; + var velocities = this.physicsBody.velocities; - getTouchAction: function() { - return [TOUCH_ACTION_MANIPULATION]; + for (var i = 0; i < nodeIds.length; i++) { + var nodeId = nodeIds[i]; + if (nodes[nodeId] !== undefined) { + velocities[nodeId].x = this.previousStates[nodeId].vx; + velocities[nodeId].y = this.previousStates[nodeId].vy; + nodes[nodeId].x = this.previousStates[nodeId].x; + nodes[nodeId].y = this.previousStates[nodeId].y; + } else { + delete this.previousStates[nodeId]; + } + } + }, + writable: true, + configurable: true }, + moveNodes: { + value: function moveNodes() { + var nodesPresent = false; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var maxVelocity = this.options.maxVelocity === 0 ? 1000000000 : this.options.maxVelocity; + var stabilized = true; + var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale, 0.05); - process: function(input) { - var options = this.options; - - var validPointers = input.pointers.length === options.pointers; - var validMovement = input.distance < options.threshold; - var validTouchTime = input.deltaTime < options.time; + 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.reset(); - if ((input.eventType & INPUT_START) && (this.count === 0)) { - return this.failTimeout(); + 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.body.nodes[nodeId]; + var timestep = this.options.timestep; + var forces = this.physicsBody.forces; + var velocities = this.physicsBody.velocities; - // 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(); - } + // store the state so we can revert + this.previousStates[nodeId] = { x: node.x, y: node.y, vx: velocities[nodeId].x, vy: velocities[nodeId].y }; - var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true; - var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold; + if (node.options.fixed.x === false) { + var dx = this.modelOptions.damping * velocities[nodeId].x; // damping force + var ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration + velocities[nodeId].x += ax * timestep; // velocity + velocities[nodeId].x = Math.abs(velocities[nodeId].x) > maxVelocity ? velocities[nodeId].x > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].x; + node.x += velocities[nodeId].x * timestep; // position + } else { + forces[nodeId].x = 0; + velocities[nodeId].x = 0; + } - this.pTime = input.timeStamp; - this.pCenter = input.center; + if (node.options.fixed.y === false) { + var dy = this.modelOptions.damping * velocities[nodeId].y; // damping force + var ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration + velocities[nodeId].y += ay * timestep; // velocity + velocities[nodeId].y = Math.abs(velocities[nodeId].y) > maxVelocity ? velocities[nodeId].y > 0 ? maxVelocity : -maxVelocity : velocities[nodeId].y; + node.y += velocities[nodeId].y * timestep; // position + } else { + forces[nodeId].y = 0; + velocities[nodeId].y = 0; + } - if (!validMultiTap || !validInterval) { - this.count = 1; - } else { - this.count += 1; - } + var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x, 2) + Math.pow(velocities[nodeId].y, 2)); + return totalVelocity; + }, + writable: true, + configurable: true + }, + calculateForces: { + value: function calculateForces() { + this.gravitySolver.solve(); + this.nodesSolver.solve(); + this.edgesSolver.solve(); + }, + writable: true, + configurable: true + }, + _freezeNodes: { - this._input = input; - // if tap count matches we have recognized it, - // else it has began recognizing... - var tapCount = this.count % options.taps; - if (tapCount === 0) { - // no failing requirements, immediately trigger the tap event - // or wait as long as the multitap interval to trigger - if (!this.hasRequireFailures()) { - return STATE_RECOGNIZED; - } else { - this._timer = setTimeoutContext(function() { - this.state = STATE_RECOGNIZED; - this.tryEmit(); - }, options.interval, this); - return STATE_BEGAN; - } + + /** + * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization + * because only the supportnodes for the smoothCurves have to settle. + * + * @private + */ + value: function _freezeNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].x && nodes[id].y) { + this.freezeCache[id] = { x: nodes[id].options.fixed.x, y: nodes[id].options.fixed.y }; + nodes[id].options.fixed.x = true; + nodes[id].options.fixed.y = true; } + } } - return STATE_FAILED; - }, - - failTimeout: function() { - this._timer = setTimeoutContext(function() { - this.state = STATE_FAILED; - }, this.options.interval, this); - return STATE_FAILED; + }, + writable: true, + configurable: true }, + _restoreFrozenNodes: { - reset: function() { - clearTimeout(this._timer); + /** + * Unfreezes the nodes that have been frozen by _freezeDefinedNodes. + * + * @private + */ + value: function _restoreFrozenNodes() { + var nodes = this.body.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (this.freezeCache[id] !== undefined) { + nodes[id].options.fixed.x = this.freezeCache[id].x; + nodes[id].options.fixed.y = this.freezeCache[id].y; + } + } + } + this.freezeCache = {}; + }, + writable: true, + configurable: true }, + stabilize: { - emit: function() { - if (this.state == STATE_RECOGNIZED ) { - this._input.tapCount = this.count; - this.manager.emit(this.options.event, this._input); + /** + * Find a stable position for all nodes + * @private + */ + value: function stabilize() { + if (this.options.stabilization.onlyDynamicEdges == true) { + this._freezeNodes(); } - } - }); - - /** - * Simple way to create an manager with a default set of recognizers. - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor - */ - function Hammer(element, options) { - options = options || {}; - options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset); - return new Manager(element, options); - } - - /** - * @const {string} - */ - Hammer.VERSION = '2.0.4'; - - /** - * default settings - * @namespace - */ - Hammer.defaults = { - /** - * set if DOM events are being triggered. - * But this is slower and unused by simple implementations, so disabled by default. - * @type {Boolean} - * @default false - */ - domEvents: false, - - /** - * The value for the touchAction property/fallback. - * When set to `compute` it will magically set the correct value based on the added recognizers. - * @type {String} - * @default compute - */ - touchAction: TOUCH_ACTION_COMPUTE, - - /** - * @type {Boolean} - * @default true - */ - enable: true, - - /** - * EXPERIMENTAL FEATURE -- can be removed/changed - * Change the parent input target element. - * If Null, then it is being set the to main element. - * @type {Null|EventTarget} - * @default null - */ - inputTarget: null, + this.stabilizationSteps = 0; - /** - * force an input class - * @type {Null|Function} - * @default null - */ - inputClass: null, + 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++; + } - /** - * 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] - ], + 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 }); + } - /** - * 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', + if (this.options.stabilization.onlyDynamicEdges == true) { + this._restoreFrozenNodes(); + } - /** - * Disable the Windows Phone grippers when pressing an element. - * @type {String} - * @default 'none' - */ - touchSelect: 'none', + this.body.emitter.emit("stabilizationIterationsDone"); + this.body.emitter.emit("_requestRedraw"); + this.ready = true; + }, + writable: true, + configurable: true + } + }); - /** - * 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', + return PhysicsEngine; + })(); - /** - * Specifies whether zooming is enabled. Used by IE10> - * @type {String} - * @default 'none' - */ - contentZooming: 'none', + module.exports = PhysicsEngine; - /** - * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers. - * @type {String} - * @default 'none' - */ - userDrag: 'none', +/***/ }, +/* 87 */ +/***/ function(module, exports, __webpack_require__) { - /** - * 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)' - } - }; + "use strict"; - var STOP = 1; - var FORCED_STOP = 2; + 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"); } }; /** - * Manager - * @param {HTMLElement} element - * @param {Object} [options] - * @constructor + * Created by Alex on 2/23/2015. */ - function Manager(element, options) { - options = options || {}; - - this.options = merge(options, Hammer.defaults); - this.options.inputTarget = this.options.inputTarget || element; - this.handlers = {}; - this.session = {}; - this.recognizers = []; + var BarnesHutSolver = (function () { + function BarnesHutSolver(body, physicsBody, options) { + _classCallCheck(this, BarnesHutSolver); - this.element = element; - this.input = createInputInstance(this); - this.touchAction = new TouchAction(this, this.options.touchAction); + this.body = body; + this.physicsBody = physicsBody; + this.barnesHutTree; + this.setOptions(options); + } - toggleCssProps(this, true); + _prototypeProperties(BarnesHutSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + this.thetaInversed = 1 / this.options.theta; + }, + writable: true, + configurable: true + }, + solve: { - each(options.recognizers, function(item) { - var recognizer = this.add(new (item[0])(item[1])); - item[2] && recognizer.recognizeWith(item[2]); - item[3] && recognizer.requireFailure(item[3]); - }, this); - } - Manager.prototype = { - /** - * set options - * @param {Object} options - * @returns {Manager} - */ - set: function(options) { - extend(this.options, options); + /** + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. + * + * @private + */ + value: function solve() { + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var nodeCount = nodeIndices.length; - // 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; - }, + // create the tree + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); - /** - * 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 debugging + this.barnesHutTree = barnesHutTree; - /** - * 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; + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); + } + } } + }, + writable: true, + configurable: true + }, + _getForceContribution: { - // 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; + /** + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node + * @private + */ + value: function _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; - // 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; - } + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); - var i = 0; - while (i < recognizers.length) { - recognizer = recognizers[i]; + // BarnesHutSolver condition + // original condition : s/d < theta = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.thetaInversed) { + // duplicate code to reduce function calls to speed up program + if (distance === 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; - // 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); + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount === 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); } else { - recognizer.reset(); - } + // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { + // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance === 0) { + distance = 0.5 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; - // 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; + this.physicsBody.forces[node.id].x += fx; + this.physicsBody.forces[node.id].y += fy; + } } - i++; + } } + }, + writable: true, + configurable: true }, + _formBarnesHutTree: { - /** - * get a recognizer by its event name. - * @param {Recognizer|String} recognizer - * @returns {Recognizer|Null} - */ - get: function(recognizer) { - if (recognizer instanceof Recognizer) { - return recognizer; - } - var recognizers = this.recognizers; - for (var i = 0; i < recognizers.length; i++) { - if (recognizers[i].options.event == recognizer) { - return recognizers[i]; + /** + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices + * @private + */ + value: function _formBarnesHutTree(nodes, nodeIndices) { + var node; + var nodeCount = nodeIndices.length; + + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; + + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } } - return null; - }, + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize - /** - * 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); - } + var minimumTreeSize = 0.00001; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), + centerY = 0.5 * (minY + maxY); - this.recognizers.push(recognizer); - recognizer.manager = this; + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: { data: null }, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); - this.touchAction.update(); - return recognizer; + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); + } + } + + // make global + return barnesHutTree; + }, + writable: true, + configurable: true }, + _updateBranchMass: { - /** - * 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 updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node + * @private + */ + value: function _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; - this.touchAction.update(); - return this; + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; + + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; + + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + }, + writable: true, + configurable: true }, + _placeInTree: { - /** - * 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; + + /** + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private + */ + value: function _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } + + if (parentBranch.children.NW.range.maxX > node.x) { + // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { + // in NW + this._placeInRegion(parentBranch, node, "NW"); + } else { + // in SW + this._placeInRegion(parentBranch, node, "SW"); + } + } else { + // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { + // in NE + this._placeInRegion(parentBranch, node, "NE"); + } else { + // in SE + this._placeInRegion(parentBranch, node, "SE"); + } + } + }, + writable: true, + configurable: true }, + _placeInRegion: { - /** - * 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]; + + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + value: function _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: + // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); + break; + case 1: + // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x === node.x && parentBranch.children[region].children.data.y === node.y) { + node.x += Math.random(); + node.y += Math.random(); } else { - handlers[event].splice(inArray(handlers[event], handler), 1); + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); } - }); - return this; + break; + case 4: + // place in branch + this._placeInTree(parentBranch.children[region], node); + break; + } + }, + writable: true, + configurable: true }, + _splitBranch: { - /** - * emit event to the listeners - * @param {String} event - * @param {Object} data - */ - emit: function(event, data) { - // we also want to trigger dom events - if (this.options.domEvents) { - triggerDomEvent(event, data); - } - // no handlers, so skip it all - var handlers = this.handlers[event] && this.handlers[event].slice(); - if (!handlers || !handlers.length) { - return; + /** + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch + * @private + */ + value: function _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount === 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); - data.type = event; - data.preventDefault = function() { - data.srcEvent.preventDefault(); - }; - - var i = 0; - while (i < handlers.length) { - handlers[i](data); - i++; + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); } + }, + writable: true, + configurable: true }, + _insertRegion: { - /** - * 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; - } - }; - - /** - * 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); - } + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + value: function _insertRegion(parentBranch, region) { + var minX, maxX, minY, maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } - 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, + parentBranch.children[region] = { + centerOfMass: { x: 0, y: 0 }, + mass: 0, + range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: { data: null }, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; + }, + writable: true, + configurable: true + }, + _debug: { - 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, + //--------------------------- DEBUGGING BELOW ---------------------------// - on: addEventListeners, - off: removeEventListeners, - each: each, - merge: merge, - extend: extend, - inherit: inherit, - bindFn: bindFn, - prefixed: prefixed - }); - if ("function" == TYPE_FUNCTION && __webpack_require__(99)) { - !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { - return Hammer; - }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof module != 'undefined' && module.exports) { - module.exports = Hammer; - } else { - window[exportName] = Hammer; - } + /** + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color + * @private + */ + value: function _debug(ctx, color) { + if (this.barnesHutTree !== undefined) { + ctx.lineWidth = 1; - })(window, document, 'Hammer'); + this._drawBranch(this.barnesHutTree.root, ctx, color); + } + }, + writable: true, + configurable: true + }, + _drawBranch: { -/***/ }, -/* 65 */ -/***/ function(module, exports, __webpack_require__) { + /** + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color + * @private + */ + value: function _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; + } - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict'; + if (branch.childrenCount === 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); - (function (factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - window.propagating = factory(); - } - }(function () { - // will contain the target element where the gesture started - var _firstTarget = null; // singleton + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); - /** - * 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; + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); - 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)); - }; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); - return PropagatingHammer; + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ + }, + writable: true, + configurable: true } + }); - // attach to DOM element - var element = hammer.element; - element.hammer = hammer; - - // move the original functions that we will wrap - hammer._on = hammer.on; - hammer._off = hammer.off; - hammer._emit = hammer.emit; - hammer._destroy = hammer.destroy; - - /** @type {Object.>} */ - hammer._handlers = {}; + return BarnesHutSolver; + })(); - // 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; - } - }); + module.exports = BarnesHutSolver; - /** - * 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 = []; +/***/ }, +/* 88 */ +/***/ function(module, exports, __webpack_require__) { - // register the static, propagated handler - hammer._on(event, propagatedHandler); - } - _handlers.push(handler); - }); + "use strict"; - return hammer; - }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * 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; - }) : []; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - if (_handlers.length > 0) { - hammer._handlers[event] = _handlers; - } - else { - // remove static, propagated handler - hammer._off(event, propagatedHandler); - delete hammer._handlers[event]; - } - } - }); + /** + * Created by Alex on 2/23/2015. + */ - return hammer; - }; + var RepulsionSolver = (function () { + function RepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, RepulsionSolver); - /** - * Emit to the event listeners - * @param {string} eventType - * @param {Event} event - */ - hammer.emit = function(eventType, event) { - _firstTarget = event.target; - hammer._emit(eventType, event); - }; + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - hammer.destroy = function () { - // Detach from DOM element - var element = hammer.element; - delete element.hammer; + _prototypeProperties(RepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2; - // clear all handlers - hammer._handlers = {}; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - // call original hammer destroy - hammer._destroy(); - }; + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; - // split a string with space separated words - function split(events) { - return events.match(/[^ ]+/g); - } + // approximation constants + var a = -2 / 3 / nodeDistance; + var b = 4 / 3; - /** - * 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; - } - } + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (var i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (var j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; - // attach a stopPropagation function to the event - var stopped = false; - event.stopPropagation = function () { - stopped = true; - }; + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); - // attach firstTarget property to the event - event.firstTarget = _firstTarget; + // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } - // 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); + if (distance < 2 * nodeDistance) { + if (distance < 0.5 * nodeDistance) { + repulsingForce = 1; + } else { + repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) + } + repulsingForce = repulsingForce / distance; + + fx = dx * repulsingForce; + fy = dy * repulsingForce; + + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } } } - - elem = elem.parentNode; - } + }, + writable: true, + configurable: true } + }); - return hammer; - }; - })); + return RepulsionSolver; + })(); + module.exports = RepulsionSolver; /***/ }, -/* 66 */ +/* 89 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var util = __webpack_require__(1); + /** + * Created by Alex on 2/23/2015. + */ + + var HierarchicalRepulsionSolver = (function () { + function HierarchicalRepulsionSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalRepulsionSolver); - var Label = _interopRequire(__webpack_require__(80)); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - var Box = _interopRequire(__webpack_require__(81)); + _prototypeProperties(HierarchicalRepulsionSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; + }, + writable: true, + configurable: true + }, + solve: { - var Circle = _interopRequire(__webpack_require__(82)); + /** + * Calculate the forces the nodes apply on each other based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + value: function solve() { + var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; - var CircularImage = _interopRequire(__webpack_require__(83)); + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - var Database = _interopRequire(__webpack_require__(84)); + // repulsing forces between nodes + var nodeDistance = this.options.nodeDistance; - var Diamond = _interopRequire(__webpack_require__(85)); + // we loop from i over all but the last entree in the array + // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j + for (i = 0; i < nodeIndices.length - 1; i++) { + node1 = nodes[nodeIndices[i]]; + for (j = i + 1; j < nodeIndices.length; j++) { + node2 = nodes[nodeIndices[j]]; - var Dot = _interopRequire(__webpack_require__(86)); + // nodes only affect nodes on their level + if (node1.level == node2.level) { + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); - var Ellipse = _interopRequire(__webpack_require__(87)); + var steepness = 0.05; + if (distance < nodeDistance) { + repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); + } else { + repulsingForce = 0; + } + // normalize force with + if (distance == 0) { + distance = 0.01; + } else { + repulsingForce = repulsingForce / distance; + } + fx = dx * repulsingForce; + fy = dy * repulsingForce; - var Icon = _interopRequire(__webpack_require__(88)); + forces[node1.id].x -= fx; + forces[node1.id].y -= fy; + forces[node2.id].x += fx; + forces[node2.id].y += fy; + } + } + } + }, + writable: true, + configurable: true + } + }); - var Image = _interopRequire(__webpack_require__(89)); + return HierarchicalRepulsionSolver; + })(); - var Square = _interopRequire(__webpack_require__(90)); + module.exports = HierarchicalRepulsionSolver; - var Star = _interopRequire(__webpack_require__(91)); +/***/ }, +/* 90 */ +/***/ function(module, exports, __webpack_require__) { - var Text = _interopRequire(__webpack_require__(92)); + "use strict"; - var Triangle = _interopRequire(__webpack_require__(93)); + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var TriangleDown = _interopRequire(__webpack_require__(94)); + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; /** - * @class Node - * A node. A node can be connected to other nodes via one or multiple edges. - * @param {object} options An object containing options for the node. All - * options are optional, except for the id. - * {number} id Id of the node. Required - * {string} label Text label for the node - * {number} x Horizontal position of the node - * {number} y Vertical position of the node - * {string} shape Node shape, available: - * "database", "circle", "ellipse", - * "box", "image", "text", "dot", - * "star", "triangle", "triangleDown", - * "square", "icon" - * {string} image An image url - * {string} title An title text, can be HTML - * {anytype} group A group name or number - * @param {Network.Images} imagelist A list with images. Only needed - * when the node has an image - * @param {Network.Groups} grouplist A list with groups. Needed for - * retrieving group options - * @param {Object} constants An object with default values for - * example for the color - * + * Created by Alex on 2/23/2015. */ - var Node = (function () { - function Node(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Node); - - this.options = util.bridgeObject(globalOptions); - this.body = body; - - this.edges = []; // all edges connected to this node - - // set defaults for the options - this.id = undefined; - this.imagelist = imagelist; - this.grouplist = grouplist; - // state options - this.x = undefined; - this.y = undefined; - this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate - this.selected = false; - this.hover = false; + var SpringSolver = (function () { + function SpringSolver(body, physicsBody, options) { + _classCallCheck(this, SpringSolver); - this.labelModule = new Label(this.body, this.options); + this.body = body; + this.physicsBody = physicsBody; this.setOptions(options); } - _prototypeProperties(Node, null, { - attachEdge: { - - - /** - * Attach a edge to the node - * @param {Edge} edge - */ - value: function attachEdge(edge) { - if (this.edges.indexOf(edge) == -1) { - this.edges.push(edge); - } + _prototypeProperties(SpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - detachEdge: { - + solve: { /** - * Detach a edge from the node - * @param {Edge} edge + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private */ - value: function detachEdge(edge) { - var index = this.edges.indexOf(edge); - if (index != -1) { - this.edges.splice(index, 1); + value: function solve() { + var edgeLength, edge; + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var edges = this.body.edges; + + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + // only calculate forces if nodes are in the same sector + if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { + if (edge.edgeType.via !== undefined) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; + var node1 = edge.to; + var node2 = edge.edgeType.via; + var node3 = edge.from; + + + this._calculateSpringForce(node1, node2, 0.5 * edgeLength); + this._calculateSpringForce(node2, node3, 0.5 * edgeLength); + } else { + // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use + // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. + edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; + this._calculateSpringForce(edge.from, edge.to, edgeLength); + } + } + } } }, writable: true, configurable: true }, - togglePhysics: { + _calculateSpringForce: { + /** - * Enable or disable the physics. - * @param status + * This is the code actually performing the calculation for the function above. + * + * @param node1 + * @param node2 + * @param edgeLength + * @private */ - value: function togglePhysics(status) { - this.options.physics = status; - }, - writable: true, - configurable: true - }, - setOptions: { + value: function _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; + dx = node1.x - node2.x; + dy = node1.y - node2.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; - /** - * Set or overwrite options for the node - * @param {Object} options an object with options - * @param {Object} constants and object with default, global options - */ - value: function setOptions(options) { - if (!options) { - return; - } + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; - var fields = ["borderWidth", "borderWidthSelected", "brokenImage", "customScalingFunction", "font", "hidden", "icon", "id", "image", "label", "level", "physics", "shape", "size", "title", "value", "x", "y"]; - util.selectiveDeepExtend(fields, this.options, options); + fx = dx * springForce; + fy = dy * springForce; - // basic options - if (options.id !== undefined) { - this.id = options.id; + // handle the case where one node is not part of the physcis + if (this.physicsBody.forces[node1.id] !== undefined) { + this.physicsBody.forces[node1.id].x += fx; + this.physicsBody.forces[node1.id].y += fy; } - if (this.id === undefined) { - throw "Node must have an id"; + if (this.physicsBody.forces[node2.id] !== undefined) { + this.physicsBody.forces[node2.id].x -= fx; + this.physicsBody.forces[node2.id].y -= fy; } + }, + writable: true, + configurable: true + } + }); - if (options.x !== undefined) { - this.x = options.x;this.predefinedPosition = true; - } - if (options.y !== undefined) { - this.y = options.y;this.predefinedPosition = true; - } - if (options.value !== undefined) { - this.value = options.value; - } + return SpringSolver; + })(); - // copy group options - if (typeof options.group === "number" || typeof options.group === "string" && options.group != "") { - var groupObj = this.grouplist.get(options.group); - util.deepExtend(this.options, groupObj); - // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case. - this.options.color = util.parseColor(this.options.color); - } - // individual shape options - if (options.color !== undefined) { - this.options.color = util.parseColor(options.color); - } + module.exports = SpringSolver; - 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"; - } - } +/***/ }, +/* 91 */ +/***/ function(module, exports, __webpack_require__) { - if (options.fixed !== undefined) { - if (typeof options.fixed == "boolean") { - this.options.fixed.x = true; - this.options.fixed.y = true; - } else { - if (options.fixed.x !== undefined && typeof options.fixed.x == "boolean") { - this.options.fixed.x = options.fixed.x; - } - if (options.fixed.y !== undefined && typeof options.fixed.y == "boolean") { - this.options.fixed.y = options.fixed.y; - } - } - } + "use strict"; - // choose draw method depending on the shape - switch (this.options.shape) { + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - case "box": - this.shape = new Box(this.options, this.body, this.labelModule); - break; - case "circle": - this.shape = new Circle(this.options, this.body, this.labelModule); - break; - case "circularImage": - this.shape = new CircularImage(this.options, this.body, this.labelModule, this.imageObj); - break; - case "database": - this.shape = new Database(this.options, this.body, this.labelModule); - break; - case "diamond": - this.shape = new Diamond(this.options, this.body, this.labelModule); - break; - case "dot": - this.shape = new Dot(this.options, this.body, this.labelModule); - break; - case "ellipse": - this.shape = new Ellipse(this.options, this.body, this.labelModule); - break; - case "icon": - this.shape = new Icon(this.options, this.body, this.labelModule); - break; - case "image": - this.shape = new Image(this.options, this.body, this.labelModule, this.imageObj); - break; - case "square": - this.shape = new Square(this.options, this.body, this.labelModule); - break; - case "star": - this.shape = new Star(this.options, this.body, this.labelModule); - break; - case "text": - this.shape = new Text(this.options, this.body, this.labelModule); - break; - case "triangle": - this.shape = new Triangle(this.options, this.body, this.labelModule); - break; - case "triangleDown": - this.shape = new TriangleDown(this.options, this.body, this.labelModule); - break; - default: - this.shape = new Ellipse(this.options, this.body, this.labelModule); - break; - } + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - this.labelModule.setOptions(this.options, options); + /** + * Created by Alex on 2/25/2015. + */ - // reset the size of the node, this can be changed - this._reset(); - }, - writable: true, - configurable: true - }, - select: { + var HierarchicalSpringSolver = (function () { + function HierarchicalSpringSolver(body, physicsBody, options) { + _classCallCheck(this, HierarchicalSpringSolver); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - /** - * select this node - */ - value: function select() { - this.selected = true; - this._reset(); + _prototypeProperties(HierarchicalSpringSolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - unselect: { - + solve: { /** - * unselect this node + * This function calculates the springforces on the nodes, accounting for the support nodes. + * + * @private */ - value: function unselect() { - this.selected = false; - this._reset(); - }, - writable: true, - configurable: true - }, - _reset: { + value: function solve() { + var edgeLength, edge; + var dx, dy, fx, fy, springForce, distance; + var edges = this.body.edges; + var factor = 0.5; + + var edgeIndices = this.physicsBody.physicsEdgeIndices; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; + + // initialize the spring force counters + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].springFx = 0; + forces[nodeId].springFy = 0; + } + // forces caused by the edges, modelled as springs + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - /** - * Reset the calculated size of the node, forces it to recalculate its size - * @private - */ - value: function _reset() { - this.shape.width = undefined; - this.shape.height = undefined; - }, - writable: true, - configurable: true - }, - getTitle: { + dx = edge.from.x - edge.to.x; + dy = edge.from.y - edge.to.y; + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; - /** - * get the title of this node. - * @return {string} title The title of the node, or undefined when no title - * has been set. - */ - value: function getTitle() { - return typeof this.options.title === "function" ? this.options.title() : this.options.title; - }, - writable: true, - configurable: true - }, - distanceToBorder: { + fx = dx * springForce; + fy = dy * springForce; + + if (edge.to.level != edge.from.level) { + forces[edge.toId].springFx -= fx; + forces[edge.toId].springFy -= fy; + forces[edge.fromId].springFx += fx; + forces[edge.fromId].springFy += fy; + } else { + forces[edge.toId].x -= factor * fx; + forces[edge.toId].y -= factor * fy; + forces[edge.fromId].x += factor * fx; + forces[edge.fromId].y += factor * fy; + } + } + } + + // normalize spring forces + var springForce = 1; + var springFx, springFy; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); + springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); + forces[nodeId].x += springFx; + forces[nodeId].y += springFy; + } - /** - * Calculate the distance to the border of the Node - * @param {CanvasRenderingContext2D} ctx - * @param {Number} angle Angle in radians - * @returns {number} distance Distance to the border in pixels - */ - value: function distanceToBorder(ctx, angle) { - return this.shape.distanceToBorder(ctx, angle); + // retain energy balance + var totalFx = 0; + var totalFy = 0; + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + totalFx += forces[nodeId].x; + totalFy += forces[nodeId].y; + } + var correctionFx = totalFx / nodeIndices.length; + var correctionFy = totalFy / nodeIndices.length; + + for (var i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + forces[nodeId].x -= correctionFx; + forces[nodeId].y -= correctionFy; + } }, writable: true, configurable: true - }, - isFixed: { + } + }); + return HierarchicalSpringSolver; + })(); - /** - * Check if this node has a fixed x and y position - * @return {boolean} true if fixed, false if not - */ - value: function isFixed() { - return this.options.fixed.x && this.options.fixed.y; - }, - writable: true, - configurable: true - }, - isSelected: { + module.exports = HierarchicalSpringSolver; +/***/ }, +/* 92 */ +/***/ function(module, exports, __webpack_require__) { - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - }, - writable: true, - configurable: true - }, - getValue: { + "use strict"; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - /** - * Retrieve the value of the node. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.value; - }, - writable: true, - configurable: true - }, - setValueRange: { + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * Created by Alex on 2/23/2015. + */ - /** - * Adjust the value range of the node. The node will adjust it's size - * based on its value. - * @param {Number} min - * @param {Number} max - */ - value: function setValueRange(min, max, total) { - if (this.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); - var sizeDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled == true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.size = this.options.scaling.min + scale * sizeDiff; - } - }, - writable: true, - configurable: true - }, - draw: { + var CentralGravitySolver = (function () { + function CentralGravitySolver(body, physicsBody, options) { + _classCallCheck(this, CentralGravitySolver); + this.body = body; + this.physicsBody = physicsBody; + this.setOptions(options); + } - /** - * Draw this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - this.shape.draw(ctx, this.x, this.y, this.selected, this.hover); + _prototypeProperties(CentralGravitySolver, null, { + setOptions: { + value: function setOptions(options) { + this.options = options; }, writable: true, configurable: true }, - resize: { + solve: { + value: function solve() { + var dx, dy, distance, node, i; + var nodes = this.body.nodes; + var nodeIndices = this.physicsBody.physicsNodeIndices; + var forces = this.physicsBody.forces; - /** - * Recalculate the size of this node in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function resize(ctx) { - this.shape.resize(ctx); - }, - writable: true, - configurable: true - }, - isOverlappingWith: { + var gravity = this.options.centralGravity; + var gravityForce = 0; + for (i = 0; i < nodeIndices.length; i++) { + var nodeId = nodeIndices[i]; + node = nodes[nodeId]; + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top, right, bottom - * @return {boolean} True if location is located on node - */ - value: function isOverlappingWith(obj) { - return this.shape.left < obj.right && this.shape.left + this.shape.width > obj.left && this.shape.top < obj.bottom && this.shape.top + this.shape.height > obj.top; + gravityForce = distance == 0 ? 0 : gravity / distance; + forces[nodeId].x = dx * gravityForce; + forces[nodeId].y = dy * gravityForce; + } }, writable: true, configurable: true } }); - return Node; + return CentralGravitySolver; })(); - module.exports = Node; + module.exports = CentralGravitySolver; /***/ }, -/* 67 */ +/* 93 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -31535,492 +31240,730 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var util = __webpack_require__(1); - + /** + * Created by Alex on 24-Feb-15. + */ - var Label = _interopRequire(__webpack_require__(80)); + var util = __webpack_require__(1); + var Cluster = _interopRequire(__webpack_require__(94)); - var BezierEdgeDynamic = _interopRequire(__webpack_require__(95)); + var ClusterEngine = (function () { + function ClusterEngine(body) { + _classCallCheck(this, ClusterEngine); - var BezierEdgeStatic = _interopRequire(__webpack_require__(96)); + this.body = body; + this.clusteredNodes = {}; - var StraightEdge = _interopRequire(__webpack_require__(97)); + this.options = {}; + this.defaultOptions = {}; + util.extend(this.options, this.defaultOptions); + } - /** - * @class Edge - * - * A edge connects two nodes - * @param {Object} properties Object with options. Must contain - * At least options from and to. - * Available options: from (number), - * to (number), label (string, color (string), - * width (number), style (string), - * length (number), title (string) - * @param {Network} network A Network object, used to find and edge to - * nodes. - * @param {Object} constants An object with default values for - * example for the color - */ - var Edge = (function () { - function Edge(options, body, globalOptions) { - _classCallCheck(this, Edge); + _prototypeProperties(ClusterEngine, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) {} + }, + writable: true, + configurable: true + }, + clusterByConnectionCount: { - if (body === undefined) { - throw "No body provided"; - } - this.options = util.bridgeObject(globalOptions); - this.body = body; + /** + * + * @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(); + } - // initialize variables - this.id = undefined; - this.fromId = undefined; - this.toId = undefined; - this.value = undefined; - this.selected = false; - this.hover = false; - this.labelDirty = true; - this.colorDirty = true; + 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); + } + } - this.from = undefined; // a node - this.to = undefined; // a node + for (var i = 0; i < nodesToCluster.length; i++) { + var node = this.body.nodes[nodesToCluster[i]]; + this.clusterByConnection(node, options, {}, {}, false); + } + this.body.emitter.emit("_dataChanged"); + }, + writable: true, + configurable: true + }, + clusterByNodeData: { - this.edgeType = undefined; - this.connected = false; + /** + * loop over all nodes, check if they adhere to the condition and cluster if needed. + * @param options + * @param refreshData + */ + value: function clusterByNodeData() { + var options = arguments[0] === undefined ? {} : arguments[0]; + var refreshData = arguments[1] === undefined ? true : arguments[1]; + if (options.joinCondition === undefined) { + throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options."); + } - this.labelModule = new Label(this.body, this.options); + // check if the options object is fine, append if needed + options = this._checkOptions(options); - this.setOptions(options); + var childNodesObj = {}; + var childEdgesObj = {}; - this.controlNodesEnabled = false; - this.controlNodes = { from: undefined, to: undefined, positions: {} }; - this.connectedNode = undefined; - } + // 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]; + } + } - _prototypeProperties(Edge, null, { - setOptions: { + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + }, + writable: true, + configurable: true + }, + clusterOutliers: { /** - * Set or overwrite options for the edge - * @param {Object} options an object with options - * @param doNotEmit - */ - value: function setOptions(options) { - if (!options) { - return; + * Cluster all nodes in the network that have only 1 edge + * @param options + * @param refreshData + */ + value: function clusterOutliers(options) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + options = this._checkOptions(options); + var clusters = []; + + // 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 }); + } + } } - this.colorDirty = true; - var fields = ["id", "font", "from", "hidden", "hoverWidth", "label", "length", "line", "opacity", "physics", "scaling", "selfReferenceSize", "to", "title", "value", "width", "widthMin", "widthMax", "widthSelectionMultiplier"]; - util.selectiveDeepExtend(fields, this.options, options); + for (var i = 0; i < clusters.length; i++) { + this._cluster(clusters[i].nodes, clusters[i].edges, options, false); + } - util.mergeOptions(this.options, options, "smooth"); - util.mergeOptions(this.options, options, "dashes"); + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } + }, + writable: true, + configurable: true + }, + clusterByConnection: { - if (options.id !== undefined) { - this.id = options.id; + /** + * + * @param nodeId + * @param options + * @param refreshData + */ + value: function clusterByConnection(nodeId, options) { + var refreshData = arguments[2] === undefined ? true : arguments[2]; + // kill conditions + if (nodeId === undefined) { + throw new Error("No nodeId supplied to clusterByConnection!"); } - if (options.from !== undefined) { - this.fromId = options.from; + if (this.body.nodes[nodeId] === undefined) { + throw new Error("The nodeId given to clusterByConnection does not exist!"); } - if (options.to !== undefined) { - this.toId = options.to; + + var node = this.body.nodes[nodeId]; + options = this._checkOptions(options, node); + if (options.clusterNodeProperties.x === undefined) { + options.clusterNodeProperties.x = node.x; } - if (options.title !== undefined) { - this.title = options.title; + if (options.clusterNodeProperties.y === undefined) { + options.clusterNodeProperties.y = node.y; } - if (options.value !== undefined) { - this.value = options.value; + if (options.clusterNodeProperties.fixed === undefined) { + options.clusterNodeProperties.fixed = {}; + options.clusterNodeProperties.fixed.x = node.options.fixed.x; + options.clusterNodeProperties.fixed.y = node.options.fixed.y; } - // hanlde multiple input cases for arrows - if (options.arrows !== undefined) { - if (typeof options.arrows === "string") { - var arrows = options.arrows.toLowerCase(); - if (arrows.indexOf("to") != -1) { - this.options.arrows.to.enabled = true; - } - if (arrows.indexOf("middle") != -1) { - this.options.arrows.middle.enabled = true; - } - if (arrows.indexOf("from") != -1) { - this.options.arrows.from.enabled = true; - } - } else if (typeof options.arrows === "object") { - util.mergeOptions(this.options.arrows, options.arrows, "to"); - util.mergeOptions(this.options.arrows, options.arrows, "middle"); - util.mergeOptions(this.options.arrows, options.arrows, "from"); - } else { - throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows)); - } - } - // hanlde multiple input cases for color - if (options.color !== undefined) { - if (util.isString(options.color)) { - util.assignAllKeys(this.options.color, options.color); - this.options.color.inherit.enabled = false; - } else { - util.extend(this.options.color, options.color); - if (options.color.inherit === undefined) { - this.options.color.inherit.enabled = false; + 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; } - util.mergeOptions(this.options.color, options.color, "inherit"); } - // A node is connected when it has a from and to node that both exist in the network.body.nodes. - this.connect(); + this._cluster(childNodesObj, childEdgesObj, options, refreshData); + }, + writable: true, + configurable: true + }, + _cloneOptions: { - this.labelModule.setOptions(this.options); - var dataChanged = this.updateEdgeType(); - return dataChanged; + /** + * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. + * @param objId + * @param type + * @returns {{}} + * @private + */ + value: function _cloneOptions(objId, type) { + var clonedOptions = {}; + if (type === undefined || type == "node") { + util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); + util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); + clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; + } else { + util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); + } + return clonedOptions; }, writable: true, configurable: true }, - updateEdgeType: { - value: function updateEdgeType() { - var dataChanged = false; - var changeInType = true; - if (this.edgeType !== undefined) { - if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { - changeInType = false; - } - if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false) { - changeInType = false; - } - if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) { - changeInType = false; - } + _createClusterEdges: { - if (changeInType == true) { - dataChanged = this.edgeType.cleanup(); - } - } - if (changeInType === true) { - if (this.options.smooth.enabled === true) { - if (this.options.smooth.dynamic === true) { - dataChanged = true; - this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); - } else { - this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); + /** + * This function creates the edges that will be attached to the cluster. + * + * @param childNodesObj + * @param childEdgesObj + * @param newEdges + * @param options + * @private + */ + value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) { + var edge, childNodeId, childNode; + + var childKeys = Object.keys(childNodesObj); + for (var i = 0; i < childKeys.length; i++) { + childNodeId = childKeys[i]; + childNode = childNodesObj[childNodeId]; + + // mark all edges for removal from global and construct new edges from the cluster to others + for (var j = 0; j < childNode.edges.length; j++) { + edge = childNode.edges[j]; + childEdgesObj[edge.id] = edge; + + var otherNodeId = edge.toId; + var otherOnTo = true; + if (edge.toId != childNodeId) { + otherNodeId = edge.toId; + otherOnTo = true; + } else if (edge.fromId != childNodeId) { + otherNodeId = edge.fromId; + otherOnTo = false; + } + + if (childNodesObj[otherNodeId] === undefined) { + var clonedOptions = this._cloneOptions(edge.id, "edge"); + util.deepExtend(clonedOptions, options.clusterEdgeProperties); + if (otherOnTo === true) { + clonedOptions.from = options.clusterNodeProperties.id; + clonedOptions.to = otherNodeId; + } else { + clonedOptions.from = otherNodeId; + clonedOptions.to = options.clusterNodeProperties.id; + } + clonedOptions.id = "clusterEdge:" + util.randomUUID(); + newEdges.push(this.body.functions.createEdge(clonedOptions)); } - } else { - this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); } - } else { - // if nothing changes, we just set the options. - this.edgeType.setOptions(this.options); } - - return dataChanged; }, writable: true, configurable: true }, - togglePhysics: { + _checkOptions: { /** - * Enable or disable the physics. - * @param status - */ - value: function togglePhysics(status) { - if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) { - if (this.via === undefined) { - this.via.pptions.physics = status; - } + * This 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 = {}; } - this.options.physics = status; + if (options.clusterNodeProperties === undefined) { + options.clusterNodeProperties = {}; + } + + return options; }, writable: true, configurable: true }, - connect: { + _cluster: { /** - * Connect an edge to its nodes - */ - value: function connect() { - this.disconnect(); + * + * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node + * @param {Object} childEdgesObj | object with edge objects, id as keys + * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} + * @param {Boolean} refreshData | when true, do not wrap up + * @private + */ + value: function _cluster(childNodesObj, childEdgesObj, options) { + var refreshData = arguments[3] === undefined ? true : arguments[3]; + // kill condition: no children so cant cluster + if (Object.keys(childNodesObj).length == 0) { + return; + } - this.from = this.body.nodes[this.fromId] || undefined; - this.to = this.body.nodes[this.toId] || undefined; - this.connected = this.from !== undefined && this.to !== undefined; + // check if we have an unique id; + if (options.clusterNodeProperties.id === undefined) { + options.clusterNodeProperties.id = "cluster:" + util.randomUUID(); + } + var clusterId = options.clusterNodeProperties.id; - if (this.connected === true) { - this.from.attachEdge(this); - this.to.attachEdge(this); - } else { - if (this.from) { - this.from.detachEdge(this); + // 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); } - if (this.to) { - this.to.detachEdge(this); + + // 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!"); } } - }, - writable: true, - configurable: true - }, - disconnect: { + if (clusterNodeProperties.label === undefined) { + clusterNodeProperties.label = "cluster"; + } - /** - * Disconnect an edge from its nodes - */ - value: function disconnect() { - if (this.from) { - this.from.detachEdge(this); - this.from = undefined; + // 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 (this.to) { - this.to.detachEdge(this); - this.to = undefined; + if (clusterNodeProperties.x === undefined) { + if (pos === undefined) { + pos = this._getClusterPosition(childNodesObj); + } + clusterNodeProperties.y = pos.y; + clusterNodeProperties.allowedToMoveY = true; } - this.connected = false; - }, - writable: true, - configurable: true - }, - getTitle: { + // force the ID to remain the same + clusterNodeProperties.id = clusterId; - /** - * get the title of this edge. - * @return {string} title The title of the edge, or undefined when no title - * has been set. - */ - value: function getTitle() { - return typeof this.title === "function" ? this.title() : this.title; - }, - writable: true, - configurable: true - }, - isSelected: { + // create the clusterNode + var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); + clusterNode.isCluster = true; + clusterNode.containedNodes = childNodesObj; + clusterNode.containedEdges = childEdgesObj; - /** - * check if this node is selecte - * @return {boolean} selected True if node is selected, else false - */ - value: function isSelected() { - return this.selected; - }, - writable: true, - configurable: true - }, - getValue: { + // disable the childEdges + for (var edgeId in childEdgesObj) { + if (childEdgesObj.hasOwnProperty(edgeId)) { + if (this.body.edges[edgeId] !== undefined) { + var edge = this.body.edges[edgeId]; + edge.togglePhysics(false); + edge.options.hidden = true; + } + } + } - /** - * Retrieve the value of the edge. Can be undefined - * @return {Number} value - */ - value: function getValue() { - return this.value; + // disable the childNodes + for (var nodeId in childNodesObj) { + if (childNodesObj.hasOwnProperty(nodeId)) { + this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] }; + this.body.nodes[nodeId].togglePhysics(false); + this.body.nodes[nodeId].options.hidden = true; + } + } + + + // 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(); + } + + // set ID to undefined so no duplicates arise + clusterNodeProperties.id = undefined; + + + // wrap up + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); + } }, writable: true, configurable: true }, - setValueRange: { + isCluster: { /** - * Adjust the value range of the edge. The edge will adjust it's width - * based on its value. - * @param {Number} min - * @param {Number} max - * @param total - */ - value: function setValueRange(min, max, total) { - if (this.value !== undefined) { - var scale = this.options.scaling.customScalingFunction(min, max, total, this.value); - var widthDiff = this.options.scaling.max - this.options.scaling.min; - if (this.options.scaling.label.enabled == true) { - var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min; - this.options.font.size = this.options.scaling.label.min + scale * fontDiff; - } - this.options.width = this.options.scaling.min + scale * widthDiff; + * Check if a node is a cluster. + * @param nodeId + * @returns {*} + */ + value: function isCluster(nodeId) { + if (this.body.nodes[nodeId] !== undefined) { + return this.body.nodes[nodeId].isCluster === true; + } else { + console.log("Node does not exist."); + return false; } }, writable: true, configurable: true }, - draw: { - + _getClusterPosition: { /** - * Redraw a edge - * Draw this edge in the given canvas - * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); - * @param {CanvasRenderingContext2D} ctx - */ - value: function draw(ctx) { - var via = this.edgeType.drawLine(ctx, this.selected, this.hover); - this.drawArrows(ctx, via); - this.drawLabel(ctx, via); + * 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 }, - drawArrows: { - value: function drawArrows(ctx, viaNode) { - if (this.options.arrows.from.enabled === true) { - this.edgeType.drawArrowHead(ctx, "from", viaNode, this.selected, this.hover); - } - if (this.options.arrows.middle.enabled === true) { - this.edgeType.drawArrowHead(ctx, "middle", viaNode, this.selected, this.hover); + openCluster: { + + + /** + * Open a cluster by calling this function. + * @param {String} clusterNodeId | the ID of the cluster node + * @param {Boolean} refreshData | wrap up afterwards if not true + */ + value: function openCluster(clusterNodeId) { + var refreshData = arguments[1] === undefined ? true : arguments[1]; + // kill conditions + if (clusterNodeId === undefined) { + throw new Error("No clusterNodeId supplied to openCluster."); } - if (this.options.arrows.to.enabled === true) { - this.edgeType.drawArrowHead(ctx, "to", viaNode, this.selected, this.hover); + if (this.body.nodes[clusterNodeId] === undefined) { + throw new Error("The clusterNodeId supplied to openCluster does not exist."); } - }, - writable: true, - configurable: true - }, - drawLabel: { - value: function drawLabel(ctx, viaNode) { - if (this.options.label !== undefined) { - // set style - var node1 = this.from; - var node2 = this.to; - var selected = this.from.selected || this.to.selected || this.selected; - if (node1.id != node2.id) { - var point = this.edgeType.getPoint(0.5, viaNode); - ctx.save(); + if (this.body.nodes[clusterNodeId].containedNodes === undefined) { + console.log("The node:" + clusterNodeId + " is not a cluster.");return; + }; - // if the label has to be rotated: - if (this.options.font.align !== "horizontal") { - this.labelModule.calculateLabelSize(ctx, selected, point.x, point.y); - ctx.translate(point.x, this.labelModule.size.yLine); - this._rotateForLabelAlignment(ctx); - } + var clusterNode = this.body.nodes[clusterNodeId]; + var containedNodes = clusterNode.containedNodes; + var containedEdges = clusterNode.containedEdges; - // draw the label - this.labelModule.draw(ctx, point.x, point.y, selected); - ctx.restore(); - } else { - var x, y; - var radius = this.options.selfReferenceSize; - if (node1.width > node1.height) { - x = node1.x + node1.width * 0.5; - y = node1.y - radius; - } else { - x = node1.x + radius; - y = node1.y - node1.height * 0.5; - } - point = this._pointOnCircle(x, y, radius, 0.125); + // release nodes + for (var nodeId in containedNodes) { + if (containedNodes.hasOwnProperty(nodeId)) { + var containedNode = this.body.nodes[nodeId]; + containedNode = containedNodes[nodeId]; + // inherit position + containedNode.x = clusterNode.x; + containedNode.y = clusterNode.y; - this.labelModule.draw(ctx, point.x, point.y, selected); + // inherit speed + containedNode.vx = clusterNode.vx; + containedNode.vy = clusterNode.vy; + + containedNode.options.hidden = false; + containedNode.togglePhysics(true); + + delete this.clusteredNodes[nodeId]; + } + } + + // release edges + for (var edgeId in containedEdges) { + if (containedEdges.hasOwnProperty(edgeId)) { + var edge = this.body.edges[edgeId]; + edge.options.hidden = false; + edge.togglePhysics(true); + } + } + + // remove all temporary edges + for (var i = 0; i < clusterNode.edges.length; i++) { + var edgeId = clusterNode.edges[i].id; + var viaId = this.body.edges[edgeId].via.id; + if (viaId) { + this.body.edges[edgeId].via = undefined; + delete this.body.nodes[viaId]; } + // this removes the edge from node.edges, which is why edgeIds is formed + this.body.edges[edgeId].disconnect(); + delete this.body.edges[edgeId]; + } + + // remove clusterNode + delete this.body.nodes[clusterNodeId]; + + if (refreshData === true) { + this.body.emitter.emit("_dataChanged"); } }, writable: true, configurable: true }, - isOverlappingWith: { - + _connectEdge: { - /** - * Check if this object is overlapping with the provided object - * @param {Object} obj an object with parameters left, top - * @return {boolean} True if location is located on the edge - */ - value: function isOverlappingWith(obj) { - if (this.connected) { - var distMax = 10; - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; - var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - return dist < distMax; + /** + * 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 { - return false; + edge.to = clusterStack[clusterStack.length - 1]; + edge.toId = clusterStack[clusterStack.length - 1].id; + clusterStack.pop(); + edge.toArray = clusterStack; } + edge.connect(); }, writable: true, configurable: true }, - _rotateForLabelAlignment: { - + _getClusterStack: { /** - * Rotates the canvas so the text is most readable - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _rotateForLabelAlignment(ctx) { - var dy = this.from.y - this.to.y; - var dx = this.from.x - this.to.x; - var angleInDegrees = Math.atan2(dy, dx); + * 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; - // rotate so label it is readable - if (angleInDegrees < -1 && dx < 0 || angleInDegrees > 0 && dx < 0) { - angleInDegrees = angleInDegrees + Math.PI; + while (this.clusteredNodes[nodeId] !== undefined && counter < max) { + stack.push(this.clusteredNodes[nodeId].node); + nodeId = this.clusteredNodes[nodeId].clusterId; + counter++; } - - ctx.rotate(angleInDegrees); + stack.push(this.body.nodes[nodeId]); + return stack; }, writable: true, configurable: true }, - _pointOnCircle: { + _getConnectedId: { /** - * Get a point on a circle - * @param {Number} x - * @param {Number} y - * @param {Number} radius - * @param {Number} percentage. Value between 0 (line start) and 1 (line end) - * @return {Object} point - * @private - */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; - }, - writable: true, - configurable: true - }, - select: { - value: function select() { - this.selected = true; + * 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 }, - unselect: { - value: function unselect() { - this.selected = false; + _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); + + // always have at least one to cluster + if (hubThreshold > largestHub) { + hubThreshold = largestHub; + } + + return hubThreshold; }, writable: true, configurable: true } }); - return Edge; + return ClusterEngine; })(); - module.exports = Edge; + module.exports = ClusterEngine; /***/ }, -/* 68 */ +/* 94 */ +/***/ function(module, exports, __webpack_require__) { + + "use strict"; + + var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + + var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + + var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + + var Node = _interopRequire(__webpack_require__(60)); + + /** + * + */ + var Cluster = (function (Node) { + function Cluster(options, body, imagelist, grouplist, globalOptions) { + _classCallCheck(this, Cluster); + + _get(Object.getPrototypeOf(Cluster.prototype), "constructor", this).call(this, options, body, imagelist, grouplist, globalOptions); + + this.isCluster = true; + this.containedNodes = {}; + this.containedEdges = {}; + } + + _inherits(Cluster, Node); + + return Cluster; + })(Node); + + module.exports = Cluster; + +/***/ }, +/* 95 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32030,506 +31973,326 @@ 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/23/2015. + * Created by Alex on 26-Feb-15. */ - var BarnesHutSolver = (function () { - function BarnesHutSolver(body, physicsBody, options) { - _classCallCheck(this, BarnesHutSolver); + 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.physicsBody = physicsBody; - this.barnesHutTree; - this.setOptions(options); - } + this.canvas = canvas; - _prototypeProperties(BarnesHutSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - }, - writable: true, - configurable: true - }, - solve: { + this.redrawRequested = false; + this.renderTimer = false; + this.requiresTimeout = true; + this.renderingActive = false; + this.renderRequests = 0; + this.pixelRatio = undefined; + // redefined in this._redraw + this.canvasTopLeft = { x: 0, y: 0 }; + this.canvasBottomRight = { x: 0, y: 0 }; - /** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. - * - * @private - */ - value: function solve() { - if (this.options.gravitationalConstant != 0) { - var node; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var nodeCount = nodeIndices.length; + this.dragging = false; - // create the tree - var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + this.body.emitter.on("dragStart", function () { + _this.dragging = true; + }); + this.body.emitter.on("dragEnd", function () { + return _this.dragging = false; + }); + this.body.emitter.on("_redraw", function () { + if (_this.renderingActive === false) { + _this._redraw(); + } + }); + this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); + this.body.emitter.on("_startRendering", function () { + _this.renderRequests += 1;_this.renderingActive = true;_this.startRendering(); + }); + this.body.emitter.on("_stopRendering", function () { + _this.renderRequests -= 1;_this.renderingActive = _this.renderRequests > 0; + }); - // for debugging - this.barnesHutTree = barnesHutTree; + this.options = {}; + this.defaultOptions = { + hideEdgesOnDrag: false, + hideNodesOnDrag: false + }; + util.extend(this.options, this.defaultOptions); - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHutSolver condition - this._getForceContribution(barnesHutTree.root.children.NW, node); - this._getForceContribution(barnesHutTree.root.children.NE, node); - this._getForceContribution(barnesHutTree.root.children.SW, node); - this._getForceContribution(barnesHutTree.root.children.SE, node); - } - } + this._determineBrowserMethod(); + } + + _prototypeProperties(CanvasRenderer, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); } }, writable: true, configurable: true }, - _getForceContribution: { - - - /** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. - * - * @param parentBranch - * @param node - * @private - */ - value: function _getForceContribution(parentBranch, node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx, dy, distance; - - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // BarnesHutSolver condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.options.thetaInverted) { - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.1 * Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; - } else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { - this._getForceContribution(parentBranch.children.NW, node); - this._getForceContribution(parentBranch.children.NE, node); - this._getForceContribution(parentBranch.children.SW, node); - this._getForceContribution(parentBranch.children.SE, node); + startRendering: { + value: function startRendering() { + if (this.renderingActive === true) { + if (!this.renderTimer) { + if (this.requiresTimeout == true) { + this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function } else { - // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { - // if it is not self - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.5 * Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - - this.physicsBody.forces[node.id].x += fx; - this.physicsBody.forces[node.id].y += fy; - } + this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function } } - } + } else {} }, writable: true, configurable: true }, - _formBarnesHutTree: { - - - /** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. - * - * @param nodes - * @param nodeIndices - * @private - */ - value: function _formBarnesHutTree(nodes, nodeIndices) { - var node; - var nodeCount = nodeIndices.length; - - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX = -Number.MAX_VALUE, - maxY = -Number.MAX_VALUE; + renderStep: { + value: function renderStep() { + // reset the renderTimer so a new scheduled animation step can be set + this.renderTimer = undefined; - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { - minX = x; - } - if (x > maxX) { - maxX = x; - } - if (y < minY) { - minY = y; - } - if (y > maxY) { - maxY = y; - } - } + if (this.requiresTimeout == true) { + // this schedules a new simulation step + this.startRendering(); } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) { - minY -= 0.5 * sizeDiff; - maxY += 0.5 * sizeDiff; - } // xSize > ySize - else { - minX += 0.5 * sizeDiff; - maxX -= 0.5 * sizeDiff; - } // xSize < ySize - - var minimumTreeSize = 0.00001; - var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), - centerY = 0.5 * (minY + maxY); - - // construct the barnesHutTree - var barnesHutTree = { - root: { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { - minX: centerX - halfRootSize, maxX: centerX + halfRootSize, - minY: centerY - halfRootSize, maxY: centerY + halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data: null }, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); + this._redraw(); - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root, node); - } + if (this.requiresTimeout == false) { + // this schedules a new simulation step + this.startRendering(); } - - // make global - return barnesHutTree; }, writable: true, configurable: true }, - _updateBranchMass: { - + redraw: { /** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private + * Redraw the network with the current data + * chart will be resized too. */ - value: function _updateBranchMass(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1 / totalMass; - - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; - - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; - - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); - parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth; + value: function redraw() { + this.setSize(this.constants.width, this.constants.height); + this._redraw(); }, writable: true, configurable: true }, - _placeInTree: { - + _requestRedraw: { /** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate + * 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 _placeInTree(parentBranch, node, skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch, node); - } - - if (parentBranch.children.NW.range.maxX > node.x) { - // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { - // in NW - this._placeInRegion(parentBranch, node, "NW"); - } else { - // in SW - this._placeInRegion(parentBranch, node, "SW"); - } - } else { - // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { - // in NE - this._placeInRegion(parentBranch, node, "NE"); + value: function _requestRedraw() { + if (this.redrawRequested !== true && this.renderingActive === false) { + this.redrawRequested = true; + if (this.requiresTimeout === true) { + window.setTimeout(this._redraw.bind(this, false), 0); } else { - // in SE - this._placeInRegion(parentBranch, node, "SE"); + window.requestAnimationFrame(this._redraw.bind(this, false)); } } }, writable: true, configurable: true }, - _placeInRegion: { + _redraw: { + value: function _redraw() { + var hidden = arguments[0] === undefined ? false : arguments[0]; + this.body.emitter.emit("initRedraw"); + this.redrawRequested = false; + var ctx = this.canvas.frame.canvas.getContext("2d"); - /** - * actually place the node in a region (or branch) - * - * @param parentBranch - * @param node - * @param region - * @private - */ - value: function _placeInRegion(parentBranch, node, region) { - switch (parentBranch.children[region].childrenCount) { - case 0: - // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region], node); - break; - case 1: - // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && parentBranch.children[region].children.data.y == node.y) { - node.x += Math.random(); - node.y += Math.random(); - } else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region], node); - } - break; - case 4: - // place in branch - this._placeInTree(parentBranch.children[region], node); - break; + if (this.pixelRation === undefined) { + this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1); + } + + ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + + // clear the canvas + var w = this.canvas.frame.canvas.clientWidth; + var h = this.canvas.frame.canvas.clientHeight; + ctx.clearRect(0, 0, w, h); + + this.body.emitter.emit("beforeDrawing", ctx); + + // set scaling and translation + ctx.save(); + ctx.translate(this.body.view.translation.x, this.body.view.translation.y); + ctx.scale(this.body.view.scale, this.body.view.scale); + + this.canvasTopLeft = this.canvas.DOMtoCanvas({ x: 0, y: 0 }); + this.canvasBottomRight = this.canvas.DOMtoCanvas({ x: this.canvas.frame.canvas.clientWidth, y: this.canvas.frame.canvas.clientHeight }); + + if (hidden === false) { + if (this.dragging === false || this.dragging === true && this.options.hideEdgesOnDrag === false) { + this._drawEdges(ctx); + } + } + + if (this.dragging === false || this.dragging === true && this.options.hideNodesOnDrag === false) { + this._drawNodes(ctx, hidden); } + + if (this.controlNodesActive === true) { + this._drawControlNodes(ctx); + } + + //this.physics.nodesSolver._debug(ctx,"#F00F0F"); + + this.body.emitter.emit("afterDrawing", ctx); + + // restore original scaling and translation + ctx.restore(); + + if (hidden === true) { + ctx.clearRect(0, 0, w, h); + } + }, writable: true, configurable: true }, - _splitBranch: { + _drawNodes: { /** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. - * - * @param parentBranch + * 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 _splitBranch(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount == 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; - parentBranch.centerOfMass.x = 0; - parentBranch.centerOfMass.y = 0; + value: function _drawNodes(ctx) { + var alwaysShow = arguments[1] === undefined ? false : arguments[1]; + var nodes = this.body.nodes; + var nodeIndices = this.body.nodeIndices; + var node; + var selected = []; + + // draw unselected nodes; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + // set selected nodes aside + if (node.isSelected()) { + selected.push(nodeIndices[i]); + } else { + if (alwaysShow === true) { + node.draw(ctx); + } + // todo: replace check + //else if (node.inArea() === true) { + node.draw(ctx); + //} + } } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch, "NW"); - this._insertRegion(parentBranch, "NE"); - this._insertRegion(parentBranch, "SW"); - this._insertRegion(parentBranch, "SE"); - if (containedNode != null) { - this._placeInTree(parentBranch, containedNode); + // draw the selected nodes on top + for (var i = 0; i < selected.length; i++) { + node = nodes[selected[i]]; + node.draw(ctx); } }, writable: true, configurable: true }, - _insertRegion: { + _drawEdges: { /** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch - * - * @param parentBranch - * @param region - * @param parentRange + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _insertRegion(parentBranch, region) { - var minX, maxX, minY, maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - } - + value: function _drawEdges(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; - parentBranch.children[region] = { - centerOfMass: { x: 0, y: 0 }, - mass: 0, - range: { minX: minX, maxX: maxX, minY: minY, maxY: maxY }, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: { data: null }, - maxWidth: 0, - level: parentBranch.level + 1, - childrenCount: 0 - }; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.connected === true) { + edge.draw(ctx); + } + } }, writable: true, configurable: true }, - _debug: { - - - - - //--------------------------- DEBUGGING BELOW ---------------------------// - + _drawControlNodes: { /** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx * @private */ - value: function _debug(ctx, color) { - if (this.barnesHutTree !== undefined) { - ctx.lineWidth = 1; + value: function _drawControlNodes(ctx) { + var edges = this.body.edges; + var edgeIndices = this.body.edgeIndices; + var edge; - this._drawBranch(this.barnesHutTree.root, ctx, color); + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + edge._drawControlNodes(ctx); } }, writable: true, configurable: true }, - _drawBranch: { - + _determineBrowserMethod: { /** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color + * 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 _drawBranch(branch, ctx, color) { - if (color === undefined) { - color = "#FF0000"; - } - - if (branch.childrenCount == 4) { - this._drawBranch(branch.children.NW, ctx); - this._drawBranch(branch.children.NE, ctx); - this._drawBranch(branch.children.SE, ctx); - this._drawBranch(branch.children.SW, ctx); + 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; } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.minY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.minY); - ctx.lineTo(branch.range.maxX, branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.minX, branch.range.maxY); - ctx.lineTo(branch.range.minX, branch.range.minY); - ctx.stroke(); - - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ }, writable: true, configurable: true } }); - return BarnesHutSolver; + return CanvasRenderer; })(); - module.exports = BarnesHutSolver; + module.exports = CanvasRenderer; /***/ }, -/* 69 */ +/* 96 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32538,191 +32301,310 @@ return /******/ (function(modules) { // webpackBootstrap var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + + var util = __webpack_require__(1); + /** - * Created by Alex on 2/23/2015. + * 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 */ - - var RepulsionSolver = (function () { - function RepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, RepulsionSolver); + var Canvas = (function () { + function Canvas(body) { + var _this = this; + _classCallCheck(this, Canvas); this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); + + this.options = {}; + this.defaultOptions = { + width: "100%", + height: "100%" + }; + util.extend(this.options, this.defaultOptions); + + this.body.emitter.once("resize", function (obj) { + _this.body.view.translation.x = obj.width * 0.5;_this.body.view.translation.y = obj.height * 0.5; + }); + this.body.emitter.on("destroy", function () { + return _this.hammer.destroy(); + }); + + this.pixelRatio = 1; } - _prototypeProperties(RepulsionSolver, null, { + _prototypeProperties(Canvas, null, { setOptions: { value: function setOptions(options) { - this.options = options; + if (options !== undefined) { + util.deepExtend(this.options, options); + } }, writable: true, configurable: true }, - solve: { - /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * - * @private - */ - value: function solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2; + create: { + value: function create() { + // remove all elements from the container element. + while (this.body.container.hasChildNodes()) { + this.body.container.removeChild(this.body.container.firstChild); + } - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + 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; - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; + ////////////////////////////////////////////////////////////////// - // approximation constants - var a = -2 / 3 / nodeDistance; - var b = 4 / 3; + this.frame.canvas = document.createElement("canvas"); + this.frame.canvas.style.position = "relative"; + this.frame.appendChild(this.frame.canvas); - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (var i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (var j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; + 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); - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); + } - // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. - if (distance == 0) { - distance = 0.1 * Math.random(); - dx = distance; - } + // add the frame to the container element + this.body.container.appendChild(this.frame); - if (distance < 2 * nodeDistance) { - if (distance < 0.5 * nodeDistance) { - repulsingForce = 1; - } else { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) - } - repulsingForce = repulsingForce / distance; + this.body.view.scale = 1; + this.body.view.translation = { x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight }; - fx = dx * repulsingForce; - fy = dy * repulsingForce; + this._bindHammer(); + }, + writable: true, + configurable: true + }, + _bindHammer: { - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; - } - } + + /** + * This function binds hammer, it can be repeated over and over due to the uniqueness check. + * @private + */ + value: function _bindHammer() { + var _this = this; + if (this.hammer !== undefined) { + this.hammer.destroy(); } + this.drag = {}; + this.pinch = {}; + + // init hammer + this.hammer = new Hammer(this.frame.canvas); + this.hammer.get("pinch").set({ enable: true }); + + hammerUtil.onTouch(this.hammer, function (event) { + _this.body.eventListeners.onTouch(event); + }); + this.hammer.on("tap", function (event) { + _this.body.eventListeners.onTap(event); + }); + this.hammer.on("doubletap", function (event) { + _this.body.eventListeners.onDoubleTap(event); + }); + this.hammer.on("press", function (event) { + _this.body.eventListeners.onHold(event); + }); + this.hammer.on("panstart", function (event) { + _this.body.eventListeners.onDragStart(event); + }); + this.hammer.on("panmove", function (event) { + _this.body.eventListeners.onDrag(event); + }); + this.hammer.on("panend", function (event) { + _this.body.eventListeners.onDragEnd(event); + }); + this.hammer.on("pinch", function (event) { + _this.body.eventListeners.onPinch(event); + }); + + // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work? + this.frame.canvas.addEventListener("mousewheel", function (event) { + _this.body.eventListeners.onMouseWheel(event); + }); + this.frame.canvas.addEventListener("DOMMouseScroll", function (event) { + _this.body.eventListeners.onMouseWheel(event); + }); + + this.frame.canvas.addEventListener("mousemove", function (event) { + _this.body.eventListeners.onMouseMove(event); + }); + + this.hammerFrame = new Hammer(this.frame); + hammerUtil.onRelease(this.hammerFrame, function (event) { + _this.body.eventListeners.onRelease(event); + }); }, writable: true, configurable: true - } - }); + }, + setSize: { - return RepulsionSolver; - })(); - module.exports = RepulsionSolver; + /** + * 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; -/***/ }, -/* 70 */ -/***/ function(module, exports, __webpack_require__) { + this.frame.canvas.style.width = "100%"; + this.frame.canvas.style.height = "100%"; - "use strict"; + this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; + this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + this.options.width = width; + this.options.height = height; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + emitEvent = true; + } else { + // this would adapt the width of the canvas to the width from 100% if and only if + // there is a change. - /** - * Created by Alex on 2/23/2015. - */ + 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; + } + } - var HierarchicalRepulsionSolver = (function () { - function HierarchicalRepulsionSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalRepulsionSolver); + 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: { - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } - _prototypeProperties(HierarchicalRepulsionSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + /** + * 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 }, - solve: { + _XconvertCanvasToDOM: { /** - * Calculate the forces the nodes apply on each other based on a repulsion field. - * This field is linearly approximated. - * + * 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 solve() { - var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; - - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; - - // repulsing forces between nodes - var nodeDistance = this.options.nodeDistance; + value: function _XconvertCanvasToDOM(x) { + return x * this.body.view.scale + this.body.view.translation.x; + }, + writable: true, + configurable: true + }, + _YconvertDOMtoCanvas: { - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; + /** + * 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: { - // nodes only affect nodes on their level - if (node1.level == node2.level) { - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + /** + * 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: { - var steepness = 0.05; - if (distance < nodeDistance) { - repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); - } else { - repulsingForce = 0; - } - // normalize force with - if (distance == 0) { - distance = 0.01; - } else { - repulsingForce = repulsingForce / distance; - } - fx = dx * repulsingForce; - fy = dy * repulsingForce; - forces[node1.id].x -= fx; - forces[node1.id].y -= fy; - forces[node2.id].x += fx; - forces[node2.id].y += fy; - } - } - } + /** + * + * @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 HierarchicalRepulsionSolver; + return Canvas; })(); - module.exports = HierarchicalRepulsionSolver; + module.exports = Canvas; /***/ }, -/* 71 */ +/* 97 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -32732,290 +32614,392 @@ 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/23/2015. + * Created by Alex on 26-Feb-15. */ - var SpringSolver = (function () { - function SpringSolver(body, physicsBody, options) { - _classCallCheck(this, SpringSolver); + var util = __webpack_require__(1); + + var View = (function () { + function View(body, canvas) { + var _this = this; + _classCallCheck(this, View); this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); + this.canvas = canvas; + + this.animationSpeed = 1 / this.renderRefreshRate; + this.animationEasingFunction = "easeInOutQuint"; + this.easingTime = 0; + this.sourceScale = 0; + this.targetScale = 0; + this.sourceTranslation = 0; + this.targetTranslation = 0; + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + this.touchTime = 0; + + this.viewFunction = undefined; + + this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); + this.body.emitter.on("animationFinished", function () { + _this.body.emitter.emit("_stopRendering"); + }); + this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); } - _prototypeProperties(SpringSolver, null, { + _prototypeProperties(View, null, { setOptions: { - value: function setOptions(options) { + value: function setOptions() { + var options = arguments[0] === undefined ? {} : arguments[0]; this.options = options; }, writable: true, configurable: true }, - solve: { + _getRange: { + + // zoomExtent /** - * This function calculates the springforces on the nodes, accounting for the support nodes. - * + * Find the center position of the network * @private */ - value: function solve() { - var edgeLength, edge; - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var edges = this.body.edges; - - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - // only calculate forces if nodes are in the same sector - if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { - if (edge.edgeType.via !== undefined) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - var node1 = edge.to; - var node2 = edge.edgeType.via; - var node3 = edge.from; - - - this._calculateSpringForce(node1, node2, 0.5 * edgeLength); - this._calculateSpringForce(node2, node3, 0.5 * edgeLength); - } else { - // the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use - // the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger. - edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5 : edge.options.length; - this._calculateSpringForce(edge.from, edge.to, edgeLength); + value: function _getRange() { + var specificNodes = arguments[0] === undefined ? [] : arguments[0]; + var minY = 1000000000, + maxY = -1000000000, + minX = 1000000000, + maxX = -1000000000, + node; + if (specificNodes.length > 0) { + for (var i = 0; i < specificNodes.length; i++) { + node = this.body.nodes[specificNodes[i]]; + if (minX > node.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; + } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.boundingBox.bottom; + } // top is negative, bottom is positive + } + } else { + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (minX > node.shape.boundingBox.left) { + minX = node.shape.boundingBox.left; + } + if (maxX < node.shape.boundingBox.right) { + maxX = node.shape.boundingBox.right; } + if (minY > node.shape.boundingBox.bottom) { + minY = node.shape.boundingBox.top; + } // top is negative, bottom is positive + if (maxY < node.shape.boundingBox.top) { + maxY = node.shape.boundingBox.bottom; + } // top is negative, bottom is positive } } } + + 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 }, - _calculateSpringForce: { + _findCenter: { /** - * This is the code actually performing the calculation for the function above. - * - * @param node1 - * @param node2 - * @param edgeLength + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} * @private */ - value: function _calculateSpringForce(node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; - - dx = node1.x - node2.x; - dy = node1.y - node2.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance == 0 ? 0.01 : distance; - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - // handle the case where one node is not part of the physcis - if (this.physicsBody.forces[node1.id] !== undefined) { - this.physicsBody.forces[node1.id].x += fx; - this.physicsBody.forces[node1.id].y += fy; - } - - if (this.physicsBody.forces[node2.id] !== undefined) { - this.physicsBody.forces[node2.id].x -= fx; - this.physicsBody.forces[node2.id].y -= fy; - } + value: function _findCenter(range) { + return { x: 0.5 * (range.maxX + range.minX), + y: 0.5 * (range.maxY + range.minY) }; }, writable: true, configurable: true - } - }); - - return SpringSolver; - })(); + }, + zoomExtent: { - module.exports = SpringSolver; -/***/ }, -/* 72 */ -/***/ function(module, exports, __webpack_require__) { + /** + * 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; - "use strict"; + 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; + } - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + range = this._getRange(options.nodes); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var numberOfNodes = this.body.nodeIndices.length; + zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. - /** - * Created by Alex on 2/25/2015. - */ + // 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 HierarchicalSpringSolver = (function () { - function HierarchicalSpringSolver(body, physicsBody, options) { - _classCallCheck(this, HierarchicalSpringSolver); + var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; + zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel; + } - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + if (zoomLevel > 1) { + zoomLevel = 1; + } - _prototypeProperties(HierarchicalSpringSolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + var center = this._findCenter(range); + var animationOptions = { position: center, scale: zoomLevel, animation: options }; + this.moveTo(animationOptions); }, writable: true, configurable: true }, - solve: { + focusOnNode: { + + // animation /** - * This function calculates the springforces on the nodes, accounting for the support nodes. + * Center a node in view. * - * @private + * @param {Number} nodeId + * @param {Number} [options] */ - value: function solve() { - var edgeLength, edge; - var dx, dy, fx, fy, springForce, distance; - var edges = this.body.edges; - var factor = 0.5; - - var edgeIndices = this.physicsBody.physicsEdgeIndices; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + 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; - // initialize the spring force counters - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].springFx = 0; - forces[nodeId].springFy = 0; + this.moveTo(options); + } else { + console.log("Node: " + nodeId + " cannot be found."); } + }, + writable: true, + configurable: true + }, + moveTo: { - - // forces caused by the edges, modelled as springs - for (var i = 0; i < edgeIndices.length; i++) { - edge = edges[edgeIndices[i]]; - if (edge.connected === true) { - edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length; - - dx = edge.from.x - edge.to.x; - dy = edge.from.y - edge.to.y; - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance == 0 ? 0.01 : distance; - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - if (edge.to.level != edge.from.level) { - forces[edge.toId].springFx -= fx; - forces[edge.toId].springFy -= fy; - forces[edge.fromId].springFx += fx; - forces[edge.fromId].springFy += fy; - } else { - forces[edge.toId].x -= factor * fx; - forces[edge.toId].y -= factor * fy; - forces[edge.fromId].x += factor * fx; - forces[edge.fromId].y += factor * fy; - } - } + /** + * + * @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; } - - // normalize spring forces - var springForce = 1; - var springFx, springFy; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - springFx = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFx)); - springFy = Math.min(springForce, Math.max(-springForce, forces[nodeId].springFy)); - - forces[nodeId].x += springFx; - forces[nodeId].y += springFy; + if (options.offset === undefined) { + options.offset = { x: 0, y: 0 }; } - - // retain energy balance - var totalFx = 0; - var totalFy = 0; - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - totalFx += forces[nodeId].x; - totalFy += forces[nodeId].y; + if (options.offset.x === undefined) { + options.offset.x = 0; } - var correctionFx = totalFx / nodeIndices.length; - var correctionFy = totalFy / nodeIndices.length; - - for (var i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - forces[nodeId].x -= correctionFx; - forces[nodeId].y -= correctionFy; + if (options.offset.y === undefined) { + options.offset.y = 0; + } + if (options.scale === undefined) { + options.scale = this.body.view.scale; + } + if (options.position === undefined) { + options.position = this.body.view.translation; + } + if (options.animation === undefined) { + options.animation = { duration: 0 }; + } + if (options.animation === false) { + options.animation = { duration: 0 }; + } + if (options.animation === true) { + options.animation = {}; } + if (options.animation.duration === undefined) { + options.animation.duration = 1000; + } // default duration + if (options.animation.easingFunction === undefined) { + options.animation.easingFunction = "easeInOutQuad"; + } // default easing function + + this.animateView(options); }, writable: true, configurable: true - } - }); - - return HierarchicalSpringSolver; - })(); + }, + animateView: { - module.exports = HierarchicalSpringSolver; + /** + * + * @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; + } -/***/ }, -/* 73 */ -/***/ function(module, exports, __webpack_require__) { + // 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. + } - "use strict"; + this.sourceScale = this.body.view.scale; + this.sourceTranslation = this.body.view.translation; + this.targetScale = options.scale; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + // 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 + }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // if the time is set to 0, don't do an animation + if (options.animation.duration == 0) { + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + } else { + this.body.view.scale = this.targetScale; + this.body.view.translation = this.targetTranslation; + this.body.emitter.emit("_requestRedraw"); + } + } else { + this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's + this.animationEasingFunction = options.animation.easingFunction; - /** - * Created by Alex on 2/23/2015. - */ - var CentralGravitySolver = (function () { - function CentralGravitySolver(body, physicsBody, options) { - _classCallCheck(this, CentralGravitySolver); + this.viewFunction = this._transitionRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + this.body.emitter.emit("_startRendering"); + } + }, + writable: true, + configurable: true + }, + _lockedRedraw: { - this.body = body; - this.physicsBody = physicsBody; - this.setOptions(options); - } + /** + * 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 + }; - _prototypeProperties(CentralGravitySolver, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + this.body.view.translation = targetTranslation; }, writable: true, configurable: true }, - solve: { - value: function solve() { - var dx, dy, distance, node, i; - var nodes = this.body.nodes; - var nodeIndices = this.physicsBody.physicsNodeIndices; - var forces = this.physicsBody.forces; + releaseNode: { + value: function releaseNode() { + if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.lockedOnNodeId = undefined; + this.lockedOnNodeOffset = undefined; + } + }, + writable: true, + configurable: true + }, + _transitionRedraw: { + /** + * + * @param easingTime + * @private + */ + value: function _transitionRedraw() { + var finished = arguments[0] === undefined ? false : arguments[0]; + this.easingTime += this.animationSpeed; + this.easingTime = finished === true ? 1 : this.easingTime; - var gravity = this.options.centralGravity; - var gravityForce = 0; + var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); - for (i = 0; i < nodeIndices.length; i++) { - var nodeId = nodeIndices[i]; - node = nodes[nodeId]; - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); + 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 + }; - gravityForce = distance == 0 ? 0 : gravity / distance; - forces[nodeId].x = dx * gravityForce; - forces[nodeId].y = dy * gravityForce; + // cleanup + if (this.easingTime >= 1) { + this.body.emitter.off("initRedraw", this.viewFunction); + this.easingTime = 0; + if (this.lockedOnNodeId != undefined) { + this.viewFunction = this._lockedRedraw.bind(this); + this.body.emitter.on("initRedraw", this.viewFunction); + } + this.body.emitter.emit("animationFinished"); } }, writable: true, @@ -33023,3649 +33007,4021 @@ return /******/ (function(modules) { // webpackBootstrap } }); - return CentralGravitySolver; + return View; })(); - module.exports = CentralGravitySolver; + module.exports = View; /***/ }, -/* 74 */ +/* 98 */ /***/ function(module, exports, __webpack_require__) { "use strict"; var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var Node = _interopRequire(__webpack_require__(66)); - /** + * Created by Alex on 2/27/2015. * */ - var Cluster = (function (Node) { - function Cluster(options, body, imagelist, grouplist, globalOptions) { - _classCallCheck(this, Cluster); - - _get(Object.getPrototypeOf(Cluster.prototype), "constructor", this).call(this, options, body, imagelist, grouplist, globalOptions); - - this.isCluster = true; - this.containedNodes = {}; - this.containedEdges = {}; - } - _inherits(Cluster, Node); - - return Cluster; - })(Node); - - module.exports = Cluster; - -/***/ }, -/* 75 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + var util = __webpack_require__(1); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var NavigationHandler = _interopRequire(__webpack_require__(99)); - var util = __webpack_require__(1); - var Hammer = __webpack_require__(41); - var hammerUtil = __webpack_require__(43); - var keycharm = __webpack_require__(78); + var Popup = _interopRequire(__webpack_require__(100)); - var NavigationHandler = (function () { - function NavigationHandler(body, canvas) { - var _this = this; - _classCallCheck(this, NavigationHandler); + var InteractionHandler = (function () { + function InteractionHandler(body, canvas, selectionHandler) { + _classCallCheck(this, InteractionHandler); this.body = body; this.canvas = canvas; + this.selectionHandler = selectionHandler; + this.navigationHandler = new NavigationHandler(body, canvas); + + // bind the events from hammer to functions in this object + this.body.eventListeners.onTap = this.onTap.bind(this); + this.body.eventListeners.onTouch = this.onTouch.bind(this); + this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); + this.body.eventListeners.onHold = this.onHold.bind(this); + this.body.eventListeners.onDragStart = this.onDragStart.bind(this); + this.body.eventListeners.onDrag = this.onDrag.bind(this); + this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); + this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); + this.body.eventListeners.onPinch = this.onPinch.bind(this); + this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); + this.body.eventListeners.onRelease = this.onRelease.bind(this); - this.iconsCreated = false; - this.navigationHammers = []; - this.boundFunctions = {}; this.touchTime = 0; - this.activated = false; + this.drag = {}; + this.pinch = {}; + this.hoverObj = { nodes: {}, edges: {} }; + this.popup = undefined; + this.popupObj = undefined; + this.popupTimer = undefined; + this.body.functions.getPointer = this.getPointer.bind(this); - this.body.emitter.on("release", this._stopMovement.bind(this)); - this.body.emitter.on("activate", function () { - _this.activated = true;_this.configureKeyboardBindings(); - }); - this.body.emitter.on("deactivate", function () { - _this.activated = false;_this.configureKeyboardBindings(); - }); - this.body.emitter.on("destroy", function () { - if (_this.keycharm !== undefined) { - _this.keycharm.destroy(); - } - }); this.options = {}; + this.defaultOptions = { + dragNodes: true, + dragView: true, + zoomView: true, + hoverEnabled: false, + showNavigationIcons: false, + tooltip: { + delay: 300, + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" + } + }, + keyboard: { + enabled: false, + speed: { x: 10, y: 10, zoom: 0.02 }, + bindToWindow: true + } + }; + util.extend(this.options, this.defaultOptions); } - _prototypeProperties(NavigationHandler, null, { + _prototypeProperties(InteractionHandler, null, { setOptions: { value: function setOptions(options) { if (options !== undefined) { - this.options = options; - this.create(); + // extend all but the values in fields + var fields = ["keyboard", "tooltip"]; + util.selectiveNotDeepExtend(fields, this.options, options); + + // merge the keyboard options in. + util.mergeOptions(this.options, options, "keyboard"); + + if (options.tooltip) { + util.extend(this.options.tooltip, options.tooltip); + if (options.tooltip.color) { + this.options.tooltip.color = util.parseColor(options.tooltip.color); + } + } } + + this.navigationHandler.setOptions(this.options); }, writable: true, configurable: true }, - create: { - value: function create() { - if (this.options.showNavigationIcons === true) { - if (this.iconsCreated === false) { - this.loadNavigationElements(); - } - } else if (this.iconsCreated === true) { - this.cleanNavigation(); - } + getPointer: { - this.configureKeyboardBindings(); + + /** + * 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 }, - cleanNavigation: { - value: function cleanNavigation() { - // clean hammer bindings - if (this.navigationHammers.length != 0) { - for (var i = 0; i < this.navigationHammers.length; i++) { - this.navigationHammers[i].destroy(); - } - this.navigationHammers = []; - } + onTouch: { - this._navigationReleaseOverload = function () {}; - // clean up previous navigation items - if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { - this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); + /** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ + value: function onTouch(event) { + if (new Date().valueOf() - this.touchTime > 100) { + this.drag.pointer = this.getPointer(event.center); + this.drag.pinched = false; + this.pinch.scale = this.body.view.scale; + // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) + this.touchTime = new Date().valueOf(); } - - this.iconsCreated = false; }, writable: true, configurable: true }, - loadNavigationElements: { + onTap: { /** - * 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. - * + * handle tap/click event: select/unselect a node * @private */ - value: function loadNavigationElements() { - 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.canvas.frame.appendChild(this.navigationDOM.wrapper); - - for (var i = 0; i < navigationDivs.length; i++) { - this.navigationDOM[navigationDivs[i]] = document.createElement("div"); - this.navigationDOM[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; - this.navigationDOM.wrapper.appendChild(this.navigationDOM[navigationDivs[i]]); + value: function onTap(event) { + var pointer = this.getPointer(event.center); - var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); - if (navigationDivActions[i] == "_zoomExtent") { - hammerUtil.onTouch(hammer, this._zoomExtent.bind(this)); - } else { - hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); - } + var previouslySelected = this.selectionHandler._getSelectedObjectCount() > 0; + var selected = this.selectionHandler.selectOnPoint(pointer); - this.navigationHammers.push(hammer); + if (selected === true || previouslySelected == true && selected === false) { + // select or unselect + this.body.emitter.emit("select", this.selectionHandler.getSelection()); } - this.iconsCreated = true; - }, - writable: true, - configurable: true - }, - bindToRedraw: { - value: function bindToRedraw(action) { - if (this.boundFunctions[action] === undefined) { - this.boundFunctions[action] = this[action].bind(this); - this.body.emitter.on("initRedraw", this.boundFunctions[action]); - this.body.emitter.emit("_startRendering"); - } + this.selectionHandler._generateClickEvent("click", pointer); }, writable: true, configurable: true }, - unbindFromRedraw: { - value: function unbindFromRedraw(action) { - if (this.boundFunctions[action] !== undefined) { - this.body.emitter.off("initRedraw", this.boundFunctions[action]); - this.body.emitter.emit("_stopRendering"); - delete this.boundFunctions[action]; - } + onDoubleTap: { + + + /** + * handle doubletap event + * @private + */ + value: function onDoubleTap(event) { + var pointer = this.getPointer(event.center); + this.selectionHandler._generateClickEvent("doubleClick", pointer); }, writable: true, configurable: true }, - _zoomExtent: { + onHold: { + + /** - * this stops all movement induced by the navigation buttons - * + * handle long tap event: multi select nodes * @private */ - value: function _zoomExtent() { - if (new Date().valueOf() - this.touchTime > 700) { - // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) - this.body.emitter.emit("zoomExtent", { duration: 700 }); - this.touchTime = new Date().valueOf(); + value: function onHold(event) { + var pointer = this.getPointer(event.center); + + var selectionChanged = this.selectionHandler.selectAdditionalOnPoint(pointer); + + if (selectionChanged === true) { + // select or longpress + this.body.emitter.emit("select", this.selectionHandler.getSelection()); } + + this.selectionHandler._generateClickEvent("click", pointer); }, writable: true, configurable: true }, - _stopMovement: { + onRelease: { + /** - * this stops all movement induced by the navigation buttons + * handle the release of the screen * * @private */ - value: function _stopMovement() { - for (var boundAction in this.boundFunctions) { - if (this.boundFunctions.hasOwnProperty(boundAction)) { - this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]); - this.body.emitter.emit("_stopRendering"); - } - } - this.boundFunctions = {}; - }, - writable: true, - configurable: true - }, - _moveUp: { - value: function _moveUp() { - this.body.view.translation.y += this.options.keyboard.speed.y; - }, - writable: true, - configurable: true - }, - _moveDown: { - value: function _moveDown() { - this.body.view.translation.y -= this.options.keyboard.speed.y; - }, - writable: true, - configurable: true - }, - _moveLeft: { - value: function _moveLeft() { - this.body.view.translation.x += this.options.keyboard.speed.x; - }, - writable: true, - configurable: true - }, - _moveRight: { - value: function _moveRight() { - this.body.view.translation.x -= this.options.keyboard.speed.x; - }, - writable: true, - configurable: true - }, - _zoomIn: { - value: function _zoomIn() { - this.body.view.scale += this.options.keyboard.speed.zoom; - }, - writable: true, - configurable: true - }, - _zoomOut: { - value: function _zoomOut() { - this.body.view.scale -= this.options.keyboard.speed.zoom; + value: function onRelease(event) { + this.body.emitter.emit("release", event); }, writable: true, configurable: true }, - configureKeyboardBindings: { + onDragStart: { /** - * bind all keys using keycharm. + * This function is called by onDragStart. + * It is separated out because we can then overload it for the datamanipulation system. + * + * @private */ - value: function configureKeyboardBindings() { - if (this.keycharm !== undefined) { - this.keycharm.destroy(); + 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); } - if (this.options.keyboard.enabled === true) { - if (this.options.keyboard.bindToWindow === true) { - this.keycharm = keycharm({ container: window, preventDefault: false }); - } else { - this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: false }); + // note: drag.pointer is set in onTouch to get the initial touch location + var node = this.selectionHandler.getNodeAt(this.drag.pointer); + + this.drag.dragging = true; + this.drag.selection = []; + this.drag.translation = util.extend({}, this.body.view.translation); // copy the object + this.drag.nodeId = undefined; + + this.body.emitter.emit("dragStart", { nodeIds: this.selectionHandler.getSelection().nodes }); + + if (node !== undefined && this.options.dragNodes === true) { + this.drag.nodeId = node.id; + // select the clicked node if not yet selected + if (node.isSelected() === false) { + this.selectionHandler.unselectAll(); + this.selectionHandler.selectObject(node); } - this.keycharm.reset(); + 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, - if (this.activated === true) { - this.keycharm.bind("up", this.bindToRedraw.bind(this, "_moveUp"), "keydown"); - this.keycharm.bind("down", this.bindToRedraw.bind(this, "_moveDown"), "keydown"); - this.keycharm.bind("left", this.bindToRedraw.bind(this, "_moveLeft"), "keydown"); - this.keycharm.bind("right", this.bindToRedraw.bind(this, "_moveRight"), "keydown"); - this.keycharm.bind("=", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("num+", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("num-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("[", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); - this.keycharm.bind("]", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("pageup", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); - this.keycharm.bind("pagedown", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: object.x, + y: object.y, + xFixed: object.options.fixed.x, + yFixed: object.options.fixed.y + }; - this.keycharm.bind("up", this.unbindFromRedraw.bind(this, "_moveUp"), "keyup"); - this.keycharm.bind("down", this.unbindFromRedraw.bind(this, "_moveDown"), "keyup"); - this.keycharm.bind("left", this.unbindFromRedraw.bind(this, "_moveLeft"), "keyup"); - this.keycharm.bind("right", this.unbindFromRedraw.bind(this, "_moveRight"), "keyup"); - this.keycharm.bind("=", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("num+", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("num-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("[", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); - this.keycharm.bind("]", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("pageup", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); - this.keycharm.bind("pagedown", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + object.options.fixed.x = true; + object.options.fixed.y = true; + + this.drag.selection.push(s); + } } } }, writable: true, configurable: true - } - }); - - return NavigationHandler; - })(); - - module.exports = NavigationHandler; - -/***/ }, -/* 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); }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - /** - * Popup is a class to create a popup window with some text - * @param {Element} container The container object. - * @param {Number} [x] - * @param {Number} [y] - * @param {String} [text] - * @param {Object} [style] An object containing borderColor, - * backgroundColor, etc. - */ - var Popup = (function () { - function Popup(container, x, y, text, style) { - _classCallCheck(this, Popup); + }, + onDrag: { - 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" - } - }; - } - } + /** + * handle drag event + * @private + */ + value: function onDrag(event) { + var _this = this; + if (this.drag.pinched === true) { + return; + } - this.x = 0; - this.y = 0; - this.padding = 5; - this.hidden = false; + // remove the focus on node if it is focussed on by the focusOnNode + this.body.emitter.emit("unlockNode"); - if (x !== undefined && y !== undefined) { - this.setPosition(x, y); - } - if (text !== undefined) { - this.setText(text); - } + var pointer = this.getPointer(event.center); + var selection = this.drag.selection; + if (selection && selection.length && this.options.dragNodes === true) { + (function () { + // calculate delta's and new location + var deltaX = pointer.x - _this.drag.pointer.x; + var deltaY = pointer.y - _this.drag.pointer.y; - // 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); - } + // update position of all selected nodes + selection.forEach(function (selection) { + var node = selection.node; + // only move the node if it was not fixed initially + if (selection.xFixed === false) { + node.x = _this.canvas._XconvertDOMtoCanvas(_this.canvas._XconvertCanvasToDOM(selection.x) + deltaX); + } + // only move the node if it was not fixed initially + if (selection.yFixed === false) { + node.y = _this.canvas._YconvertDOMtoCanvas(_this.canvas._YconvertCanvasToDOM(selection.y) + deltaY); + } + }); - _prototypeProperties(Popup, null, { - setPosition: { + // 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; - /** - * @param {number} x Horizontal position of the popup window - * @param {number} y Vertical position of the popup window - */ - value: function setPosition(x, y) { - this.x = parseInt(x); - this.y = parseInt(y); + 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 }, - setText: { + onDragEnd: { + /** - * Set the content for the popup window. This can be HTML code or text. - * @param {string | Element} content + * handle drag start event + * @private */ - value: function setText(content) { - if (content instanceof Element) { - this.frame.innerHTML = ""; - this.frame.appendChild(content); + value: function onDragEnd(event) { + this.drag.dragging = false; + var selection = this.drag.selection; + if (selection && selection.length) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.options.fixed.x = s.xFixed; + s.node.options.fixed.y = s.yFixed; + }); + this.body.emitter.emit("startSimulation"); } else { - this.frame.innerHTML = content; // string containing text or HTML + this.body.emitter.emit("_requestRedraw"); } + + this.body.emitter.emit("dragEnd", { nodeIds: this.selectionHandler.getSelection().nodes }); }, writable: true, configurable: true }, - show: { + onPinch: { + + /** - * Show the popup window - * @param {boolean} show Optional. Show or hide the window + * Handle pinch event + * @param event + * @private */ - value: function show(show) { - if (show === undefined) { - show = true; + value: function onPinch(event) { + var pointer = this.getPointer(event.center); + + this.drag.pinched = true; + if (this.pinch.scale === undefined) { + this.pinch.scale = 1; } - if (show === true) { - var height = this.frame.clientHeight; - var width = this.frame.clientWidth; - var maxHeight = this.frame.parentNode.clientHeight; - var maxWidth = this.frame.parentNode.clientWidth; + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.scale; + this.zoom(scale, pointer); + }, + writable: true, + configurable: true + }, + zoom: { - var top = this.y - height; - if (top + height + this.padding > maxHeight) { - top = maxHeight - height - this.padding; + + /** + * 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 (top < this.padding) { - top = this.padding; + if (scale > 10) { + scale = 10; } - var left = this.x; - if (left + width + this.padding > maxWidth) { - left = maxWidth - width - this.padding; + var preScaleDragPointer = undefined; + if (this.drag !== undefined) { + if (this.drag.dragging === true) { + preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); + } } - if (left < this.padding) { - left = this.padding; + // + 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 != undefined) { + var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); + this.drag.pointer.x = postScaleDragPointer.x; + this.drag.pointer.y = postScaleDragPointer.y; } - this.frame.style.left = left + "px"; - this.frame.style.top = top + "px"; - this.frame.style.visibility = "visible"; - this.hidden = false; - } else { - this.hide(); + 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 }, - hide: { + onMouseWheel: { + /** - * Hide the popup window + * 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 hide() { - this.hidden = true; - this.frame.style.visibility = "hidden"; - }, - writable: true, - configurable: true - } - }); - - return Popup; - })(); - - module.exports = Popup; - -/***/ }, -/* 77 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - // English - exports.en = { - edit: "Edit", - del: "Delete selected", - back: "Back", - addNode: "Add Node", - addEdge: "Add Edge", - editNode: "Edit Node", - editEdge: "Edit Edge", - addDescription: "Click in an empty space to place a new node.", - edgeDescription: "Click on a node and drag the edge to another node to connect them.", - editEdgeDescription: "Click on the control points and drag them to a node to connect to it.", - createEdgeError: "Cannot link edges to a cluster.", - deleteClusterError: "Clusters cannot be deleted.", - editClusterError: "Clusters cannot be edited." - }; - exports.en_EN = exports.en; - exports.en_US = exports.en; + 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; + } - // Dutch - exports.nl = { - edit: "Wijzigen", - del: "Selectie verwijderen", - back: "Terug", - addNode: "Node toevoegen", - addEdge: "Link toevoegen", - editNode: "Node wijzigen", - editEdge: "Link wijzigen", - addDescription: "Klik op een leeg gebied om een nieuwe node te maken.", - edgeDescription: "Klik op een node en sleep de link naar een andere node om ze te verbinden.", - editEdgeDescription: "Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.", - createEdgeError: "Kan geen link maken naar een cluster.", - deleteClusterError: "Clusters kunnen niet worden verwijderd.", - editClusterError: "Clusters kunnen niet worden aangepast." - }; - exports.nl_NL = exports.nl; - exports.nl_BE = exports.nl; + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta !== 0) { + // calculate the new scale + var scale = this.body.view.scale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= 1 + zoom; -/***/ }, -/* 78 */ -/***/ function(module, exports, __webpack_require__) { + // calculate the pointer location + var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); - var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;"use strict"; - /** - * Created by Alex on 11/6/2014. - */ + // apply the new scale + this.zoom(scale, pointer); + } - // https://github.com/umdjs/umd/blob/master/returnExports.js#L40-L60 - // if the module has no dependencies, the above pattern can be simplified to - (function (root, factory) { - if (true) { - // AMD. Register as an anonymous module. - !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - // Browser globals (root is window) - root.keycharm = factory(); - } - }(this, function () { + // Prevent default actions caused by mouse wheel. + event.preventDefault(); + }, + writable: true, + configurable: true + }, + onMouseMove: { - 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; + /** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ + value: function onMouseMove(event) { + var _this = this; + var pointer = this.getPointer({ x: event.pageX, y: event.pageY }); + var popupVisible = false; - // 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};} + // check if the previously selected node is still selected + if (this.popup !== undefined) { + if (this.popup.hidden === false) { + this._checkHidePopup(pointer); + } - // 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}; + // 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) { + if (this.popupTimer !== undefined) { + clearInterval(this.popupTimer); // stop any running calculationTimer + this.popupTimer = undefined; + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(function () { + return _this._checkShowPopup(pointer); + }, this.options.tooltip.delay); + } + } - var down = function(event) {handleEvent(event,'keydown');}; - var up = function(event) {handleEvent(event,'keyup');}; + /** + * 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]; + } + } - // 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); + // adding hover highlights + var obj = this.selectionHandler.getNodeAt(pointer); + if (obj == undefined) { + obj = this.selectionHandler.getEdgeAt(pointer); } - else if (bound[i].shift == true && event.shiftKey == true) { - bound[i].fn(event); + if (obj != undefined) { + this.selectionHandler.hoverObject(obj); } - else if (bound[i].shift == false && event.shiftKey == false) { - bound[i].fn(event); + + // removing all node hover highlights except for the selected one. + for (var nodeId in this.hoverObj.nodes) { + if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { + if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == undefined) { + this.selectionHandler.blurObject(this.hoverObj.nodes[nodeId]); + delete this.hoverObj.nodes[nodeId]; + } + } } + this.body.emitter.emit("_requestRedraw"); } + }, + writable: true, + configurable: true + }, + _checkShowPopup: { - if (preventDefault == true) { - event.preventDefault(); - } - } - }; - // bind a key to a callback - _exportFunctions.bind = function(key, callback, type) { - if (type === undefined) { - type = 'keydown'; - } - if (_keys[key] === undefined) { - throw new Error("unsupported key: " + key); - } - if (_bound[type][_keys[key].code] === undefined) { - _bound[type][_keys[key].code] = []; - } - _bound[type][_keys[key].code].push({fn:callback, shift:_keys[key].shift}); - }; + /** + * Check if there is an element on the given position in the network + * (a node or edge). If so, and if this element has a title, + * show a popup window with its title. + * + * @param {{x:Number, y:Number}} pointer + * @private + */ + value: function _checkShowPopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: y + }; - // bind all keys to a call back (demo purposes) - _exportFunctions.bindAll = function(callback, type) { - if (type === undefined) { - type = 'keydown'; - } - for (var key in _keys) { - if (_keys.hasOwnProperty(key)) { - _exportFunctions.bind(key,callback,type); - } - } - }; + var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; + var nodeUnderCursor = false; + var popupType = "node"; - // 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; + // check if a node is under the cursor. + if (this.popupObj === undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodeIndices = this.body.nodeIndices; + var nodes = this.body.nodes; + var node = undefined; + var overlappingNodes = []; + for (var i = 0; i < nodeIndices.length; i++) { + node = nodes[nodeIndices[i]]; + if (node.isOverlappingWith(pointerObj) === true) { + if (node.getTitle() !== undefined) { + overlappingNodes.push(nodeIndices[i]); + } + } } - } - } - 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]); + if (overlappingNodes.length > 0) { + // if there are overlapping nodes, select the last one, this is the one which is drawn on top of the others + this.popupObj = nodes[overlappingNodes[overlappingNodes.length - 1]]; + // if you hover over a node, the title of the edge is not supposed to be shown. + nodeUnderCursor = true; + } + } + + if (this.popupObj === undefined && nodeUnderCursor == false) { + // search the edges for overlap + var edgeIndices = this.body.edgeIndices; + var edges = this.body.edges; + var edge = undefined; + var overlappingEdges = []; + for (var i = 0; i < edgeIndices.length; i++) { + edge = edges[edgeIndices[i]]; + if (edge.isOverlappingWith(pointerObj) === true) { + if (edge.connected === true && edge.getTitle() !== undefined) { + overlappingEdges.push(edgeIndices[i]); + } } } + + if (overlappingEdges.length > 0) { + this.popupObj = edges[overlappingEdges[overlappingEdges.length - 1]]; + popupType = "edge"; + } } - _bound[type][_keys[key].code] = newBindings; - } - else { - _bound[type][_keys[key].code] = []; - } - }; - // reset all bound variables. - _exportFunctions.reset = function() { - _bound = {keydown:{}, keyup:{}}; - }; + if (this.popupObj !== undefined) { + // show popup message window + if (this.popupObj.id != previousPopupObjId) { + if (this.popup === undefined) { + this.popup = new Popup(this.frame, this.options.tooltip); + } - // unbind all listeners and reset all variables. - _exportFunctions.destroy = function() { - _bound = {keydown:{}, keyup:{}}; - container.removeEventListener('keydown', down, true); - container.removeEventListener('keyup', up, true); - }; + this.popup.popupTargetType = popupType; + this.popup.popupTargetId = this.popupObj.id; - // create listeners. - container.addEventListener('keydown',down,true); - container.addEventListener('keyup',up,true); + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + this.popup.setPosition(pointer.x + 3, pointer.y - 5); + this.popup.setText(this.popupObj.getTitle()); + this.popup.show(); + } + } else { + if (this.popup) { + this.popup.hide(); + } + } + }, + writable: true, + configurable: true + }, + _checkHidePopup: { - // return the public functions. - return _exportFunctions; - } - return keycharm; - })); + /** + * Check if the popup must be hidden, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer + * @private + */ + value: function _checkHidePopup(pointer) { + var x = this.canvas._XconvertDOMtoCanvas(pointer.x); + var y = this.canvas._YconvertDOMtoCanvas(pointer.y); + var pointerObj = { + left: x, + top: y, + right: x, + bottom: y + }; + var stillOnObj = false; + if (this.popup.popupTargetType == "node") { + if (this.body.nodes[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); + // if the mouse is still one the node, we have to check if it is not also on one that is drawn on top of it. + // we initially only check stillOnObj because this is much faster. + if (stillOnObj === true) { + var overNode = this.selectionHandler.getNodeAt(pointer); + stillOnObj = overNode.id == this.popup.popupTargetId; + } + } + } else { + if (this.selectionHandler.getNodeAt(pointer) === undefined) { + if (this.body.edges[this.popup.popupTargetId] !== undefined) { + stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); + } + } + } -/***/ }, -/* 79 */ -/***/ function(module, exports, __webpack_require__) { + if (stillOnObj === false) { + this.popupObj = undefined; + this.popup.hide(); + } + }, + writable: true, + configurable: true + } + }); - function webpackContext(req) { - throw new Error("Cannot find module '" + req + "'."); - } - webpackContext.keys = function() { return []; }; - webpackContext.resolve = webpackContext; - module.exports = webpackContext; - webpackContext.id = 79; + return InteractionHandler; + })(); + module.exports = InteractionHandler; /***/ }, -/* 80 */ +/* 99 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var util = __webpack_require__(1); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + var keycharm = __webpack_require__(39); - /** - * Created by Alex on 3/17/2015. - */ - - var Label = (function () { - function Label(body, options) { - _classCallCheck(this, Label); + var NavigationHandler = (function () { + function NavigationHandler(body, canvas) { + var _this = this; + _classCallCheck(this, NavigationHandler); this.body = body; + this.canvas = canvas; - this.fontOptions = {}; - this.defaultOptions = { - color: "#343434", - size: 14, // px - face: "arial", - background: "none", - stroke: 0, // px - strokeColor: "white", - align: "horizontal" - }; - util.extend(this.fontOptions, this.defaultOptions); + this.iconsCreated = false; + this.navigationHammers = []; + this.boundFunctions = {}; + this.touchTime = 0; + this.activated = false; - this.setOptions(options); - this.size = { top: 0, left: 0, width: 0, height: 0, yLine: 0 }; // could be cached + + this.body.emitter.on("release", this._stopMovement.bind(this)); + this.body.emitter.on("activate", function () { + _this.activated = true;_this.configureKeyboardBindings(); + }); + this.body.emitter.on("deactivate", function () { + _this.activated = false;_this.configureKeyboardBindings(); + }); + this.body.emitter.on("destroy", function () { + if (_this.keycharm !== undefined) { + _this.keycharm.destroy(); + } + }); + + this.options = {}; } - _prototypeProperties(Label, null, { + _prototypeProperties(NavigationHandler, null, { setOptions: { value: function setOptions(options) { - this.options = options; - if (options.label !== undefined) { - this.labelDirty = true; - } - if (options.font) { - if (typeof options.font === "string") { - var optionsArray = options.font.split(" "); - this.fontOptions.size = optionsArray[0].replace("px", ""); - this.fontOptions.face = optionsArray[1]; - this.fontOptions.color = optionsArray[2]; - } else if (typeof options.font == "object") { - util.protoExtend(this.fontOptions, options.font); - } - this.fontOptions.size = Number(this.fontOptions.size); + if (options !== undefined) { + this.options = options; + this.create(); } }, writable: true, configurable: true }, - draw: { - - - /** - * Main function. This is called from anything that wants to draw a label. - * @param ctx - * @param x - * @param y - * @param selected - * @param baseline - */ - value: function draw(ctx, x, y, selected) { - var baseline = arguments[4] === undefined ? "middle" : arguments[4]; - // if no label, return - if (this.options.label === undefined) { - return; - } // check if we have to render the label - var viewFontSize = this.fontOptions.size * this.body.view.scale; - if (this.options.label && viewFontSize < this.options.scaling.label.drawThreshold - 1) { - return; - } // update the size cache if required - this.calculateLabelSize(ctx, selected, x, y, baseline); + create: { + value: function create() { + if (this.options.showNavigationIcons === true) { + if (this.iconsCreated === false) { + this.loadNavigationElements(); + } + } else if (this.iconsCreated === true) { + this.cleanNavigation(); + } - // create the fontfill background - this._drawBackground(ctx); - // draw text - this._drawText(ctx, selected, x, y, baseline); + this.configureKeyboardBindings(); }, writable: true, configurable: true }, - _drawBackground: { - - /** - * Draws the label background - * @param {CanvasRenderingContext2D} ctx - * @private - */ - value: function _drawBackground(ctx) { - if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") { - ctx.fillStyle = this.fontOptions.background; + cleanNavigation: { + value: function cleanNavigation() { + // clean hammer bindings + if (this.navigationHammers.length != 0) { + for (var i = 0; i < this.navigationHammers.length; i++) { + this.navigationHammers[i].destroy(); + } + this.navigationHammers = []; + } - var lineMargin = 2; + this._navigationReleaseOverload = function () {}; - switch (this.fontOptions.align) { - case "middle": - ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height); - break; - case "top": - ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height); - break; - case "bottom": - ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height); - break; - default: - ctx.fillRect(this.size.left, this.size.top, this.size.width, this.size.height); - break; - } + // clean up previous navigation items + if (this.navigationDOM && this.navigationDOM.wrapper && this.navigationDOM.wrapper.parentNode) { + this.navigationDOM.wrapper.parentNode.removeChild(this.navigationDOM.wrapper); } + + this.iconsCreated = false; }, writable: true, configurable: true }, - _drawText: { - + loadNavigationElements: { /** + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. * - * @param ctx - * @param x - * @param baseline * @private */ - value: function _drawText(ctx, selected, x, y) { - var baseline = arguments[4] === undefined ? "middle" : arguments[4]; - var fontSize = this.fontOptions.size; - var viewFontSize = fontSize * this.body.view.scale; - // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel) - if (viewFontSize >= this.options.scaling.label.maxVisible) { - fontSize = Number(this.options.scaling.label.maxVisible) / this.body.view.scale; - } - - var yLine = this.size.yLine; - var _getColor = this._getColor(viewFontSize); - - var _getColor2 = _slicedToArray(_getColor, 2); - - var fontColor = _getColor2[0]; - var strokeColor = _getColor2[1]; - var _ref = this._setAlignment(ctx, x, yLine, baseline); + value: function loadNavigationElements() { + this.cleanNavigation(); - var _ref2 = _slicedToArray(_ref, 2); + this.navigationDOM = {}; + var navigationDivs = ["up", "down", "left", "right", "zoomIn", "zoomOut", "zoomExtends"]; + var navigationDivActions = ["_moveUp", "_moveDown", "_moveLeft", "_moveRight", "_zoomIn", "_zoomOut", "_zoomExtent"]; - x = _ref2[0]; - yLine = _ref2[1]; + this.navigationDOM.wrapper = document.createElement("div"); + this.canvas.frame.appendChild(this.navigationDOM.wrapper); + for (var i = 0; i < navigationDivs.length; i++) { + this.navigationDOM[navigationDivs[i]] = document.createElement("div"); + this.navigationDOM[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; + this.navigationDOM.wrapper.appendChild(this.navigationDOM[navigationDivs[i]]); - // configure context for drawing the text - ctx.font = (selected ? "bold " : "") + fontSize + "px " + this.fontOptions.face; - ctx.fillStyle = fontColor; - ctx.textAlign = "center"; + var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]); + if (navigationDivActions[i] == "_zoomExtent") { + hammerUtil.onTouch(hammer, this._zoomExtent.bind(this)); + } else { + hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this, navigationDivActions[i])); + } - // set the strokeWidth - if (this.fontOptions.stroke > 0) { - ctx.lineWidth = this.fontOptions.stroke; - ctx.strokeStyle = strokeColor; - ctx.lineJoin = "round"; + this.navigationHammers.push(hammer); } - // draw the text - for (var i = 0; i < this.lineCount; i++) { - if (this.fontOptions.stroke > 0) { - ctx.strokeText(this.lines[i], x, yLine); - } - ctx.fillText(this.lines[i], x, yLine); - yLine += fontSize; + this.iconsCreated = true; + }, + writable: true, + configurable: true + }, + bindToRedraw: { + value: function bindToRedraw(action) { + if (this.boundFunctions[action] === undefined) { + this.boundFunctions[action] = this[action].bind(this); + this.body.emitter.on("initRedraw", this.boundFunctions[action]); + this.body.emitter.emit("_startRendering"); } }, writable: true, configurable: true }, - _setAlignment: { - value: function _setAlignment(ctx, x, yLine, baseline) { - // check for label alignment (for edges) - // TODO: make alignment for nodes - if (this.fontOptions.align !== "horizontal") { - x = 0; - yLine = 0; - - var lineMargin = 2; - if (this.fontOptions.align === "top") { - ctx.textBaseline = "alphabetic"; - yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers - } else if (this.fontOptions.align === "bottom") { - ctx.textBaseline = "hanging"; - yLine += 2 * lineMargin; // distance from edge, required because we use hanging. Hanging has less difference between browsers - } else { - ctx.textBaseline = "middle"; - } - } else { - ctx.textBaseline = baseline; + unbindFromRedraw: { + value: function unbindFromRedraw(action) { + if (this.boundFunctions[action] !== undefined) { + this.body.emitter.off("initRedraw", this.boundFunctions[action]); + this.body.emitter.emit("_stopRendering"); + delete this.boundFunctions[action]; } - - return [x, yLine]; }, writable: true, configurable: true }, - _getColor: { + _zoomExtent: { /** - * fade in when relative scale is between threshold and threshold - 1. - * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here. + * this stops all movement induced by the navigation buttons * - * @param viewFontSize - * @returns {*[]} * @private */ - value: function _getColor(viewFontSize) { - var fontColor = this.fontOptions.color || "#000000"; - var strokeColor = this.fontOptions.strokeColor || "#ffffff"; - if (viewFontSize <= this.options.scaling.label.drawThreshold) { - var opacity = Math.max(0, Math.min(1, 1 - (this.options.scaling.label.drawThreshold - viewFontSize))); - fontColor = util.overrideOpacity(fontColor, opacity); - strokeColor = util.overrideOpacity(strokeColor, opacity); + value: function _zoomExtent() { + if (new Date().valueOf() - this.touchTime > 700) { + // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?) + this.body.emitter.emit("zoomExtent", { duration: 700 }); + this.touchTime = new Date().valueOf(); } - return [fontColor, strokeColor]; }, writable: true, configurable: true }, - getTextSize: { - + _stopMovement: { /** + * this stops all movement induced by the navigation buttons * - * @param ctx - * @param selected - * @returns {{width: number, height: number}} + * @private */ - value: function getTextSize(ctx) { - var selected = arguments[1] === undefined ? false : arguments[1]; - var size = { - width: this._processLabel(ctx, selected), - height: this.fontOptions.size * this.lineCount - }; - return size; + value: function _stopMovement() { + for (var boundAction in this.boundFunctions) { + if (this.boundFunctions.hasOwnProperty(boundAction)) { + this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]); + this.body.emitter.emit("_stopRendering"); + } + } + this.boundFunctions = {}; }, writable: true, configurable: true }, - calculateLabelSize: { - - - /** - * - * @param ctx - * @param selected - * @param x - * @param y - * @param baseline - */ - value: function calculateLabelSize(ctx, selected) { - var x = arguments[2] === undefined ? 0 : arguments[2]; - var y = arguments[3] === undefined ? 0 : arguments[3]; - var baseline = arguments[4] === undefined ? "middle" : arguments[4]; - if (this.labelDirty === true) { - this.size.width = this._processLabel(ctx, selected); - } - this.size.height = this.fontOptions.size * this.lineCount; - this.size.left = x - this.size.width * 0.5; - this.size.top = y - this.size.height * 0.5; - this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size; - if (baseline == "hanging") { - this.size.top += 0.5 * this.fontOptions.size; - this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers - this.size.yLine += 4; // distance from node - } - - this.labelDirty = false; + _moveUp: { + value: function _moveUp() { + this.body.view.translation.y += this.options.keyboard.speed.y; }, writable: true, configurable: true }, - _processLabel: { + _moveDown: { + value: function _moveDown() { + this.body.view.translation.y -= this.options.keyboard.speed.y; + }, + writable: true, + configurable: true + }, + _moveLeft: { + value: function _moveLeft() { + this.body.view.translation.x += this.options.keyboard.speed.x; + }, + writable: true, + configurable: true + }, + _moveRight: { + value: function _moveRight() { + this.body.view.translation.x -= this.options.keyboard.speed.x; + }, + writable: true, + configurable: true + }, + _zoomIn: { + value: function _zoomIn() { + this.body.view.scale += this.options.keyboard.speed.zoom; + }, + writable: true, + configurable: true + }, + _zoomOut: { + value: function _zoomOut() { + this.body.view.scale -= this.options.keyboard.speed.zoom; + }, + writable: true, + configurable: true + }, + configureKeyboardBindings: { /** - * This calculates the width as well as explodes the label string and calculates the amount of lines. - * @param ctx - * @param selected - * @returns {number} - * @private + * bind all keys using keycharm. */ - value: function _processLabel(ctx, selected) { - var width = 0; - var lines = [""]; - var lineCount = 0; - if (this.options.label !== undefined) { - lines = String(this.options.label).split("\n"); - lineCount = lines.length; - ctx.font = (selected ? "bold " : "") + this.fontOptions.size + "px " + this.fontOptions.face; - width = ctx.measureText(lines[0]).width; - for (var i = 1; i < lineCount; i++) { - var lineWidth = ctx.measureText(lines[i]).width; - width = lineWidth > width ? lineWidth : width; + value: function configureKeyboardBindings() { + if (this.keycharm !== undefined) { + this.keycharm.destroy(); + } + + if (this.options.keyboard.enabled === true) { + if (this.options.keyboard.bindToWindow === true) { + this.keycharm = keycharm({ container: window, preventDefault: false }); + } else { + this.keycharm = keycharm({ container: this.canvas.frame, preventDefault: false }); + } + + this.keycharm.reset(); + + if (this.activated === true) { + this.keycharm.bind("up", this.bindToRedraw.bind(this, "_moveUp"), "keydown"); + this.keycharm.bind("down", this.bindToRedraw.bind(this, "_moveDown"), "keydown"); + this.keycharm.bind("left", this.bindToRedraw.bind(this, "_moveLeft"), "keydown"); + this.keycharm.bind("right", this.bindToRedraw.bind(this, "_moveRight"), "keydown"); + this.keycharm.bind("=", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("num+", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("num-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("-", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("[", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + this.keycharm.bind("]", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("pageup", this.bindToRedraw.bind(this, "_zoomIn"), "keydown"); + this.keycharm.bind("pagedown", this.bindToRedraw.bind(this, "_zoomOut"), "keydown"); + + this.keycharm.bind("up", this.unbindFromRedraw.bind(this, "_moveUp"), "keyup"); + this.keycharm.bind("down", this.unbindFromRedraw.bind(this, "_moveDown"), "keyup"); + this.keycharm.bind("left", this.unbindFromRedraw.bind(this, "_moveLeft"), "keyup"); + this.keycharm.bind("right", this.unbindFromRedraw.bind(this, "_moveRight"), "keyup"); + this.keycharm.bind("=", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("num+", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("num-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("-", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("[", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); + this.keycharm.bind("]", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("pageup", this.unbindFromRedraw.bind(this, "_zoomIn"), "keyup"); + this.keycharm.bind("pagedown", this.unbindFromRedraw.bind(this, "_zoomOut"), "keyup"); } } - this.lines = lines; - this.lineCount = lineCount; - - return width; }, writable: true, configurable: true } }); - return Label; + return NavigationHandler; })(); - module.exports = Label; + module.exports = NavigationHandler; /***/ }, -/* 81 */ +/* 100 */ /***/ function(module, exports, __webpack_require__) { - /** - * Created by Alex on 3/18/2015. - */ "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + /** + * Popup is a class to create a popup window with some text + * @param {Element} container The container object. + * @param {Number} [x] + * @param {Number} [y] + * @param {String} [text] + * @param {Object} [style] An object containing borderColor, + * backgroundColor, etc. + */ + var Popup = (function () { + function Popup(container, x, y, text, style) { + _classCallCheck(this, Popup); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + if (container) { + this.container = container; + } else { + this.container = document.body; + } - var NodeBase = _interopRequire(__webpack_require__(100)); + // x, y and text are optional, see if a style object was passed in their place + if (style === undefined) { + if (typeof x === "object") { + style = x; + x = undefined; + } else if (typeof text === "object") { + style = text; + text = undefined; + } else { + // for backwards compatibility, in case clients other than Network are creating Popup directly + style = { + fontColor: "black", + fontSize: 14, // px + fontFace: "verdana", + color: { + border: "#666", + background: "#FFFFC6" + } + }; + } + } - var Box = (function (NodeBase) { - function Box(options, body, labelModule) { - _classCallCheck(this, Box); + this.x = 0; + this.y = 0; + this.padding = 5; + this.hidden = false; - _get(Object.getPrototypeOf(Box.prototype), "constructor", this).call(this, options, body, labelModule); + if (x !== undefined && y !== undefined) { + this.setPosition(x, y); + } + if (text !== undefined) { + this.setText(text); + } + + // 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); } - _inherits(Box, NodeBase); + _prototypeProperties(Popup, null, { + setPosition: { - _prototypeProperties(Box, null, { - resize: { - value: function resize(ctx) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, this.selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; - } + /** + * @param {number} x Horizontal position of the popup window + * @param {number} y Vertical position of the popup window + */ + value: function setPosition(x, y) { + this.x = parseInt(x); + this.y = parseInt(y); }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + setText: { - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + /** + * Set the content for the popup window. This can be HTML code or text. + * @param {string | Element} content + */ + value: function setText(content) { + if (content instanceof Element) { + this.frame.innerHTML = ""; + this.frame.appendChild(content); + } else { + this.frame.innerHTML = content; // string containing text or HTML + } + }, + writable: true, + configurable: true + }, + show: { - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + /** + * Show the popup window + * @param {boolean} show Optional. Show or hide the window + */ + value: function show(show) { + if (show === undefined) { + show = true; + } - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; + if (show === true) { + var height = this.frame.clientHeight; + var width = this.frame.clientWidth; + var maxHeight = this.frame.parentNode.clientHeight; + var maxWidth = this.frame.parentNode.clientWidth; - ctx.roundRect(this.left, this.top, this.width, this.height, this.options.size); - ctx.fill(); - ctx.stroke(); + var top = this.y - height; + if (top + height + this.padding > maxHeight) { + top = maxHeight - height - this.padding; + } + if (top < this.padding) { + top = this.padding; + } - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + var left = this.x; + if (left + width + this.padding > maxWidth) { + left = maxWidth - width - this.padding; + } + if (left < this.padding) { + left = this.padding; + } - this.labelModule.draw(ctx, x, y, selected); + this.frame.style.left = left + "px"; + this.frame.style.top = top + "px"; + this.frame.style.visibility = "visible"; + this.hidden = false; + } else { + this.hide(); + } }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + hide: { + + /** + * Hide the popup window + */ + value: function hide() { + this.hidden = true; + this.frame.style.visibility = "hidden"; }, writable: true, configurable: true } }); - return Box; - })(NodeBase); + return Popup; + })(); - module.exports = Box; + module.exports = Popup; /***/ }, -/* 82 */ +/* 101 */ /***/ function(module, exports, __webpack_require__) { - /** - * Created by Alex on 3/18/2015. - */ "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + /** + * Created by Alex on 2/27/2015. + */ - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var Node = __webpack_require__(60); + var util = __webpack_require__(1); + + var SelectionHandler = (function () { + function SelectionHandler(body, canvas) { + var _this = this; + _classCallCheck(this, SelectionHandler); - var CircleImageBase = _interopRequire(__webpack_require__(101)); + this.body = body; + this.canvas = canvas; + this.selectionObj = { nodes: [], edges: [] }; + this.forceSelectEdges = false; - var Circle = (function (CircleImageBase) { - function Circle(options, body, labelModule) { - _classCallCheck(this, Circle); + this.options = {}; + this.defaultOptions = { + select: true, + selectConnectedEdges: true + }; + util.extend(this.options, this.defaultOptions); - _get(Object.getPrototypeOf(Circle.prototype), "constructor", this).call(this, options, body, labelModule); + this.body.emitter.on("_dataChanged", function () { + _this.updateSelection(); + }); } - _inherits(Circle, CircleImageBase); + _prototypeProperties(SelectionHandler, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.options, options); + } + }, + writable: true, + configurable: true + }, + selectOnPoint: { - _prototypeProperties(Circle, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var diameter = Math.max(textSize.width, textSize.height) + 2 * margin; - this.options.size = diameter / 2; - this.width = diameter; - this.height = diameter; + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + value: function selectOnPoint(pointer) { + var selected = false; + if (this.options.select === true) { + this.unselectAll(); + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; + if (obj !== undefined) { + selected = this.selectObject(obj); + } + this.body.emitter.emit("_requestRedraw"); } + return selected; }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - this._drawRawCircle(ctx, x, y, selected, hover, this.options.size); + selectAdditionalOnPoint: { + value: function selectAdditionalOnPoint(pointer) { + var selectionChanged = false; + if (this.options.select === true) { + var obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);; - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + if (obj !== undefined) { + selectionChanged = true; + if (obj.isSelected() === true) { + this.deselectObject(obj); + } else { + this.selectObject(obj); + } - this.labelModule.draw(ctx, x, y, selected); + this.body.emitter.emit("_requestRedraw"); + } + } + return selectionChanged; }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + _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 - } - }); - - return Circle; - })(CircleImageBase); + }, + selectObject: { + value: function selectObject(obj) { + if (obj !== undefined) { + if (obj instanceof Node) { + if (this.options.selectConnectedEdges === true || this.forceSelectEdges === true) { + this._selectConnectedEdges(obj); + } + } + obj.select(); + this._addToSelection(obj); + return true; + } + return false; + }, + writable: true, + configurable: true + }, + deselectObject: { + value: function deselectObject(obj) { + if (obj.isSelected() === true) { + obj.selected = false; + this._removeFromSelection(obj); + } + }, + writable: true, + configurable: true + }, + _getAllNodesOverlappingWith: { - module.exports = Circle; -/***/ }, -/* 83 */ -/***/ function(module, exports, __webpack_require__) { - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getAllNodesOverlappingWith(object) { + var overlappingNodes = []; + var nodes = this.body.nodes; + for (var i = 0; i < this.body.nodeIndices.length; i++) { + var nodeId = this.body.nodeIndices[i]; + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + return overlappingNodes; + }, + writable: true, + configurable: true + }, + _pointerToPositionObject: { - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + value: function _pointerToPositionObject(pointer) { + var canvasPos = this.canvas.DOMtoCanvas(pointer); + return { + left: canvasPos.x - 1, + top: canvasPos.y + 1, + right: canvasPos.x + 1, + bottom: canvasPos.y - 1 + }; + }, + writable: true, + configurable: true + }, + getNodeAt: { - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + /** + * 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 _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // if there 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: { - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var CircleImageBase = _interopRequire(__webpack_require__(101)); + /** + * retrieve all edges overlapping with given object, selector is around center + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + value: function _getEdgesOverlappingWith(object, overlappingEdges) { + var edges = this.body.edges; + for (var i = 0; i < this.body.edgeIndices.length; i++) { + var edgeId = this.body.edgeIndices[i]; + if (edges[edgeId].isOverlappingWith(object)) { + overlappingEdges.push(edgeId); + } + } + }, + writable: true, + configurable: true + }, + _getAllEdgesOverlappingWith: { - var CircularImage = (function (CircleImageBase) { - function CircularImage(options, body, labelModule, imageObj) { - _classCallCheck(this, CircularImage); - _get(Object.getPrototypeOf(CircularImage.prototype), "constructor", this).call(this, options, body, labelModule); - this.imageObj = imageObj; - } + /** + * 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: { - _inherits(CircularImage, CircleImageBase); - _prototypeProperties(CircularImage, null, { - resize: { - value: function resize(ctx) { - if (this.imageObj.src !== undefined || this.imageObj.width !== undefined || this.imageObj.height !== undefined) { - if (!this.width) { - var diameter = this.options.size * 2; - this.width = diameter; - this.height = diameter; - this._swapToImageResizeWhenImageLoaded = true; - } + /** + * 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 { - if (this._swapToImageResizeWhenImageLoaded) { - this.width = 0; - this.height = 0; - delete this._swapToImageResizeWhenImageLoaded; - } - this._resizeImage(ctx); + return undefined; } }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var size = Math.abs(this.height / 2); - this._drawRawCircle(ctx, x, y, selected, hover, size); - - ctx.save(); - ctx.circle(x, y, size); - ctx.stroke(); - ctx.clip(); - - this._drawImageAtPosition(ctx); - - ctx.restore(); - - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + _addToSelection: { - this._drawImageLabel(ctx, x, y, selected); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + /** + * 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 }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); + _addToHover: { + + /** + * 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 - } - }); - - return CircularImage; - })(CircleImageBase); - - module.exports = CircularImage; - -/***/ }, -/* 84 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var NodeBase = _interopRequire(__webpack_require__(100)); - - var Database = (function (NodeBase) { - function Database(options, body, labelModule) { - _classCallCheck(this, Database); - - _get(Object.getPrototypeOf(Database.prototype), "constructor", this).call(this, options, body, labelModule); - } + }, + _removeFromSelection: { - _inherits(Database, NodeBase); - _prototypeProperties(Database, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - var size = textSize.width + 2 * margin; - this.width = size; - this.height = size; + /** + * 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 }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; + unselectAll: { - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = this.selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + /** + * 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(); + } + } - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.database(x - this.width / 2, y - this.height * 0.5, this.width, this.height); - ctx.fill(); - ctx.stroke(); + this.selectionObj = { nodes: {}, edges: {} }; + }, + writable: true, + configurable: true + }, + _getSelectedNodeCount: { - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; - this.labelModule.draw(ctx, x, y, selected); + /** + * 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 }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + _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]; + } + } + return undefined; }, writable: true, configurable: true - } - }); - - return Database; - })(NodeBase); - - module.exports = Database; - -/***/ }, -/* 85 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var ShapeBase = _interopRequire(__webpack_require__(102)); - - var Diamond = (function (ShapeBase) { - function Diamond(options, body, labelModule) { - _classCallCheck(this, Diamond); + }, + _getSelectedEdge: { - _get(Object.getPrototypeOf(Diamond.prototype), "constructor", this).call(this, options, body, labelModule); - } + /** + * 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: { - _inherits(Diamond, ShapeBase); - _prototypeProperties(Diamond, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); + /** + * 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 }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "diamond", 4, x, y, selected, hover); + _getSelectedObjectCount: { + + + /** + * return the number of selected objects. + * + * @returns {number} + * @private + */ + value: function _getSelectedObjectCount() { + var count = 0; + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; + } + } + return count; }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); + _selectionIsEmpty: { + + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + value: function _selectionIsEmpty() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + return false; + } + } + return true; }, writable: true, configurable: true - } - }); - - return Diamond; - })(ShapeBase); - - module.exports = Diamond; - -/***/ }, -/* 86 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var ShapeBase = _interopRequire(__webpack_require__(102)); - - var Dot = (function (ShapeBase) { - function Dot(options, body, labelModule) { - _classCallCheck(this, Dot); - - _get(Object.getPrototypeOf(Dot.prototype), "constructor", this).call(this, options, body, labelModule); - } + }, + _clusterInSelection: { - _inherits(Dot, ShapeBase); - _prototypeProperties(Dot, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); + /** + * 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 }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "circle", 2, x, y, selected, hover); + _selectConnectedEdges: { + + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _selectConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.select(); + this._addToSelection(edge); + } }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this.options.size + this.options.borderWidth; + _hoverConnectedEdges: { + + /** + * select the edges connected to the node that is being selected + * + * @param {Node} node + * @private + */ + value: function _hoverConnectedEdges(node) { + for (var i = 0; i < node.edges.length; i++) { + var edge = node.edges[i]; + edge.hover = true; + this._addToHover(edge); + } }, writable: true, configurable: true - } - }); - - return Dot; - })(ShapeBase); - - module.exports = Dot; - -/***/ }, -/* 87 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + }, + _unselectConnectedEdges: { - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * 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: { - var NodeBase = _interopRequire(__webpack_require__(100)); - var Ellipse = (function (NodeBase) { - function Ellipse(options, body, labelModule) { - _classCallCheck(this, Ellipse); - _get(Object.getPrototypeOf(Ellipse.prototype), "constructor", this).call(this, options, body, labelModule); - } - _inherits(Ellipse, NodeBase); - _prototypeProperties(Ellipse, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width * 1.5; - this.height = textSize.height * 2; - if (this.width < this.height) { - this.width = this.height; - } + /** + * 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 }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected); - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; + hoverObject: { - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); + /** + * 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: { - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.ellipse(this.left, this.top, this.width, this.height); - ctx.fill(); - ctx.stroke(); - this.boundingBox.left = this.left; - this.boundingBox.top = this.top; - this.boundingBox.bottom = this.top + this.height; - this.boundingBox.right = this.left + this.width; - this.labelModule.draw(ctx, x, y, selected); + /** + * + * 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 }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + getSelectedNodes: { + + /** + * + * retrieve the currently selected nodes + * @return {String[]} selection An array with the ids of the + * selected nodes. + */ + value: function getSelectedNodes() { + var idArray = []; + if (this.options.select == true) { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); + } + } + } + return idArray; }, writable: true, configurable: true - } - }); - - return Ellipse; - })(NodeBase); - - module.exports = Ellipse; - -/***/ }, -/* 88 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + }, + getSelectedEdges: { - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + /** + * + * 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: { - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var NodeBase = _interopRequire(__webpack_require__(100)); + /** + * 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 Icon = (function (NodeBase) { - function Icon(options, body, labelModule) { - _classCallCheck(this, Icon); + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - _get(Object.getPrototypeOf(Icon.prototype), "constructor", this).call(this, options, body, labelModule); - } + // first unselect any selected node + this.unselectAll(true); - _inherits(Icon, NodeBase); + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - _prototypeProperties(Icon, null, { - resize: { - value: function resize(ctx) { - if (this.width === undefined) { - var margin = 5; - var iconSize = { - width: Number(this.options.icon.size), - height: Number(this.options.icon.size) - }; - this.width = iconSize.width + 2 * margin; - this.height = iconSize.height + 2 * margin; + 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 }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.options.icon.size = this.options.icon.size || 50; + selectEdges: { - this.left = x - this.width * 0.5; - this.top = y - this.height * 0.5; - this._icon(ctx, x, y, selected); + /** + * 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; - this.boundingBox.top = y - this.options.icon.size * 0.5; - this.boundingBox.left = x - this.options.icon.size * 0.5; - this.boundingBox.right = x + this.options.icon.size * 0.5; - this.boundingBox.bottom = y + this.options.icon.size * 0.5; + if (!selection || selection.length == undefined) throw "Selection must be an array with ids"; - if (this.options.label !== undefined) { - var iconTextSpacing = 5; - this.labelModule.draw(ctx, x, y + this.height * 0.5 + iconTextSpacing, selected); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); - } - }, - writable: true, - configurable: true - }, - _icon: { - value: function _icon(ctx, x, y, selected) { - var iconSize = Number(this.options.icon.size); - var relativeIconSize = iconSize * this.body.view.scale; + // first unselect any selected node + this.unselectAll(true); - if (this.options.icon.code && relativeIconSize > this.options.scaling.label.drawThreshold - 1) { - ctx.font = (selected ? "bold " : "") + iconSize + "px " + this.options.icon.face; + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - // draw icon - ctx.fillStyle = this.options.icon.color || "black"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.options.icon.code, x, y); + 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 }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - this._distanceToBorder(angle); + updateSelection: { + + /** + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ + value: function updateSelection() { + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.body.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; + } + } + } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.body.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; + } + } + } }, writable: true, configurable: true } }); - return Icon; - })(NodeBase); + return SelectionHandler; + })(); - module.exports = Icon; + module.exports = SelectionHandler; /***/ }, -/* 89 */ +/* 102 */ /***/ function(module, exports, __webpack_require__) { - /** - * Created by Alex on 3/18/2015. - */ "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + /** + * Created by Alex on 3/3/2015. + */ - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var util = __webpack_require__(1); - var CircleImageBase = _interopRequire(__webpack_require__(101)); + var LayoutEngine = (function () { + function LayoutEngine(body) { + var _this = this; + _classCallCheck(this, LayoutEngine); - var Image = (function (CircleImageBase) { - function Image(options, body, labelModule, imageObj) { - _classCallCheck(this, Image); + this.body = body; - _get(Object.getPrototypeOf(Image.prototype), "constructor", this).call(this, options, body, labelModule); - this.imageObj = imageObj; + this.initialRandomSeed = Math.round(Math.random() * 1000000); + this.randomSeed = this.initialRandomSeed; + this.options = {}; + this.defaultOptions = { + randomSeed: undefined, + hierarchical: { + enabled: false, + levelSeparation: 150, + direction: "UD", // UD, DU, LR, RL + sortMethod: "hubsize" // hubsize, directed + } + }; + util.extend(this.options, this.defaultOptions); + + this.hierarchicalLevels = {}; + + this.body.emitter.on("_dataChanged", function () { + _this.setupHierarchicalLayout(); + }); } - _inherits(Image, CircleImageBase); + _prototypeProperties(LayoutEngine, null, { + setOptions: { + value: function setOptions(options, allOptions) { + if (options !== undefined) { + util.mergeOptions(this.options, options, "hierarchical"); + if (options.randomSeed !== undefined) { + this.randomSeed = options.randomSeed; + } - _prototypeProperties(Image, null, { - resize: { - value: function resize() { - if (!this.width || !this.height) { - // undefined or 0 - var width, height; - if (this.value) { - var scale = this.imageObj.height / this.imageObj.width; - if (scale !== undefined) { - width = this.options.size || this.imageObj.width; - height = this.options.size * scale || this.imageObj.height; + if (this.options.hierarchical.enabled === true) { + // make sure the level seperation is the right way up + if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "DU") { + if (this.options.hierarchical.levelSeparation > 0) { + this.options.hierarchical.levelSeparation *= -1; + } } else { - width = 0; - height = 0; + if (this.options.hierarchical.levelSeparation < 0) { + this.options.hierarchical.levelSeparation *= -1; + } } - } else { - width = this.imageObj.width; - height = this.imageObj.height; + + // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. + return this.adaptAllOptions(allOptions); } - this.width = width; - this.height = height; } + return allOptions; }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + adaptAllOptions: { + value: function adaptAllOptions(allOptions) { + if (this.options.hierarchical.enabled === true) { + // set the physics + if (allOptions.physics === undefined || allOptions.physics === true) { + allOptions.physics = { solver: "hierarchicalRepulsion" }; + } else if (options.physics !== false) { + allOptions.physics.solver = "hierarchicalRepulsion"; + } - this._drawImageAtPosition(ctx); + // get the type of static smooth curve in case it is required + var type = "horizontal"; + if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "LR") { + type = "vertical"; + } - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. + if (allOptions.edges === undefined) { + allOptions.edges = { smooth: false }; + } else if (allOptions.edges.smooth === undefined) { + allOptions.edges.smooth = false; + } else { + allOptions.edges.smooth = { enabled: true, dynamic: false, type: type }; + } - this._drawImageLabel(ctx, x, y, selected || hover); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + // force all edges into static smooth curves. + this.body.emitter.emit("_forceDisableDynamicCurves", type); + } + return allOptions; }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - var a = this.width / 2; - var b = this.height / 2; - var w = Math.sin(angle) * a; - var h = Math.cos(angle) * b; - return a * b / Math.sqrt(w * w + h * h); + seededRandom: { + value: function seededRandom() { + var x = Math.sin(this.randomSeed++) * 10000; + return x - Math.floor(x); }, writable: true, configurable: true - } - }); - - return Image; - })(CircleImageBase); - - module.exports = Image; - -/***/ }, -/* 90 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var ShapeBase = _interopRequire(__webpack_require__(102)); - - var Square = (function (ShapeBase) { - function Square(options, body, labelModule) { - _classCallCheck(this, Square); - - _get(Object.getPrototypeOf(Square.prototype), "constructor", this).call(this, options, body, labelModule); - } - - _inherits(Square, ShapeBase); + }, + positionInitially: { + value: function positionInitially(nodesArray) { + if (this.options.hierarchical.enabled !== true) { + for (var i = 0; i < nodesArray.length; i++) { + var node = nodesArray[i]; + if (!node.isFixed() && (node.x === undefined || node.y === undefined)) { + var radius = 10 * 0.1 * nodesArray.length + 10; + var angle = 2 * Math.PI * this.seededRandom(); - _prototypeProperties(Square, null, { - resize: { - value: function resize() { - this._resizeShape(); + if (node.options.fixed.x == false) { + node.x = radius * Math.cos(angle); + } + if (node.options.fixed.x == false) { + node.y = radius * Math.sin(angle); + } + } + } + } }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "square", 2, x, y, selected, hover); + getSeed: { + value: function getSeed() { + return this.initialRandomSeed; }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true - } - }); - - return Square; - })(ShapeBase); - - module.exports = Square; - -/***/ }, -/* 91 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + setupHierarchicalLayout: { - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + /** + * This is the main function to layout the nodes in a hierarchical way. + * It checks if the node details are supplied correctly + * + * @private + */ + value: function setupHierarchicalLayout() { + if (this.options.hierarchical.enabled == true && this.body.nodeIndices.length > 0) { + // get the size of the largest hubs and check if the user has defined a level for a node. + var node = undefined, + nodeId = undefined; + var definedLevel = false; + var undefinedLevel = false; + this.hierarchicalLevels = {}; + this.nodeSpacing = 100; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.options.level !== undefined) { + definedLevel = true; + this.hierarchicalLevels[nodeId] = node.options.level; + } else { + undefinedLevel = true; + } + } + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // if the user defined some levels but not all, alert and run without hierarchical layout + if (undefinedLevel == true && definedLevel == true) { + throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); + return; + } else { + // setup the system to use hierarchical method. + //this._changeConstants(); - var ShapeBase = _interopRequire(__webpack_require__(102)); + // define levels if undefined by the users. Based on hubsize + if (undefinedLevel == true) { + if (this.options.hierarchical.sortMethod == "hubsize") { + this._determineLevelsByHubsize(); + } else if (this.options.hierarchical.sortMethod == "directed" || "direction") { + this._determineLevelsDirected(); + } + } + // check the distribution of the nodes per level. + var distribution = this._getDistribution(); - var Star = (function (ShapeBase) { - function Star(options, body, labelModule) { - _classCallCheck(this, Star); + // place the nodes on the canvas. + this._placeNodesByHierarchy(distribution); + } + } + }, + writable: true, + configurable: true + }, + _placeNodesByHierarchy: { - _get(Object.getPrototypeOf(Star.prototype), "constructor", this).call(this, options, body, labelModule); - } + /** + * This function places the nodes on the canvas based on the hierarchial distribution. + * + * @param {Object} distribution | obtained by the function this._getDistribution() + * @private + */ + value: function _placeNodesByHierarchy(distribution) { + var nodeId = undefined, + node = undefined; + this.positionedNodes = {}; - _inherits(Star, ShapeBase); + // start placing all the level 0 nodes first. Then recursively position their branches. + for (var level in distribution) { + if (distribution.hasOwnProperty(level)) { + for (nodeId in distribution[level].nodes) { + if (distribution[level].nodes.hasOwnProperty(nodeId)) { + node = distribution[level].nodes[nodeId]; + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + if (node.x === undefined) { + node.x = distribution[level].distance; + } + distribution[level].distance = node.x + this.nodeSpacing; + } else { + if (node.y === undefined) { + node.y = distribution[level].distance; + } + distribution[level].distance = node.y + this.nodeSpacing; + } - _prototypeProperties(Star, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); + this.positionedNodes[nodeId] = true; + this._placeBranchNodes(node.edges, node.id, distribution, level); + } + } + } + } }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "star", 4, x, y, selected, hover); + _getDistribution: { + + + /** + * This function get the distribution of levels based on hubsize + * + * @returns {Object} + * @private + */ + value: function _getDistribution() { + var distribution = {}; + var nodeId = undefined, + node = undefined; + + // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // the fix of X is removed after the x value has been set. + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + node.y = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; + node.options.fixed.y = true; + } else { + node.x = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId]; + node.options.fixed.x = true; + } + if (distribution[this.hierarchicalLevels[nodeId]] === undefined) { + distribution[this.hierarchicalLevels[nodeId]] = { amount: 0, nodes: {}, distance: 0 }; + } + distribution[this.hierarchicalLevels[nodeId]].amount += 1; + distribution[this.hierarchicalLevels[nodeId]].nodes[nodeId] = node; + } + } + return distribution; }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); + _getHubSize: { + + + /** + * Get the hubsize from all remaining unlevelled nodes. + * + * @returns {number} + * @private + */ + value: function _getHubSize() { + var hubSize = 0; + for (var nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + var node = this.body.nodes[nodeId]; + if (this.hierarchicalLevels[nodeId] === undefined) { + hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; + } + } + } + return hubSize; }, writable: true, configurable: true - } - }); - - return Star; - })(ShapeBase); - - module.exports = Star; - -/***/ }, -/* 92 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - var NodeBase = _interopRequire(__webpack_require__(100)); + }, + _determineLevelsByHubsize: { - var Text = (function (NodeBase) { - function Text(options, body, labelModule) { - _classCallCheck(this, Text); - _get(Object.getPrototypeOf(Text.prototype), "constructor", this).call(this, options, body, labelModule); - } + /** + * this function allocates nodes in levels based on the recursive branching from the largest hubs. + * + * @param hubsize + * @private + */ + value: function _determineLevelsByHubsize() { + var nodeId = undefined, + node = undefined; + var hubSize = 1; - _inherits(Text, NodeBase); + while (hubSize > 0) { + // determine hubs + hubSize = this._getHubSize(); + if (hubSize == 0) break; - _prototypeProperties(Text, null, { - resize: { - value: function resize(ctx, selected) { - if (this.width === undefined) { - var margin = 5; - var textSize = this.labelModule.getTextSize(ctx, selected); - this.width = textSize.width + 2 * margin; - this.height = textSize.height + 2 * margin; + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + if (node.edges.length == hubSize) { + this._setLevel(0, node); + } + } + } } }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this.resize(ctx, selected || hover); - this.left = x - this.width / 2; - this.top = y - this.height / 2; + _setLevel: { - this.labelModule.draw(ctx, x, y, selected || hover); - this.boundingBox.top = this.top; - this.boundingBox.left = this.left; - this.boundingBox.right = this.left + this.width; - this.boundingBox.bottom = this.top + this.height; + /** + * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. + * + * @param level + * @param edges + * @param parentId + * @private + */ + value: function _setLevel(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) { + return; + }var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId == node.id) { + childNode = node.edges[i].from; + } else { + childNode = node.edges[i].to; + } + this._setLevel(level + 1, childNode); + } }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - this.resize(ctx); - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true - } - }); - - return Text; - })(NodeBase); - - module.exports = Text; - -/***/ }, -/* 93 */ -/***/ function(module, exports, __webpack_require__) { - - /** - * Created by Alex on 3/18/2015. - */ - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + _determineLevelsDirected: { - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var ShapeBase = _interopRequire(__webpack_require__(102)); - var Triangle = (function (ShapeBase) { - function Triangle(options, body, labelModule) { - _classCallCheck(this, Triangle); + /** + * this function allocates nodes in levels based on the direction of the edges + * + * @param hubsize + * @private + */ + value: function _determineLevelsDirected() { + var nodeId = undefined, + node = undefined; + var minLevel = 10000; - _get(Object.getPrototypeOf(Triangle.prototype), "constructor", this).call(this, options, body, labelModule); - } + // set first node to source + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + node = this.body.nodes[nodeId]; + this._setLevelDirected(minLevel, node); + } + } - _inherits(Triangle, ShapeBase); + // get the minimum level + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel; + } + } - _prototypeProperties(Triangle, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); + // subtract the minimum from the set so we have a range starting from 0 + for (nodeId in this.body.nodes) { + if (this.body.nodes.hasOwnProperty(nodeId)) { + this.hierarchicalLevels[nodeId] -= minLevel; + } + } }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "triangle", 3, x, y, selected, hover); + _setLevelDirected: { + + + /** + * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction + * + * @param level + * @param edges + * @param parentId + * @private + */ + value: function _setLevelDirected(level, node) { + if (this.hierarchicalLevels[node.id] !== undefined) { + return; + }var childNode = undefined; + this.hierarchicalLevels[node.id] = level; + + for (var i = 0; i < node.edges.length; i++) { + if (node.edges[i].toId == node.id) { + childNode = node.edges[i].from; + this._setLevelDirected(level - 1, childNode); + } else { + childNode = node.edges[i].to; + this._setLevelDirected(level + 1, childNode); + } + } }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); + _placeBranchNodes: { + + + + /** + * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes + * on a X position that ensures there will be no overlap. + * + * @param edges + * @param parentId + * @param distribution + * @param parentLevel + * @private + */ + value: function _placeBranchNodes(edges, parentId, distribution, parentLevel) { + for (var i = 0; i < edges.length; i++) { + var childNode = undefined; + var parentNode = undefined; + if (edges[i].toId == parentId) { + childNode = edges[i].from; + parentNode = edges[i].to; + } else { + childNode = edges[i].to; + parentNode = edges[i].from; + } + var childNodeLevel = this.hierarchicalLevels[childNode.id]; + if (this.positionedNodes[childNode.id] === undefined) { + // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. + if (childNodeLevel > parentLevel) { + if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") { + if (childNode.x === undefined) { + childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x); + } + distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing; + this.positionedNodes[childNode.id] = true; + } else { + if (childNode.y === undefined) { + childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y); + } + distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing; + } + this.positionedNodes[childNode.id] = true; + + if (childNode.edges.length > 1) { + this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel); + } + } + } + } }, writable: true, configurable: true } }); - return Triangle; - })(ShapeBase); + return LayoutEngine; + })(); - module.exports = Triangle; + module.exports = LayoutEngine; /***/ }, -/* 94 */ +/* 103 */ /***/ function(module, exports, __webpack_require__) { - /** - * Created by Alex on 3/18/2015. - */ "use strict"; - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + var util = __webpack_require__(1); + var Hammer = __webpack_require__(19); + var hammerUtil = __webpack_require__(24); + var locales = __webpack_require__(104); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + /** + * clears the toolbar div element of children + * + * @private + */ + var ManipulationSystem = (function () { + function ManipulationSystem(body, canvas, selectionHandler) { + _classCallCheck(this, ManipulationSystem); - var ShapeBase = _interopRequire(__webpack_require__(102)); + this.body = body; + this.canvas = canvas; + this.selectionHandler = selectionHandler; - var TriangleDown = (function (ShapeBase) { - function TriangleDown(options, body, labelModule) { - _classCallCheck(this, TriangleDown); + this.editMode = false; + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; - _get(Object.getPrototypeOf(TriangleDown.prototype), "constructor", this).call(this, options, body, labelModule); + this.manipulationHammers = []; + this.temporaryUIFunctions = {}; + this.temporaryEventFunctions = []; + + this.touchTime = 0; + this.temporaryIds = { nodes: [], edges: [] }; + this.guiEnabled = false; + this.selectedControlNode = undefined; + + this.options = {}; + this.defaultOptions = { + enabled: false, + initiallyVisible: false, + locale: "en", + locales: locales, + functionality: { + addNode: true, + addEdge: true, + editNode: true, + editEdge: true, + deleteNode: true, + deleteEdge: true + }, + handlerFunctions: { + addNode: undefined, + addEdge: undefined, + editNode: undefined, + editEdge: undefined, + deleteNode: undefined, + deleteEdge: undefined + }, + controlNodeStyle: { + shape: "dot", + size: 6, + color: { background: "#ff0000", border: "#3c3c3c", highlight: { background: "#07f968" } }, + borderWidth: 2, + borderWidthSelected: 2 + } + }; + util.extend(this.options, this.defaultOptions); } - _inherits(TriangleDown, ShapeBase); + _prototypeProperties(ManipulationSystem, null, { + setOptions: { - _prototypeProperties(TriangleDown, null, { - resize: { - value: function resize(ctx) { - this._resizeShape(); + + /** + * Set the Options + * @param options + */ + value: function setOptions(options) { + if (options !== undefined) { + if (typeof options == "boolean") { + this.options.enabled = options; + } else { + this.options.enabled = true; + util.deepExtend(this.options, options); + } + if (this.options.initiallyVisible === true) { + this.editMode = true; + } + this._setup(); + } }, writable: true, configurable: true }, - draw: { - value: function draw(ctx, x, y, selected, hover) { - this._drawShape(ctx, "triangleDown", 3, x, y, selected, hover); + toggleEditMode: { + + + /** + * Enable or disable edit-mode. Draws the DOM required and cleans up after itself. + * + * @private + */ + value: function toggleEditMode() { + this.editMode = !this.editMode; + if (this.guiEnabled === true) { + var toolbar = this.manipulationDiv; + var closeDiv = this.closeDiv; + var editModeDiv = this.editModeDiv; + if (this.editMode === true) { + toolbar.style.display = "block"; + closeDiv.style.display = "block"; + editModeDiv.style.display = "none"; + this._bindHammerToDiv(closeDiv, this.toggleEditMode.bind(this)); + this.showManipulatorToolbar(); + } else { + toolbar.style.display = "none"; + closeDiv.style.display = "none"; + editModeDiv.style.display = "block"; + this._createEditButton(); + } + } }, writable: true, configurable: true }, - distanceToBorder: { - value: function distanceToBorder(ctx, angle) { - return this._distanceToBorder(angle); - }, - writable: true, - configurable: true - } - }); - - return TriangleDown; - })(ShapeBase); - - module.exports = TriangleDown; - -/***/ }, -/* 95 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + showManipulatorToolbar: { - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + /** + * Creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. + * + * @private + */ + value: function showManipulatorToolbar() { + // restore the state of any bound functions or events, remove control nodes, restore physics + this._clean(); - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // reset global letiables + this.manipulationDOM = {}; - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + var selectedNodeCount = this.selectionHandler._getSelectedNodeCount(); + var selectedEdgeCount = this.selectionHandler._getSelectedEdgeCount(); + var selectedTotalCount = selectedNodeCount + selectedEdgeCount; + var locale = this.options.locales[this.options.locale]; + var needSeperator = false; - /** - * Created by Alex on 3/20/2015. - */ + if (this.options.functionality.addNode === true) { + this._createAddNodeButton(locale); + needSeperator = true; + } + if (this.options.functionality.addEdge === true) { + if (needSeperator === true) { + this._createSeperator(1); + } else { + needSeperator = true; + } + this._createAddEdgeButton(locale); + } - var BezierEdgeBase = _interopRequire(__webpack_require__(103)); + if (selectedNodeCount === 1 && typeof this.options.handlerFunctions.editNode === "function" && this.options.functionality.editNode === true) { + if (needSeperator === true) { + this._createSeperator(2); + } else { + needSeperator = true; + } + this._createEditNodeButton(locale); + } else if (selectedEdgeCount === 1 && selectedNodeCount === 0 && this.options.functionality.editEdge === true) { + if (needSeperator === true) { + this._createSeperator(3); + } else { + needSeperator = true; + } + this._createEditEdgeButton(locale); + } - var BezierEdgeDynamic = (function (BezierEdgeBase) { - function BezierEdgeDynamic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeDynamic); + // remove buttons + if (selectedTotalCount !== 0) { + if (selectedNodeCount === 1 && this.options.functionality.deleteNode === true) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } else if (selectedNodeCount === 0 && this.options.functionality.deleteEdge === true) { + if (needSeperator === true) { + this._createSeperator(4); + } + this._createDeleteButton(locale); + } + } - this.via = undefined; - _get(Object.getPrototypeOf(BezierEdgeDynamic.prototype), "constructor", this).call(this, options, body, labelModule); // --> this calls the setOptions below - } + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); - _inherits(BezierEdgeDynamic, BezierEdgeBase); + // refresh this bar based on what has been selected + this._temporaryBindEvent("select", this.showManipulatorToolbar.bind(this)); - _prototypeProperties(BezierEdgeDynamic, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; - this.setupSupportNode(); - }, - writable: true, - configurable: true - }, - cleanup: { - value: function cleanup() { - if (this.via !== undefined) { - delete this.body.nodes[this.via.id]; - this.via = undefined; - return true; - } - return false; + // redraw to show any possible changes + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - setupSupportNode: { + addNodeMode: { + /** - * 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. + * Create the toolbar for adding Nodes * - * The changed data is not called, if needed, it is returned by the main edge constructor. * @private */ - value: function setupSupportNode() { - if (this.via === undefined) { - var nodeId = "edgeId:" + this.id; - var node = this.body.functions.createNode({ - id: nodeId, - mass: 1, - shape: "circle", - image: "", - physics: true, - hidden: true - }); - this.body.nodes[nodeId] = node; - this.via = node; - this.via.parentEdgeId = this.id; - this.positionBezierNode(); - } - }, - writable: true, - configurable: true - }, - positionBezierNode: { - value: function positionBezierNode() { - if (this.via !== undefined && this.from !== undefined && this.to !== undefined) { - this.via.x = 0.5 * (this.from.x + this.to.x); - this.via.y = 0.5 * (this.from.y + this.to.y); - } else if (this.via !== undefined) { - this.via.x = 0; - this.via.y = 0; + value: function addNodeMode() { + // clear the toolbar + this._clean(); + + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale.addDescription); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); } + + this._temporaryBindEvent("click", this._performAddNode.bind(this)); }, writable: true, configurable: true }, - _line: { + editNode: { /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx + * call the bound function to handle the editing of the node. The node has to be selected. + * * @private */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y); - ctx.stroke(); - return this.via; + value: function editNode() { + var _this = this; + if (typeof this.options.handlerFunctions.editNode === "function") { + var node = this.selectionHandler._getSelectedNode(); + if (node.isCluster !== true) { + var data = util.deepExtend({}, node.options, true); + data.x = node.x; + data.y = node.y; + + if (this.options.handlerFunctions.editNode.length == 2) { + this.options.handlerFunctions.editNode(data, function (finalizedData) { + _this.body.data.nodes.update(finalizedData); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for edit does not support two arguments (data, callback)"); + } + } else { + alert(this.options.locales[this.options.locale].editClusterError); + } + } else { + throw new Error("No function has been configured to handle the editing of nodes."); + } }, writable: true, configurable: true }, - getPoint: { + addEdgeMode: { /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} + * create the toolbar to connect nodes + * * @private */ - value: function getPoint(percentage) { - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * this.via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * this.via.y + Math.pow(t, 2) * this.to.y; + value: function addEdgeMode() { + // _clean the system + this._clean(); - return { x: x, y: y }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - return this._findBorderPositionBezier(nearNode, ctx, this.via); + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale.edgeDescription); + + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } + + // temporarily overload functions + this._temporaryBindUI("onTouch", this._handleConnect.bind(this)); + this._temporaryBindUI("onDragEnd", this._finishConnect.bind(this)); + this._temporaryBindUI("onHold", function () {}); }, writable: true, configurable: true }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via); - }, - writable: true, - configurable: true - } - }); - - return BezierEdgeDynamic; - })(BezierEdgeBase); - - module.exports = BezierEdgeDynamic; - -/***/ }, -/* 96 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + editEdgeMode: { - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + /** + * create the toolbar to edit edges + * + * @private + */ + value: function editEdgeMode() { + // clear the system + this._clean(); - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + if (this.guiEnabled === true) { + var locale = this.options.locales[this.options.locale]; + this.manipulationDOM = {}; + this._createBackButton(locale); + this._createSeperator(); + this._createDescription(locale.editEdgeDescription); - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + // bind the close button + this._bindHammerToDiv(this.closeDiv, this.toggleEditMode.bind(this)); + } - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + this.edgeBeingEditedId = this.selectionHandler.getSelectedEdges()[0]; + var edge = this.body.edges[this.edgeBeingEditedId]; - /** - * Created by Alex on 3/20/2015. - */ + // create control nodes + var controlNodeFrom = this._getNewTargetNode(edge.from.x, edge.from.y); + var controlNodeTo = this._getNewTargetNode(edge.to.x, edge.to.y); - var BezierEdgeBase = _interopRequire(__webpack_require__(103)); + this.temporaryIds.nodes.push(controlNodeFrom.id); + this.temporaryIds.nodes.push(controlNodeTo.id); - var BezierEdgeStatic = (function (BezierEdgeBase) { - function BezierEdgeStatic(options, body, labelModule) { - _classCallCheck(this, BezierEdgeStatic); + this.body.nodes[controlNodeFrom.id] = controlNodeFrom; + this.body.nodeIndices.push(controlNodeFrom.id); + this.body.nodes[controlNodeTo.id] = controlNodeTo; + this.body.nodeIndices.push(controlNodeTo.id); - _get(Object.getPrototypeOf(BezierEdgeStatic.prototype), "constructor", this).call(this, options, body, labelModule); - } + // temporarily overload UI functions, cleaned up automatically because of _temporaryBindUI + this._temporaryBindUI("onTouch", this._controlNodeTouch.bind(this)); // used to get the position + this._temporaryBindUI("onTap", function () {}); // disabled + this._temporaryBindUI("onHold", function () {}); // disabled + this._temporaryBindUI("onDragStart", this._controlNodeDragStart.bind(this)); // used to select control node + this._temporaryBindUI("onDrag", this._controlNodeDrag.bind(this)); // used to drag control node + this._temporaryBindUI("onDragEnd", this._controlNodeDragEnd.bind(this)); // used to connect or revert control nodes + this._temporaryBindUI("onMouseMove", function () {}); // disabled - _inherits(BezierEdgeStatic, BezierEdgeBase); + // create function to position control nodes correctly on movement + // automatically cleaned up because we use the temporary bind + this._temporaryBindEvent("beforeDrawing", function (ctx) { + var positions = edge.edgeType.findBorderPositions(ctx); + if (controlNodeFrom.selected === false) { + controlNodeFrom.x = positions.from.x; + controlNodeFrom.y = positions.from.y; + } + if (controlNodeTo.selected === false) { + controlNodeTo.x = positions.to.x; + controlNodeTo.y = positions.to.y; + } + }); - _prototypeProperties(BezierEdgeStatic, null, { - cleanup: { - value: function cleanup() { - return false; + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - _line: { + deleteSelected: { + /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx + * delete everything in the selection + * * @private */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - var via = this._getViaCoordinates(); + value: function deleteSelected() { + var _this = this; + var selectedNodes = this.selectionHandler.getSelectedNodes(); + var selectedEdges = this.selectionHandler.getSelectedEdges(); + var deleteFunction = undefined; + if (selectedNodes.length > 0) { + for (var i = 0; i < selectedNodes.length; i++) { + if (this.body.nodes[selectedNodes[i]].isCluster === true) { + alert(this.options.locales[this.options.locale].deleteClusterError); + return; + } + } - // fallback to normal straight edges - if (via.x === undefined) { - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return undefined; + if (typeof this.options.handlerFunctions.deleteNode === "function") { + deleteFunction = this.options.handlerFunctions.deleteNode; + } + } else if (selectedEdges.length > 0) { + if (typeof this.options.handlerFunctions.deleteEdge === "function") { + deleteFunction = this.options.handlerFunctions.deleteEdge; + } + } + + if (typeof deleteFunction === "function") { + var data = { nodes: selectedNodes, edges: selectedEdges }; + if (deleteFunction.length == 2) { + deleteFunction(data, function (finalizedData) { + _this.body.data.edges.remove(finalizedData.edges); + _this.body.data.nodes.remove(finalizedData.nodes); + _this.body.emitter.emit("startSimulation"); + }); + } else { + throw new Error("The function for delete does not support two arguments (data, callback)"); + } } else { - ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y); - ctx.stroke(); - return via; + this.body.data.edges.remove(selectedEdges); + this.body.data.nodes.remove(selectedNodes); + this.body.emitter.emit("startSimulation"); } }, writable: true, configurable: true }, - _getViaCoordinates: { - value: function _getViaCoordinates() { - var xVia = undefined; - var yVia = undefined; - var factor = this.options.smooth.roundness; - var type = this.options.smooth.type; - var dx = Math.abs(this.from.x - this.to.x); - var dy = Math.abs(this.from.y - this.to.y); - if (type == "discrete" || type == "diagonalCross") { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y - factor * dy; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y - factor * dy; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dy; - yVia = this.from.y + factor * dy; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dy; - yVia = this.from.y + factor * dy; - } - } - if (type == "discrete") { - xVia = dx < factor * dy ? this.from.x : xVia; - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - if (this.from.y > this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y - factor * dx; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y - factor * dx; - } - } else if (this.from.y < this.to.y) { - if (this.from.x < this.to.x) { - xVia = this.from.x + factor * dx; - yVia = this.from.y + factor * dx; - } else if (this.from.x > this.to.x) { - xVia = this.from.x - factor * dx; - yVia = this.from.y + factor * dx; - } - } - if (type == "discrete") { - yVia = dy < factor * dx ? this.from.y : yVia; - } - } - } else if (type == "straightCross") { - if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { - // up - down - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } else { - yVia = this.to.y + (1 - factor) * dy; - } - } else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { - // left - right - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } - } else if (type == "horizontal") { - if (this.from.x < this.to.x) { - xVia = this.to.x - (1 - factor) * dx; - } else { - xVia = this.to.x + (1 - factor) * dx; - } - yVia = this.from.y; - } else if (type == "vertical") { - xVia = this.from.x; - if (this.from.y < this.to.y) { - yVia = this.to.y - (1 - factor) * dy; - } else { - yVia = this.to.y + (1 - factor) * dy; - } - } else if (type == "curvedCW") { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; + _setup: { - 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") { - dx = this.to.x - this.from.x; - dy = this.from.y - this.to.y; - var radius = Math.sqrt(dx * dx + dy * dy); - var pi = Math.PI; - var originalAngle = Math.atan2(dy, dx); - var myAngle = (originalAngle + (-factor * 0.5 + 0.5) * pi) % (2 * pi); - 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; - } - } + //********************************************** PRIVATE ***************************************// + + /** + * draw or remove the DOM + * @private + */ + value: function _setup() { + if (this.options.enabled === true) { + // Enable the GUI + this.guiEnabled = true; + + // remove override + this.selectionHandler.forceSelectEdges = true; + + this._createWrappers(); + if (this.editMode === false) { + this._createEditButton(); + } else { + this.showManipulatorToolbar(); } + } else { + this._removeManipulationDOM(); + + // disable the gui + this.guiEnabled = false; } - return { x: xVia, y: yVia }; - }, - writable: true, - configurable: true - }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - var options = arguments[2] === undefined ? {} : arguments[2]; - return this._findBorderPositionBezier(nearNode, ctx, options.via); }, writable: true, configurable: true }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - var via = arguments[6] === undefined ? this._getViaCoordinates() : arguments[6]; - // x3,y3 is the point - return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via); + _createWrappers: { + + + /** + * create the div overlays that contain the DOM + * @private + */ + value: function _createWrappers() { + // load the manipulator HTML elements. All styling done in css. + if (this.manipulationDiv === undefined) { + this.manipulationDiv = document.createElement("div"); + this.manipulationDiv.className = "network-manipulationDiv"; + if (this.editMode === true) { + this.manipulationDiv.style.display = "block"; + } else { + this.manipulationDiv.style.display = "none"; + } + this.canvas.frame.appendChild(this.manipulationDiv); + } + + // container for the edit button. + if (this.editModeDiv === undefined) { + this.editModeDiv = document.createElement("div"); + this.editModeDiv.className = "network-manipulation-editMode"; + if (this.editMode === true) { + this.editModeDiv.style.display = "none"; + } else { + this.editModeDiv.style.display = "block"; + } + this.canvas.frame.appendChild(this.editModeDiv); + } + + + // container for the close div button + if (this.closeDiv === undefined) { + this.closeDiv = document.createElement("div"); + this.closeDiv.className = "network-manipulation-closeDiv"; + this.closeDiv.style.display = this.manipulationDiv.style.display; + this.canvas.frame.appendChild(this.closeDiv); + } }, writable: true, configurable: true }, - getPoint: { + _getNewTargetNode: { + /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} + * generate a new target node. Used for creating new edges and editing edges + * @param x + * @param y + * @returns {*} * @private */ - value: function getPoint(percentage) { - var via = arguments[1] === undefined ? this._getViaCoordinates() : arguments[1]; - var t = percentage; - var x = Math.pow(1 - t, 2) * this.from.x + 2 * t * (1 - t) * via.x + Math.pow(t, 2) * this.to.x; - var y = Math.pow(1 - t, 2) * this.from.y + 2 * t * (1 - t) * via.y + Math.pow(t, 2) * this.to.y; + value: function _getNewTargetNode(x, y) { + var controlNodeStyle = util.deepExtend({}, this.options.controlNodeStyle); - return { x: x, y: y }; + controlNodeStyle.id = "targetNode" + util.randomUUID(); + controlNodeStyle.hidden = false; + controlNodeStyle.physics = false; + controlNodeStyle.x = x; + controlNodeStyle.y = y; + + return this.body.functions.createNode(controlNodeStyle); }, writable: true, configurable: true - } - }); + }, + _createEditButton: { - return BezierEdgeStatic; - })(BezierEdgeBase); - module.exports = BezierEdgeStatic; + /** + * Create the edit button + */ + value: function _createEditButton() { + // restore everything to it's original state (if applicable) + this._clean(); -/***/ }, -/* 97 */ -/***/ function(module, exports, __webpack_require__) { + // reset the manipulationDOM + this.manipulationDOM = {}; - "use strict"; + // empty the editModeDiv + util.recursiveDOMDelete(this.editModeDiv); - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; + // create the contents for the editMode button + var locale = this.options.locales[this.options.locale]; + var button = this._createButton("editMode", "network-manipulationUI edit editmode", locale.edit); + this.editModeDiv.appendChild(button); - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; + // bind a hammer listener to the button, calling the function toggleEditMode. + this._bindHammerToDiv(button, this.toggleEditMode.bind(this)); + }, + writable: true, + configurable: true + }, + _clean: { - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; + /** + * this function cleans up after everything this module does. Temporary elements, functions and events are removed, physics restored, hammers removed. + * @private + */ + value: function _clean() { + // _clean the divs + if (this.guiEnabled === true) { + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.manipulationDiv); - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; + // removes all the bindings and overloads + this._cleanManipulatorHammers(); + } - /** - * Created by Alex on 3/20/2015. - */ + // remove temporary nodes and edges + this._cleanupTemporaryNodesAndEdges(); - var EdgeBase = _interopRequire(__webpack_require__(104)); + // restore overloaded UI functions + this._unbindTemporaryUIs(); - var StraightEdge = (function (EdgeBase) { - function StraightEdge(options, body, labelModule) { - _classCallCheck(this, StraightEdge); + // remove the temporaryEventFunctions + this._unbindTemporaryEvents(); - _get(Object.getPrototypeOf(StraightEdge.prototype), "constructor", this).call(this, options, body, labelModule); - } + // restore the physics if required + this.body.emitter.emit("restorePhysics"); + }, + writable: true, + configurable: true + }, + _cleanManipulatorHammers: { - _inherits(StraightEdge, EdgeBase); - _prototypeProperties(StraightEdge, null, { - cleanup: { - value: function cleanup() { - return false; + /** + * Each dom element has it's own hammer. They are stored in this.manipulationHammers. This cleans them up. + * @private + */ + value: function _cleanManipulatorHammers() { + // _clean hammer bindings + if (this.manipulationHammers.length != 0) { + for (var i = 0; i < this.manipulationHammers.length; i++) { + this.manipulationHammers[i].destroy(); + } + this.manipulationHammers = []; + } }, writable: true, configurable: true }, - _line: { + _removeManipulationDOM: { + + /** - * Draw a line between two nodes - * @param {CanvasRenderingContext2D} ctx + * Remove all DOM elements created by this module. * @private */ - value: function _line(ctx) { - // draw a straight line - ctx.beginPath(); - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - ctx.stroke(); - return undefined; + value: function _removeManipulationDOM() { + // removes all the bindings and overloads + this._clean(); + + // empty the manipulation divs + util.recursiveDOMDelete(this.manipulationDiv); + util.recursiveDOMDelete(this.editModeDiv); + util.recursiveDOMDelete(this.closeDiv); + + // remove the manipulation divs + this.canvas.frame.removeChild(this.manipulationDiv); + this.canvas.frame.removeChild(this.editModeDiv); + this.canvas.frame.removeChild(this.closeDiv); + + // set the references to undefined + this.manipulationDiv = undefined; + this.editModeDiv = undefined; + this.closeDiv = undefined; + + // remove override + this.selectionHandler.forceSelectEdges = false; }, writable: true, configurable: true }, - getPoint: { + _createSeperator: { + /** - * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way - * @param percentage - * @param via - * @returns {{x: number, y: number}} + * create a seperator line. the index is to differentiate in the manipulation dom + * @param index * @private */ - value: function getPoint(percentage) { - return { - x: (1 - percentage) * this.from.x + percentage * this.to.x, - y: (1 - percentage) * this.from.y + percentage * this.to.y - }; + value: function _createSeperator() { + var index = arguments[0] === undefined ? 1 : arguments[0]; + this.manipulationDOM["seperatorLineDiv" + index] = document.createElement("div"); + this.manipulationDOM["seperatorLineDiv" + index].className = "network-seperatorLine"; + this.manipulationDiv.appendChild(this.manipulationDOM["seperatorLineDiv" + index]); }, writable: true, configurable: true }, - _findBorderPosition: { - value: function _findBorderPosition(nearNode, ctx) { - var node1 = this.to; - var node2 = this.from; - if (nearNode.id === this.from.id) { - node1 = this.from; - node2 = this.to; - } - - var angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - var dx = node1.x - node2.x; - var dy = node1.y - node2.y; - var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy); - var toBorderDist = nearNode.distanceToBorder(ctx, angle); - var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength; + _createAddNodeButton: { - var borderPos = {}; - borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x; - borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; + // ---------------------- DOM functions for buttons --------------------------// - return borderPos; + value: function _createAddNodeButton(locale) { + var button = this._createButton("addNode", "network-manipulationUI add", locale.addNode); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addNodeMode.bind(this)); }, writable: true, configurable: true }, - _getDistanceToEdge: { - value: function _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { - // x3,y3 is the point - return this._getDistanceToLine(x1, y1, x2, y2, x3, y3); + _createAddEdgeButton: { + value: function _createAddEdgeButton(locale) { + var button = this._createButton("addEdge", "network-manipulationUI connect", locale.addEdge); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.addEdgeMode.bind(this)); }, writable: true, configurable: true - } - }); - - return StraightEdge; - })(EdgeBase); - - module.exports = StraightEdge; - -/***/ }, -/* 98 */ -/***/ function(module, exports, __webpack_require__) { - - module.exports = function(module) { - if(!module.webpackPolyfill) { - module.deprecate = function() {}; - module.paths = []; - // module.parent = undefined by default - module.children = []; - module.webpackPolyfill = 1; - } - return module; - } - - -/***/ }, -/* 99 */ -/***/ function(module, exports, __webpack_require__) { - - /* WEBPACK VAR INJECTION */(function(__webpack_amd_options__) {module.exports = __webpack_amd_options__; - - /* WEBPACK VAR INJECTION */}.call(exports, {})) - -/***/ }, -/* 100 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - 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 3/19/2015. - */ - - var NodeBase = (function () { - function NodeBase(options, body, labelModule) { - _classCallCheck(this, NodeBase); - - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.top = undefined; - this.left = undefined; - this.height = undefined; - this.boundingBox = { top: 0, left: 0, right: 0, bottom: 0 }; - } - - _prototypeProperties(NodeBase, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; + }, + _createEditNodeButton: { + value: function _createEditNodeButton(locale) { + var button = this._createButton("editNode", "network-manipulationUI edit", locale.editNode); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editNode.bind(this)); }, writable: true, configurable: true }, - _distanceToBorder: { - value: function _distanceToBorder(angle) { - var borderWidth = 1; - return Math.min(Math.abs(this.width / 2 / Math.cos(angle)), Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth; + _createEditEdgeButton: { + value: function _createEditEdgeButton(locale) { + var button = this._createButton("editEdge", "network-manipulationUI edit", locale.editEdge); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.editEdgeMode.bind(this)); }, writable: true, configurable: true - } - }); - - return NodeBase; - })(); - - module.exports = NodeBase; - -/***/ }, -/* 101 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - /** - * Created by Alex on 3/19/2015. - */ - var NodeBase = _interopRequire(__webpack_require__(100)); - - var CircleImageBase = (function (NodeBase) { - function CircleImageBase(options, body, labelModule) { - _classCallCheck(this, CircleImageBase); - - _get(Object.getPrototypeOf(CircleImageBase.prototype), "constructor", this).call(this, options, body, labelModule); - } - - _inherits(CircleImageBase, NodeBase); - - _prototypeProperties(CircleImageBase, null, { - _drawRawCircle: { - value: function _drawRawCircle(ctx, x, y, selected, hover, size) { - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth *= this.networkScaleInv; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx.circle(x, y, size); - ctx.fill(); - ctx.stroke(); + }, + _createDeleteButton: { + value: function _createDeleteButton(locale) { + var button = this._createButton("delete", "network-manipulationUI delete", locale.del); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.deleteSelected.bind(this)); }, writable: true, configurable: true }, - _drawImageAtPosition: { - value: function _drawImageAtPosition(ctx) { - if (this.imageObj.width != 0) { - // draw the image - ctx.globalAlpha = 1; - ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); - } + _createBackButton: { + value: function _createBackButton(locale) { + var button = this._createButton("back", "network-manipulationUI back", locale.back); + this.manipulationDiv.appendChild(button); + this._bindHammerToDiv(button, this.showManipulatorToolbar.bind(this)); }, writable: true, configurable: true }, - _drawImageLabel: { - value: function _drawImageLabel(ctx, x, y, selected) { - var yLabel; - var offset = 0; - - if (this.height !== undefined) { - offset = this.height * 0.5; - var labelDimensions = this.labelModule.getTextSize(ctx); - - if (labelDimensions.lineCount >= 1) { - offset += labelDimensions.height / 2; - offset += 3; - } - } - - yLabel = y + offset; - this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); + _createButton: { + value: function _createButton(id, className, label) { + var labelClassName = arguments[3] === undefined ? "network-manipulationLabel" : arguments[3]; + this.manipulationDOM[id + "Div"] = document.createElement("div"); + this.manipulationDOM[id + "Div"].className = className; + this.manipulationDOM[id + "Label"] = document.createElement("div"); + this.manipulationDOM[id + "Label"].className = labelClassName; + this.manipulationDOM[id + "Label"].innerHTML = label; + this.manipulationDOM[id + "Div"].appendChild(this.manipulationDOM[id + "Label"]); + return this.manipulationDOM[id + "Div"]; }, writable: true, configurable: true - } - }); - - return CircleImageBase; - })(NodeBase); - - module.exports = CircleImageBase; - -/***/ }, -/* 102 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - /** - * Created by Alex on 3/19/2015. - */ - var NodeBase = _interopRequire(__webpack_require__(100)); - - var ShapeBase = (function (NodeBase) { - function ShapeBase(options, body, labelModule) { - _classCallCheck(this, ShapeBase); + }, + _createDescription: { + value: function _createDescription(label) { + this.manipulationDiv.appendChild(this._createButton("description", "network-manipulationUI none", label)); + }, + writable: true, + configurable: true + }, + _temporaryBindEvent: { - _get(Object.getPrototypeOf(ShapeBase.prototype), "constructor", this).call(this, options, body, labelModule); - } + // -------------------------- End of DOM functions for buttons ------------------------------// - _inherits(ShapeBase, NodeBase); + /** + * this binds an event until cleanup by the clean functions. + * @param event + * @param newFunction + * @private + */ + value: function _temporaryBindEvent(event, newFunction) { + this.temporaryEventFunctions.push({ event: event, boundFunction: newFunction }); + this.body.emitter.on(event, newFunction); + }, + writable: true, + configurable: true + }, + _temporaryBindUI: { - _prototypeProperties(ShapeBase, null, { - _resizeShape: { - value: function _resizeShape() { - if (this.width === undefined) { - var size = 2 * this.options.size; - this.width = size; - this.height = size; + /** + * this overrides an UI function until cleanup by the clean function + * @param UIfunctionName + * @param newFunction + * @private + */ + value: function _temporaryBindUI(UIfunctionName, newFunction) { + if (this.body.eventListeners[UIfunctionName] !== undefined) { + this.temporaryUIFunctions[UIfunctionName] = this.body.eventListeners[UIfunctionName]; + this.body.eventListeners[UIfunctionName] = newFunction; + } else { + throw new Error("This UI function does not exist. Typo? You tried: \"" + UIfunctionName + "\" possible are: " + JSON.stringify(Object.keys(this.body.eventListeners))); } }, writable: true, configurable: true }, - _drawShape: { - value: function _drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover) { - this._resizeShape(); - - this.left = x - this.width / 2; - this.top = y - this.height / 2; - - var borderWidth = this.options.borderWidth; - var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth; - - ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border; - ctx.lineWidth = selected ? selectionLineWidth : borderWidth; - ctx.lineWidth /= this.body.view.scale; - ctx.lineWidth = Math.min(this.width, ctx.lineWidth); - ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background; - ctx[shape](x, y, this.options.size); - ctx.fill(); - ctx.stroke(); - - this.boundingBox.top = y - this.options.size; - this.boundingBox.left = x - this.options.size; - this.boundingBox.right = x + this.options.size; - this.boundingBox.bottom = y + this.options.size; + _unbindTemporaryUIs: { - if (this.options.label !== undefined) { - var yLabel = y + 0.5 * this.height + 3; // the + 3 is to offset it a bit below the node. - this.labelModule.draw(ctx, x, yLabel, selected, "hanging"); - this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left); - this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width); - this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height); + /** + * Restore the overridden UI functions to their original state. + * + * @private + */ + value: function _unbindTemporaryUIs() { + for (var functionName in this.temporaryUIFunctions) { + if (this.temporaryUIFunctions.hasOwnProperty(functionName)) { + this.body.eventListeners[functionName] = this.temporaryUIFunctions[functionName]; + delete this.temporaryUIFunctions[functionName]; + } } + this.temporaryUIFunctions = {}; }, writable: true, configurable: true - } - }); - - return ShapeBase; - })(NodeBase); - - module.exports = ShapeBase; - -/***/ }, -/* 103 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _get = function get(object, property, receiver) { var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc && desc.writable) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - - var _inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - /** - * Created by Alex on 3/20/2015. - */ - - var EdgeBase = _interopRequire(__webpack_require__(104)); - - var BezierEdgeBase = (function (EdgeBase) { - function BezierEdgeBase(options, body, labelModule) { - _classCallCheck(this, BezierEdgeBase); - - _get(Object.getPrototypeOf(BezierEdgeBase.prototype), "constructor", this).call(this, options, body, labelModule); - } - - _inherits(BezierEdgeBase, EdgeBase); - - _prototypeProperties(BezierEdgeBase, null, { - _findBorderPositionBezier: { + }, + _unbindTemporaryEvents: { /** - * This function uses binary search to look for the point where the bezier curve crosses the border of the node. - * - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode - * @param nearNode - * @param ctx - * @param viaNode + * Unbind the events created by _temporaryBindEvent + * @private */ - value: function _findBorderPositionBezier(nearNode, ctx) { - var viaNode = arguments[2] === undefined ? this._getViaCoordinates() : arguments[2]; - var maxIterations = 10; - var iteration = 0; - var low = 0; - var high = 1; - var pos, angle, distanceToBorder, distanceToPoint, difference; - var threshold = 0.2; - var node = this.to; - var from = false; - if (nearNode.id === this.from.id) { - node = this.from; - from = true; - } - - while (low <= high && iteration < maxIterations) { - var middle = (low + high) * 0.5; - - pos = this.getPoint(middle, viaNode); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - if (Math.abs(difference) < threshold) { - break; // found - } else if (difference < 0) { - // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (from == false) { - low = middle; - } else { - high = middle; - } - } else { - if (from == false) { - high = middle; - } else { - low = middle; - } - } - - iteration++; + value: function _unbindTemporaryEvents() { + for (var i = 0; i < this.temporaryEventFunctions.length; i++) { + var eventName = this.temporaryEventFunctions[i].event; + var boundFunction = this.temporaryEventFunctions[i].boundFunction; + this.body.emitter.off(eventName, boundFunction); } - pos.t = middle; - - return pos; + this.temporaryEventFunctions = []; }, writable: true, configurable: true }, - _getDistanceToBezierEdge: { + _bindHammerToDiv: { + /** + * Bind an hammer instance to a DOM element. + * @param domElement + * @param funct + */ + value: function _bindHammerToDiv(domElement, boundFunction) { + var hammer = new Hammer(domElement, {}); + hammerUtil.onTouch(hammer, boundFunction); + this.manipulationHammers.push(hammer); + }, + writable: true, + configurable: true + }, + _cleanupTemporaryNodesAndEdges: { /** - * 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 + * Neatly clean up temporary edges and nodes * @private */ - value: function _getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var xVia = undefined, - yVia = undefined; - xVia = via.x; - yVia = via.y; - var minDistance = 1000000000; - var distance = undefined; - var i = undefined, - t = undefined, - x = undefined, - y = undefined; - var lastX = x1; - var lastY = y1; - for (i = 1; i < 10; i++) { - t = 0.1 * i; - x = Math.pow(1 - t, 2) * x1 + 2 * t * (1 - t) * xVia + Math.pow(t, 2) * x2; - y = Math.pow(1 - t, 2) * y1 + 2 * t * (1 - t) * yVia + Math.pow(t, 2) * y2; - if (i > 0) { - distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3); - minDistance = distance < minDistance ? distance : minDistance; + value: function _cleanupTemporaryNodesAndEdges() { + // _clean temporary edges + for (var i = 0; i < this.temporaryIds.edges.length; i++) { + this.body.edges[this.temporaryIds.edges[i]].disconnect(); + delete this.body.edges[this.temporaryIds.edges[i]]; + var indexTempEdge = this.body.edgeIndices.indexOf(this.temporaryIds.edges[i]); + if (indexTempEdge !== -1) { + this.body.edgeIndices.splice(indexTempEdge, 1); } - lastX = x; - lastY = y; } - return minDistance; + // _clean temporary nodes + for (var i = 0; i < this.temporaryIds.nodes.length; i++) { + delete this.body.nodes[this.temporaryIds.nodes[i]]; + var indexTempNode = this.body.nodeIndices.indexOf(this.temporaryIds.nodes[i]); + if (indexTempNode !== -1) { + this.body.nodeIndices.splice(indexTempNode, 1); + } + } + + this.temporaryIds = { nodes: [], edges: [] }; }, writable: true, configurable: true - } - }); - - return BezierEdgeBase; - })(EdgeBase); - - module.exports = BezierEdgeBase; - -/***/ }, -/* 104 */ -/***/ function(module, exports, __webpack_require__) { - - "use strict"; - - var _slicedToArray = function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { var _arr = []; for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { _arr.push(_step.value); if (i && _arr.length === i) break; } return _arr; } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; - - var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); }; - - var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; - - /** - * Created by Alex on 3/20/2015. - */ - var util = __webpack_require__(1); - - var EdgeBase = (function () { - function EdgeBase(options, body, labelModule) { - _classCallCheck(this, EdgeBase); + }, + _controlNodeTouch: { - this.body = body; - this.labelModule = labelModule; - this.setOptions(options); - this.colorDirty = true; - } + // ------------------------------------------ EDIT EDGE FUNCTIONS -----------------------------------------// - _prototypeProperties(EdgeBase, null, { - setOptions: { - value: function setOptions(options) { - this.options = options; - this.from = this.body.nodes[this.options.from]; - this.to = this.body.nodes[this.options.to]; - this.id = this.options.id; + /** + * the touch is used to get the position of the initial click + * @param event + * @private + */ + value: function _controlNodeTouch(event) { + this.lastTouch = this.body.functions.getPointer(event.center); + this.lastTouch.translation = util.extend({}, this.body.view.translation); // copy the object }, writable: true, configurable: true }, - drawLine: { + _controlNodeDragStart: { + /** - * 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 + * the drag start is used to mark one of the control nodes as selected. + * @param event * @private */ - value: function drawLine(ctx, selected, hover) { - // set style - ctx.strokeStyle = this.getColor(ctx); - ctx.lineWidth = this.getLineWidth(selected, hover); - var via = undefined; - if (this.from != this.to) { - // draw line - if (this.options.dashes.enabled == true) { - via = this._drawDashedLine(ctx); - } else { - via = this._line(ctx); - } - } else { - var _getCircleData = this._getCircleData(); + value: function _controlNodeDragStart(event) { + var pointer = this.lastTouch; + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + var to = this.body.nodes[this.temporaryIds.nodes[1]]; + var edge = this.body.edges[this.edgeBeingEditedId]; + this.selectedControlNode = undefined; - var _getCircleData2 = _slicedToArray(_getCircleData, 3); + var fromSelect = from.isOverlappingWith(pointerObj); + var toSelect = to.isOverlappingWith(pointerObj); - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; - this._circle(ctx, x, y, radius); + if (fromSelect === true) { + this.selectedControlNode = from; + edge.edgeType.from = from; + } else if (toSelect === true) { + this.selectedControlNode = to; + edge.edgeType.to = to; } - return via; + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - _drawDashedLine: { - value: function _drawDashedLine(ctx) { - var via = undefined; - // only firefox and chrome support this method, else we use the legacy one. - if (ctx.setLineDash !== undefined) { - ctx.save(); - // configure the dash pattern - var pattern = [0]; - if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) { - pattern = [this.options.dashes.length, this.options.dashes.gap]; - } else { - pattern = [5, 5]; - } - - // set dash settings for chrome or firefox - ctx.setLineDash(pattern); - ctx.lineDashOffset = 0; + _controlNodeDrag: { - // draw the line - via = this._line(ctx); + /** + * dragging the control nodes or the canvas + * @param event + * @private + */ + value: function _controlNodeDrag(event) { + this.body.emitter.emit("disablePhysics"); + var pointer = this.body.functions.getPointer(event.center); + var pos = this.canvas.DOMtoCanvas(pointer); - // restore the dash settings. - ctx.setLineDash([0]); - ctx.lineDashOffset = 0; - ctx.restore(); - } else { - // unsupporting smooth lines - // draw dashes line - ctx.beginPath(); - ctx.lineCap = "round"; - if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value - { - ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]); - } else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value - { - ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, [this.options.dashes.length, this.options.dashes.gap]); - } else //If all else fails draw a line - { - ctx.moveTo(this.from.x, this.from.y); - ctx.lineTo(this.to.x, this.to.y); - } - ctx.stroke(); - } - return via; - }, - writable: true, - configurable: true - }, - findBorderPosition: { - value: function findBorderPosition(nearNode, ctx, options) { - if (this.from != this.to) { - return this._findBorderPosition(nearNode, ctx, options); + if (this.selectedControlNode !== undefined) { + this.selectedControlNode.x = pos.x; + this.selectedControlNode.y = pos.y; } else { - return this._findBorderPositionCircle(nearNode, ctx, options); + // if the drag was not started properly because the click started outside the network div, start it now. + var diffX = pointer.x - this.lastTouch.x; + var diffY = pointer.y - this.lastTouch.y; + this.body.view.translation = { x: this.lastTouch.translation.x + diffX, y: this.lastTouch.translation.y + diffY }; } + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - findBorderPositions: { - value: function findBorderPositions(ctx) { - var from = {}; - var to = {}; - if (this.from != this.to) { - from = this._findBorderPosition(this.from, ctx); - to = this._findBorderPosition(this.to, ctx); - } else { - var _getCircleData = this._getCircleData(); - - var _getCircleData2 = _slicedToArray(_getCircleData, 3); + _controlNodeDragEnd: { - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; + /** + * connecting or restoring the control nodes. + * @param event + * @private + */ + value: function _controlNodeDragEnd(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); + var edge = this.body.edges[this.edgeBeingEditedId]; - from = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); - to = this._findBorderPositionCircle(this.from, ctx, { x: x, y: y, low: 0.6, high: 0.8, direction: 1 }); + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + if (overlappingNodeIds[i] !== this.selectedControlNode.id) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; + } } - return { from: from, to: to }; - }, - writable: true, - configurable: true - }, - _getCircleData: { - value: function _getCircleData() { - var x = undefined, - y = undefined; - var node = this.from; - var radius = this.options.selfReferenceSize; - // get circle coordinates - if (node.shape.width > node.shape.height) { - x = node.x + node.shape.width * 0.5; - y = node.y - radius; + // perform the connection + if (node !== undefined && this.selectedControlNode !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale].createEdgeError); + } else { + var from = this.body.nodes[this.temporaryIds.nodes[0]]; + if (this.selectedControlNode.id == from.id) { + this._performEditEdge(node.id, edge.to.id); + } else { + this._performEditEdge(edge.from.id, node.id); + } + } } else { - x = node.x + radius; - y = node.y - node.shape.height * 0.5; + edge.updateEdgeType(); + this.body.emitter.emit("restorePhysics"); } - return [x, y, radius]; + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - _pointOnCircle: { + _handleConnect: { + // ------------------------------------ END OF EDIT EDGE FUNCTIONS -----------------------------------------// + + + + // ------------------------------------------- ADD EDGE FUNCTIONS -----------------------------------------// /** - * 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 + * the function bound to the selection event. It checks if you want to connect a cluster and changes the description + * to walk the user through the process. + * * @private */ - value: function _pointOnCircle(x, y, radius, percentage) { - var angle = percentage * 2 * Math.PI; - return { - x: x + radius * Math.cos(angle), - y: y - radius * Math.sin(angle) - }; + value: function _handleConnect(event) { + var _this = this; + // check to avoid double fireing of this function. + if (new Date().valueOf() - this.touchTime > 100) { + var pointer = this.body.functions.getPointer(event.center); + var node = this.selectionHandler.getNodeAt(pointer); + + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale].createEdgeError); + } else { + (function () { + // create a node the temporary line can look at + var targetNode = _this._getNewTargetNode(node.x, node.y); + var targetNodeId = targetNode.id; + _this.body.nodes[targetNode.id] = targetNode; + _this.body.nodeIndices.push(targetNode.id); + + // create a temporary edge + var connectionEdge = _this.body.functions.createEdge({ + id: "connectionEdge" + util.randomUUID(), + from: node.id, + to: targetNode.id, + physics: false, + smooth: { + enabled: true, + dynamic: false, + type: "continuous", + roundness: 0.5 + } + }); + _this.body.edges[connectionEdge.id] = connectionEdge; + _this.body.edgeIndices.push(connectionEdge.id); + + _this.temporaryIds.nodes.push(targetNode.id); + _this.temporaryIds.edges.push(connectionEdge.id); + + _this.temporaryUIFunctions.onDrag = _this.body.eventListeners.onDrag; + _this.body.eventListeners.onDrag = function (event) { + var pointer = _this.body.functions.getPointer(event.center); + var targetNode = _this.body.nodes[targetNodeId]; + targetNode.x = _this.canvas._XconvertDOMtoCanvas(pointer.x); + targetNode.y = _this.canvas._YconvertDOMtoCanvas(pointer.y); + _this.body.emitter.emit("_redraw"); + }; + })(); + } + } + this.touchTime = new Date().valueOf(); + + // do the original touch events + this.temporaryUIFunctions.onTouch(event); + } }, writable: true, configurable: true }, - _findBorderPositionCircle: { + _finishConnect: { /** - * This function uses binary search to look for the point where the circle crosses the border of the node. - * @param node - * @param ctx - * @param options - * @returns {*} + * Connect the new edge to the target if one exists, otherwise remove temp line + * @param event * @private */ - value: function _findBorderPositionCircle(node, ctx, options) { - var x = options.x; - var y = options.y; - var low = options.low; - var high = options.high; - var direction = options.direction; + value: function _finishConnect(event) { + var pointer = this.body.functions.getPointer(event.center); + var pointerObj = this.selectionHandler._pointerToPositionObject(pointer); - var maxIterations = 10; - var iteration = 0; - var radius = this.options.selfReferenceSize; - var pos = undefined, - angle = undefined, - distanceToBorder = undefined, - distanceToPoint = undefined, - difference = undefined; - var threshold = 0.05; - var middle = (low + high) * 0.5; + // remember the edge id + var connectFromId = undefined; + if (this.temporaryIds.edges[0] !== undefined) { + connectFromId = this.body.edges[this.temporaryIds.edges[0]].fromId; + } - while (low <= high && iteration < maxIterations) { - middle = (low + high) * 0.5; + //restore the drag function + if (this.temporaryUIFunctions.onDrag !== undefined) { + this.body.eventListeners.onDrag = this.temporaryUIFunctions.onDrag; + delete this.temporaryUIFunctions.onDrag; + } - pos = this._pointOnCircle(x, y, radius, middle); - angle = Math.atan2(node.y - pos.y, node.x - pos.x); - distanceToBorder = node.distanceToBorder(ctx, angle); - distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); - difference = distanceToBorder - distanceToPoint; - if (Math.abs(difference) < threshold) { - break; // found - } else if (difference > 0) { - // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. - if (direction > 0) { - low = middle; - } else { - high = middle; - } - } else { - if (direction > 0) { - high = middle; - } else { - low = middle; - } + // get the overlapping node but NOT the temporary node; + var overlappingNodeIds = this.selectionHandler._getAllNodesOverlappingWith(pointerObj); + var node = undefined; + for (var i = overlappingNodeIds.length - 1; i >= 0; i--) { + if (this.temporaryIds.nodes.indexOf(overlappingNodeIds[i]) !== -1) { + node = this.body.nodes[overlappingNodeIds[i]]; + break; } - iteration++; } - pos.t = middle; - return pos; - }, - writable: true, - configurable: true - }, - getLineWidth: { + // clean temporary nodes and edges. + this._cleanupTemporaryNodesAndEdges(); - /** - * Get the line width of the edge. Depends on width and whether one of the - * connected nodes is selected. - * @return {Number} width - * @private - */ - value: function getLineWidth(selected, hover) { - if (selected == true) { - return Math.max(Math.min(this.options.widthSelectionMultiplier * this.options.width, this.options.scaling.max), 0.3 / this.body.view.scale); - } else { - if (hover == true) { - return Math.max(Math.min(this.options.hoverWidth, this.options.scaling.max), 0.3 / this.body.view.scale); + // perform the connection + if (node !== undefined) { + if (node.isCluster === true) { + alert(this.options.locales[this.options.locale].createEdgeError); } else { - return Math.max(this.options.width, 0.3 / this.body.view.scale); + if (this.body.nodes[connectFromId] !== undefined && this.body.nodes[node.id] !== undefined) { + this._performCreateEdge(connectFromId, node.id); + } } } + this.body.emitter.emit("_redraw"); }, writable: true, configurable: true }, - getColor: { - value: function getColor(ctx) { - var colorObj = this.options.color; - - if (colorObj.inherit.enabled === true) { - if (colorObj.inherit.useGradients == true) { - var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y); - var fromColor, toColor; - fromColor = this.from.options.color.highlight.border; - toColor = this.to.options.color.highlight.border; + _performAddNode: { - if (this.from.selected == false && this.to.selected == false) { - fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); - toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); - } else if (this.from.selected == true && this.to.selected == false) { - toColor = this.to.options.color.border; - } else if (this.from.selected == false && this.to.selected == true) { - fromColor = this.from.options.color.border; - } - grd.addColorStop(0, fromColor); - grd.addColorStop(1, toColor); + // --------------------------------------- END OF ADD EDGE FUNCTIONS -------------------------------------// - // -------------------- this returns -------------------- // - return grd; - } - if (this.colorDirty === true) { - if (colorObj.inherit.source == "to") { - colorObj.highlight = this.to.options.color.highlight.border; - colorObj.hover = this.to.options.color.hover.border; - colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); - } else { - // (this.options.color.inherit.source == "from") { - colorObj.highlight = this.from.options.color.highlight.border; - colorObj.hover = this.from.options.color.hover.border; - colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); - } - } - } + // ------------------------------ Performing all the actual data manipulation ------------------------// - // if color inherit is on and gradients are used, the function has already returned by now. - this.colorDirty = false; + /** + * Adds a node on the specified location + */ + value: function _performAddNode(clickData) { + var _this = this; + var defaultData = { + id: util.randomUUID(), + x: clickData.pointer.canvas.x, + y: clickData.pointer.canvas.y, + label: "new" + }; - if (this.selected == true) { - return colorObj.highlight; - } else if (this.hover == true) { - return colorObj.hover; + if (typeof this.options.handlerFunctions.addNode === "function") { + if (this.options.handlerFunctions.addNode.length == 2) { + this.options.handlerFunctions.addNode(defaultData, function (finalizedData) { + _this.body.data.nodes.add(finalizedData); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for add does not support two arguments (data,callback)"); + this.showManipulatorToolbar(); + } } else { - return colorObj.color; + this.body.data.nodes.add(defaultData); + this.showManipulatorToolbar(); } }, writable: true, configurable: true }, - _circle: { + _performCreateEdge: { + /** - * Draw a line from a node to itself, a circle - * @param {CanvasRenderingContext2D} ctx - * @param {Number} x - * @param {Number} y - * @param {Number} radius + * connect two nodes with a new edge. + * * @private */ - value: function _circle(ctx, x, y, radius) { - // draw a circle - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.stroke(); + value: function _performCreateEdge(sourceNodeId, targetNodeId) { + var _this = this; + var defaultData = { from: sourceNodeId, to: targetNodeId }; + if (this.options.handlerFunctions.addEdge) { + if (this.options.handlerFunctions.addEdge.length == 2) { + this.options.handlerFunctions.addEdge(defaultData, function (finalizedData) { + _this.body.data.edges.add(finalizedData); + _this.selectionHandler.unselectAll(); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for connect does not support two arguments (data,callback)"); + } + } else { + this.body.data.edges.add(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); + } }, writable: true, configurable: true }, - getDistanceToEdge: { - + _performEditEdge: { /** - * 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 + * connect two nodes with a new edge. + * * @private */ - value: function getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { - // x3,y3 is the point - var returnValue = 0; - if (this.from != this.to) { - returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via); - } else { - var _getCircleData = this._getCircleData(); - - var _getCircleData2 = _slicedToArray(_getCircleData, 3); - - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; - var dx = x - x3; - var dy = y - y3; - returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius); - } - - if (this.labelModule.size.left < x3 && this.labelModule.size.left + this.labelModule.size.width > x3 && this.labelModule.size.top < y3 && this.labelModule.size.top + this.labelModule.size.height > y3) { - return 0; + value: function _performEditEdge(sourceNodeId, targetNodeId) { + var _this = this; + var defaultData = { id: this.edgeBeingEditedId, from: sourceNodeId, to: targetNodeId }; + if (this.options.handlerFunctions.editEdge) { + if (this.options.handlerFunctions.editEdge.length == 2) { + this.options.handlerFunctions.editEdge(defaultData, function (finalizedData) { + _this.body.data.edges.update(finalizedData); + _this.selectionHandler.unselectAll(); + _this.showManipulatorToolbar(); + }); + } else { + throw new Error("The function for edit does not support two arguments (data, callback)"); + } } else { - return returnValue; + this.body.data.edges.update(defaultData); + this.selectionHandler.unselectAll(); + this.showManipulatorToolbar(); } }, writable: true, configurable: true - }, - _getDistanceToLine: { - value: function _getDistanceToLine(x1, y1, x2, y2, x3, y3) { - var px = x2 - x1; - var py = y2 - y1; - var something = px * px + py * py; - var u = ((x3 - x1) * px + (y3 - y1) * py) / something; + } + }); - if (u > 1) { - u = 1; - } else if (u < 0) { - u = 0; - } + return ManipulationSystem; + })(); - var x = x1 + u * px; - var y = y1 + u * py; - var dx = x - x3; - var dy = y - y3; + module.exports = ManipulationSystem; - //# 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 +/***/ }, +/* 104 */ +/***/ function(module, exports, __webpack_require__) { - return Math.sqrt(dx * dx + dy * dy); - }, - writable: true, - configurable: true - }, - drawArrowHead: { + "use strict"; - /** - * - * @param ctx - * @param position - * @param viaNode - */ - value: function drawArrowHead(ctx, position, viaNode, selected, hover) { - // set style - ctx.strokeStyle = this.getColor(ctx); - ctx.fillStyle = ctx.strokeStyle; - ctx.lineWidth = this.getLineWidth(selected, hover); + // English + exports.en = { + edit: "Edit", + del: "Delete selected", + back: "Back", + addNode: "Add Node", + addEdge: "Add Edge", + editNode: "Edit Node", + editEdge: "Edit Edge", + addDescription: "Click in an empty space to place a new node.", + edgeDescription: "Click on a node and drag the edge to another node to connect them.", + editEdgeDescription: "Click on the control points and drag them to a node to connect to it.", + createEdgeError: "Cannot link edges to a cluster.", + deleteClusterError: "Clusters cannot be deleted.", + editClusterError: "Clusters cannot be edited." + }; + exports.en_EN = exports.en; + exports.en_US = exports.en; - // set lets - var angle = undefined; - var length = undefined; - var arrowPos = undefined; - var node1 = undefined; - var node2 = undefined; - var guideOffset = undefined; - var scaleFactor = undefined; + // Dutch + exports.nl = { + edit: "Wijzigen", + del: "Selectie verwijderen", + back: "Terug", + addNode: "Node toevoegen", + addEdge: "Link toevoegen", + editNode: "Node wijzigen", + editEdge: "Link wijzigen", + addDescription: "Klik op een leeg gebied om een nieuwe node te maken.", + edgeDescription: "Klik op een node en sleep de link naar een andere node om ze te verbinden.", + editEdgeDescription: "Klik op de verbindingspunten en sleep ze naar een node om daarmee te verbinden.", + createEdgeError: "Kan geen link maken naar een cluster.", + deleteClusterError: "Clusters kunnen niet worden verwijderd.", + editClusterError: "Clusters kunnen niet worden aangepast." + }; + exports.nl_NL = exports.nl; + exports.nl_BE = exports.nl; - if (position == "from") { - node1 = this.from; - node2 = this.to; - guideOffset = 0.1; - scaleFactor = this.options.arrows.from.scaleFactor; - } else if (position == "to") { - node1 = this.to; - node2 = this.from; - guideOffset = -0.1; - scaleFactor = this.options.arrows.to.scaleFactor; - } else { - node1 = this.to; - node2 = this.from; - scaleFactor = this.options.arrows.middle.scaleFactor; +/***/ }, +/* 105 */ +/***/ 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 3/26/2015. + */ + + + var util = __webpack_require__(1); + + + var ConfigurationSystem = (function () { + function ConfigurationSystem(network) { + _classCallCheck(this, ConfigurationSystem); + + this.network = network; + + this.possibleOptions = { + nodesHandler: { + borderWidth: 1, + borderWidthSelected: 2, + color: { + border: "color", + background: "color", + highlight: { + border: "color", + background: "#D2E5FF" + }, + hover: { + border: "color", + background: "color" + } + }, + fixed: { + x: false, + y: false + }, + font: { + color: "color", + size: [14, 0, 100, 1], // px + face: ["arial", "verdana", "tahoma"], + background: "color", + stroke: [0, 0, 50, 1], // px + strokeColor: "color" + }, + group: "string", + hidden: false, + icon: { + face: "string", //'FontAwesome', + code: "string", //'\uf007', + size: [50, 0, 200, 1], //50, + color: "color" //'#aa00ff' + }, + image: "string", // --> URL + physics: true, + scaling: { + min: [10, 0, 200, 1], + max: [30, 0, 200, 1], + label: { + enabled: true, + min: [14, 0, 200, 1], + max: [30, 0, 200, 1], + maxVisible: [30, 0, 200, 1], + drawThreshold: [3, 0, 20, 1] + } + }, + shape: ["ellipse", "box", "circle", "circularImage", "database", "diamond", "dot", "icon", "image", "square", "star", "text", "triangle", "triangleDown"], + size: [25, 0, 200, 1] + }, + edgesHandler: { + arrows: { + to: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: { enabled: false, scaleFactor: [1, 0, 3, 0.05] }, + from: { enabled: false, scaleFactor: [1, 0, 3, 0.05] } + }, + color: { + color: "color", + highlight: "color", + hover: "color", + inherit: { + enabled: true, + source: ["from", "to"], // from / to + useGradients: false + }, + opacity: [1, 0, 1, 0.05] + }, + dashes: { + enabled: false, + length: [5, 0, 50, 1], + gap: [5, 0, 50, 1], + altLength: [5, 0, 50, 1] + }, + font: { + color: "color", + size: [14, 0, 100, 1], // px + face: ["arial", "verdana", "tahoma"], + background: "color", + stroke: [0, 0, 50, 1], // px + strokeColor: "color", + align: ["horizontal", "top", "middle", "bottom"] + }, + hidden: false, + hoverWidth: [1.5, 0, 10, 0.1], + physics: true, + scaling: { + min: [10, 0, 200, 1], + max: [30, 0, 200, 1], + label: { + enabled: true, + min: [14, 0, 200, 1], + max: [30, 0, 200, 1], + maxVisible: [30, 0, 200, 1], + drawThreshold: [3, 0, 20, 1] + } + }, + selfReferenceSize: [20, 0, 200, 1], + smooth: { + enabled: true, + dynamic: true, + type: ["continuous", "discrete", "diagonalCross", "straightCross", "horizontal", "vertical", "curvedCW", "curvedCCW"], + roundness: [0.5, 0, 1, 0.05] + }, + width: [1, 0, 30, 1], + widthSelectionMultiplier: [2, 0, 5, 0.1] + }, + layout: { + randomSeed: [0, 0, 500, 1], + hierarchical: { + enabled: false, + levelSeparation: [150, 20, 500, 5], + direction: ["UD", "DU", "LR", "RL"], // UD, DU, LR, RL + sortMethod: ["hubsize", "directed"] // hubsize, directed + } + }, + interaction: { + dragNodes: true, + dragView: true, + zoomView: true, + hoverEnabled: false, + showNavigationIcons: false, + tooltip: { + delay: [300, 0, 1000, 25], + fontColor: "color", + fontSize: [14, 0, 40, 1], // px + fontFace: ["arial", "verdana", "tahoma"], + color: { + border: "color", + background: "color" + } + }, + keyboard: { + enabled: false, + speed: { x: [10, 0, 40, 1], y: [10, 0, 40, 1], zoom: [0.02, 0, 0.1, 0.005] }, + bindToWindow: true } + }, + manipulation: { + enabled: false, + initiallyVisible: false, + locale: ["en", "nl"], + functionality: { + addNode: true, + addEdge: true, + editNode: true, + editEdge: true, + deleteNode: true, + deleteEdge: true + } + }, + physics: { + barnesHut: { + theta: [0.5, 0.1, 1, 0.05], + gravitationalConstant: [-2000, -300000, 0, 50], + centralGravity: [0.3, 0, 10, 0.05], + springLength: [95, 0, 500, 5], + springConstant: [0.04, 0, 5, 0.005], + damping: [0.09, 0, 1, 0.01] + }, + repulsion: { + centralGravity: [0.2, 0, 10, 0.05], + springLength: [200, 0, 500, 5], + springConstant: [0.05, 0, 5, 0.005], + nodeDistance: [100, 0, 500, 5], + damping: [0.09, 0, 1, 0.01] + }, + hierarchicalRepulsion: { + centralGravity: 0.2, + springLength: [100, 0, 500, 5], + springConstant: [0.01, 0, 5, 0.005], + nodeDistance: [120, 0, 500, 5], + damping: [0.09, 0, 1, 0.01] + }, + maxVelocity: [50, 0, 150, 1], + minVelocity: [0.1, 0.01, 0.5, 0.01], + solver: ["BarnesHut", "Repulsion", "HierarchicalRepulsion"], + timestep: [0.5, 0, 1, 0.05] + }, + selection: { + select: true, + selectConnectedEdges: true + }, + renderer: { + hideEdgesOnDrag: false, + hideNodesOnDrag: false + } + }; - // if not connected to itself - if (node1 != node2) { - if (position !== "middle") { - // draw arrow head - if (this.options.smooth.enabled == true) { - arrowPos = this.findBorderPosition(node1, ctx, { via: viaNode }); - var guidePos = this.getPoint(Math.max(0, Math.min(1, arrowPos.t + guideOffset)), viaNode); - angle = Math.atan2(arrowPos.y - guidePos.y, arrowPos.x - guidePos.x); + this.actualOptions = {}; + + this.domElements = []; + } + + _prototypeProperties(ConfigurationSystem, null, { + setOptions: { + value: function setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.actualOptions, options); + + if (options.configure !== undefined && options.configure !== false) { + var config = undefined; + if (options.configure instanceof Array) { + config = options.configure.join(); + } else if (typeof options.configure === "string") { + config = options.configure; + } else if (typeof options.configure === "boolean") { + config = options.configure; } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.findBorderPosition(node1, ctx); + this._clean(); + throw new Error("the option for configure has to be either a string, boolean or an array. Supplied:" + options.configure); + return; } + this._create(config); } else { - angle = Math.atan2(node1.y - node2.y, node1.x - node2.x); - arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow. + this._clean(); } - // draw arrow at the end of the line - length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(arrowPos.x, arrowPos.y, angle, length); - ctx.fill(); - ctx.stroke(); - } else { - // draw circle - var _angle = undefined, - point = undefined; - var _getCircleData = this._getCircleData(); - - var _getCircleData2 = _slicedToArray(_getCircleData, 3); - - var x = _getCircleData2[0]; - var y = _getCircleData2[1]; - var radius = _getCircleData2[2]; + } + }, + writable: true, + configurable: true + }, + _create: { + /** + * + * @param {Boolean | String} config + * @private + */ + value: function _create(config) { + this._clean(); - if (position == "from") { - point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.25, high: 0.6, direction: -1 }); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; - } else if (position == "to") { - point = this.findBorderPosition(this.from, ctx, { x: x, y: y, low: 0.6, high: 1, direction: 1 }); - _angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; - } else { - point = this._pointOnCircle(x, y, radius, 0.175); - _angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; + for (var option in this.possibleOptions) { + if (this.possibleOptions.hasOwnProperty(option)) { + if (config === true || config.indexOf(option) !== -1) { + var optionObj = this.possibleOptions[option]; + // a header for the category + this._makeHeader(option); + + // get the suboptions + for (var subOption in optionObj) { + if (optionObj.hasOwnProperty(subOption)) { + this._makeLabel(subOption); + var subOptionObj = optionObj[subOption]; + if (subOptionObj instanceof Array) { + this._handleArray(option, subOption, subOptionObj); + } else if (subOptionObj instanceof Object) { + this._handleObject(option, subOption, subOptionObj); + } else if (typeof subOptionObj === "string") { + this._handleString(option, subOption, subOptionObj); + } else if (typeof subOptionObj === "boolean") { + this._handleBoolean(option, subOption, subOptionObj); + } else { + console.error("dont know how to handle", subOptionObj); + } + } + } + } } - - // draw the arrowhead - var _length = (10 + 5 * this.options.width) * scaleFactor; - ctx.arrow(point.x, point.y, _angle, _length); - ctx.fill(); - ctx.stroke(); } }, writable: true, configurable: true + }, + _clean: { + value: function _clean() {}, + writable: true, + configurable: true + }, + _makeHeader: { + value: function _makeHeader(name) { + console.log("header", name); + //let div = document.createElement('div'); + //div.className = 'vis-network-configuration header'; + //div.innerHTML = name; + //this.domElements.push(div); + }, + writable: true, + configurable: true + }, + _makeLabel: { + value: function _makeLabel(name) { + console.log("label", name); + //let div = document.createElement('div'); + //div.className = 'vis-network-configuration label'; + //div.innerHTML = name; + //this.domElements.push(div); + }, + writable: true, + configurable: true + }, + _handleObject: { + value: function _handleObject(category, subcategory, obj) { + console.log("obj", obj); + }, + writable: true, + configurable: true + }, + _handleArray: { + value: function _handleArray(category, subcategory, arr) { + console.log("arr", arr); + }, + writable: true, + configurable: true + }, + _handleString: { + value: function _handleString(category, subcategory, string) { + console.log("string", string); + }, + writable: true, + configurable: true + }, + _handleBoolean: { + value: function _handleBoolean(category, subcategory, boolean) { + console.log("boolean", boolean); + }, + writable: true, + configurable: true } }); - return EdgeBase; + return ConfigurationSystem; })(); - module.exports = EdgeBase; + module.exports = ConfigurationSystem; /***/ } /******/ ]) diff --git a/examples/network/01_basic_usage.html b/examples/network/01_basic_usage.html index 63da51ed..2b3ca1eb 100644 --- a/examples/network/01_basic_usage.html +++ b/examples/network/01_basic_usage.html @@ -44,20 +44,9 @@ nodes: nodes, edges: edges }; - var options = {edges:{arrows:'to'}, - manipulation:{ - initiallyVisible: true, - handlerFunctions: { - editNode: function(data, callback) { - console.log(data) - data.label = data.label + "i" - callback(data) - } - }, - controlNodeStyle: { - shape:'square' - } - }}//{physics:{stabilization:false}}; + var options = { + configure:true + }//{physics:{stabilization:false}}; var network = new vis.Network(container, data, options); diff --git a/lib/network/Network.js b/lib/network/Network.js index 10705511..6b26fe99 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -23,6 +23,7 @@ import InteractionHandler from './modules/InteractionHandler'; import SelectionHandler from "./modules/SelectionHandler"; import LayoutEngine from "./modules/LayoutEngine"; import ManipulationSystem from "./modules/ManipulationSystem"; +import ConfigurationSystem from "./modules/ConfigurationSystem"; /** * @constructor Network @@ -93,7 +94,6 @@ function Network (container, data, options) { // setting up all modules var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images - this.groups = new Groups(); // object with groups this.canvas = new Canvas(this.body); // DOM handler this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler @@ -108,6 +108,8 @@ function Network (container, data, options) { this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options + this.configurationSystem = new ConfigurationSystem(this); + // create the DOM elements this.canvas.create(); @@ -133,6 +135,8 @@ Network.prototype.setOptions = function (options) { // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system. options = this.layoutEngine.setOptions(options.layout, options); + + // pass the options to the modules this.groups.setOptions(options.groups); this.nodesHandler.setOptions(options.nodes); this.edgesHandler.setOptions(options.edges); @@ -145,7 +149,9 @@ Network.prototype.setOptions = function (options) { this.clustering.setOptions(options.clustering); this.manipulation.setOptions(options.manipulation); + this.configurationSystem.setOptions(options); + // handle network global options if (options.clickToUse !== undefined) { if (options.clickToUse === true) { if (this.activator === undefined) { diff --git a/lib/network/css/configuration.css b/lib/network/css/configuration.css new file mode 100644 index 00000000..5841ec31 --- /dev/null +++ b/lib/network/css/configuration.css @@ -0,0 +1,11 @@ + +vis-network-configuration { +} + + +vis-network-configuration.header{ +} + +vis-network-configuration.label{ +} + diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index 9c8451ee..7106f1ed 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -9,10 +9,16 @@ class ClusterEngine { constructor(body) { this.body = body; this.clusteredNodes = {}; + + this.options = {}; + this.defaultOptions = {}; + util.extend(this.options, this.defaultOptions); } setOptions(options) { + if (options !== undefined) { + } } /** diff --git a/lib/network/modules/ConfigurationSystem.js b/lib/network/modules/ConfigurationSystem.js new file mode 100644 index 00000000..5e4c8e94 --- /dev/null +++ b/lib/network/modules/ConfigurationSystem.js @@ -0,0 +1,316 @@ +/** + * Created by Alex on 3/26/2015. + */ + + +var util = require('../../util'); + + +class ConfigurationSystem { + constructor(network) { + this.network = network; + + this.possibleOptions = { + nodesHandler: { + borderWidth: 1, + borderWidthSelected: 2, + color: { + border: 'color', + background: 'color', + highlight: { + border: 'color', + background: '#D2E5FF' + }, + hover: { + border: 'color', + background: 'color' + } + }, + fixed: { + x:false, + y:false + }, + font: { + color: 'color', + size: [14,0,100,1], // px + face: ['arial','verdana','tahoma'], + background: 'color', + stroke: [0,0,50,1], // px + strokeColor: 'color' + }, + group: 'string', + hidden: false, + icon: { + face: 'string', //'FontAwesome', + code: 'string', //'\uf007', + size: [50,0,200,1], //50, + color:'color' //'#aa00ff' + }, + image: 'string', // --> URL + physics: true, + scaling: { + min: [10,0,200,1], + max: [30,0,200,1], + label: { + enabled: true, + min: [14,0,200,1], + max: [30,0,200,1], + maxVisible: [30,0,200,1], + drawThreshold: [3,0,20,1] + } + }, + shape: ['ellipse','box','circle','circularImage','database','diamond','dot','icon','image','square','star','text','triangle','triangleDown'], + size: [25,0,200,1] + }, + edgesHandler: { + arrows: { + to: {enabled: false, scaleFactor:[1,0,3,0.05]}, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1} + middle: {enabled: false, scaleFactor:[1,0,3,0.05]}, + from: {enabled: false, scaleFactor:[1,0,3,0.05]} + }, + color: { + color:'color', + highlight:'color', + hover: 'color', + inherit: { + enabled: true, + source: ['from','to'], // from / to + useGradients: false + }, + opacity:[1,0,1,0.05] + }, + dashes:{ + enabled: false, + length: [5,0,50,1], + gap: [5,0,50,1], + altLength: [5,0,50,1] + }, + font: { + color: 'color', + size: [14,0,100,1], // px + face: ['arial','verdana','tahoma'], + background: 'color', + stroke: [0,0,50,1], // px + strokeColor: 'color', + align:['horizontal','top','middle','bottom'] + }, + hidden: false, + hoverWidth: [1.5,0,10,0.1], + physics: true, + scaling: { + min: [10,0,200,1], + max: [30,0,200,1], + label: { + enabled: true, + min: [14,0,200,1], + max: [30,0,200,1], + maxVisible: [30,0,200,1], + drawThreshold: [3,0,20,1] + } + }, + selfReferenceSize:[20,0,200,1], + smooth: { + enabled: true, + dynamic: true, + type: ["continuous",'discrete','diagonalCross','straightCross','horizontal','vertical','curvedCW','curvedCCW'], + roundness: [0.5,0,1,0.05] + }, + width: [1,0,30,1], + widthSelectionMultiplier: [2,0,5,0.1] + }, + layout:{ + randomSeed: [0,0,500,1], + hierarchical: { + enabled:false, + levelSeparation: [150,20,500,5], + direction: ["UD",'DU','LR','RL'], // UD, DU, LR, RL + sortMethod: ["hubsize",'directed'] // hubsize, directed + } + }, + interaction: { + dragNodes:true, + dragView: true, + zoomView: true, + hoverEnabled: false, + showNavigationIcons: false, + tooltip: { + delay: [300,0,1000,25], + fontColor: 'color', + fontSize: [14,0,40,1], // px + fontFace: ['arial','verdana','tahoma'], + color: { + border: 'color', + background: 'color' + } + }, + keyboard: { + enabled: false, + speed: {x: [10,0,40,1], y: [10,0,40,1], zoom: [0.02,0,0.1,0.005]}, + bindToWindow: true + } + }, + manipulation:{ + enabled: false, + initiallyVisible: false, + locale: ['en','nl'], + functionality:{ + addNode: true, + addEdge: true, + editNode: true, + editEdge: true, + deleteNode: true, + deleteEdge: true + } + }, + physics: { + barnesHut: { + theta: [0.5,0.1,1,0.05], + gravitationalConstant: [-2000,-300000,0,50], + centralGravity: [0.3,0,10,0.05], + springLength: [95,0,500,5], + springConstant: [0.04,0,5,0.005], + damping: [0.09,0,1,0.01] + }, + repulsion: { + centralGravity: [0.2,0,10,0.05], + springLength: [200,0,500,5], + springConstant: [0.05,0,5,0.005], + nodeDistance: [100,0,500,5], + damping: [0.09,0,1,0.01] + }, + hierarchicalRepulsion: { + centralGravity: 0.2, + springLength: [100,0,500,5], + springConstant: [0.01,0,5,0.005], + nodeDistance: [120,0,500,5], + damping: [0.09,0,1,0.01] + }, + maxVelocity: [50,0,150,1], + minVelocity: [0.1,0.01,0.5,0.01], + solver: ['BarnesHut','Repulsion','HierarchicalRepulsion'], + timestep: [0.5,0,1,0.05] + }, + selection:{ + select: true, + selectConnectedEdges: true + }, + renderer: { + hideEdgesOnDrag: false, + hideNodesOnDrag: false + } + } + + this.actualOptions = {}; + + this.domElements = []; + } + + setOptions(options) { + if (options !== undefined) { + util.deepExtend(this.actualOptions, options); + + if (options.configure !== undefined && options.configure !== false) { + let config; + if (options.configure instanceof Array) { + config = options.configure.join(); + } + else if (typeof options.configure === 'string') { + config = options.configure; + } + else if (typeof options.configure === 'boolean') { + config = options.configure; + } + else { + this._clean(); + throw new Error("the option for configure has to be either a string, boolean or an array. Supplied:" + options.configure); + return; + } + this._create(config); + } + else { + this._clean(); + } + } + } + + /** + * + * @param {Boolean | String} config + * @private + */ + _create(config) { + this._clean(); + + for (let option in this.possibleOptions) { + if (this.possibleOptions.hasOwnProperty(option)) { + if (config === true || config.indexOf(option) !== -1) { + let optionObj = this.possibleOptions[option]; + // a header for the category + this._makeHeader(option); + + // get the suboptions + for (let subOption in optionObj) { + if (optionObj.hasOwnProperty(subOption)) { + this._makeLabel(subOption); + let subOptionObj = optionObj[subOption]; + if (subOptionObj instanceof Array) { + this._handleArray(option, subOption, subOptionObj); + } + else if (subOptionObj instanceof Object) { + this._handleObject(option, subOption, subOptionObj); + } + else if (typeof subOptionObj === 'string') { + this._handleString(option, subOption, subOptionObj); + } + else if (typeof subOptionObj === 'boolean') { + this._handleBoolean(option, subOption, subOptionObj); + } + else { + console.error("dont know how to handle", subOptionObj); + } + } + } + } + } + } + } + + _clean() { + + } + + _makeHeader(name) { + console.log("header",name); + //let div = document.createElement('div'); + //div.className = 'vis-network-configuration header'; + //div.innerHTML = name; + //this.domElements.push(div); + } + + _makeLabel(name) { + console.log("label",name); + //let div = document.createElement('div'); + //div.className = 'vis-network-configuration label'; + //div.innerHTML = name; + //this.domElements.push(div); + } + + _handleObject(category, subcategory, obj) { + console.log("obj",obj); + } + + _handleArray(category, subcategory, arr) { + console.log("arr",arr); + } + + _handleString(category, subcategory, string) { + console.log("string",string); + } + + _handleBoolean(category, subcategory, boolean) { + console.log("boolean",boolean); + } +} + + +export default ConfigurationSystem; \ No newline at end of file diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 5890198a..3a270cad 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -32,7 +32,7 @@ class PhysicsEngine { this.options = {}; this.defaultOptions = { barnesHut: { - thetaInverted: 1 / 0.5, // inverted to save time during calculation + theta: 0.5, // inverted to save time during calculation gravitationalConstant: -2000, centralGravity: 0.3, springLength: 95, @@ -40,7 +40,7 @@ class PhysicsEngine { damping: 0.09 }, repulsion: { - centralGravity: 0.0, + centralGravity: 0.2, springLength: 200, springConstant: 0.05, nodeDistance: 100, @@ -53,17 +53,17 @@ class PhysicsEngine { nodeDistance: 120, damping: 0.09 }, - solver: 'BarnesHut', - timestep: 0.5, maxVelocity: 50, minVelocity: 0.1, // px/s + solver: 'BarnesHut', stabilization: { enabled: true, iterations: 1000, // maximum number of iteration to stabilize updateInterval: 100, onlyDynamicEdges: false, zoomExtent: true - } + }, + timestep: 0.5 } util.extend(this.options, this.defaultOptions); @@ -87,17 +87,19 @@ class PhysicsEngine { } setOptions(options) { - if (options === false) { - this.physicsEnabled = false; - this.stopSimulation(); - } - else { - this.physicsEnabled = true; - if (options !== undefined) { - util.selectiveNotDeepExtend(['stabilization'],this.options, options); - util.mergeOptions(this.options, options, 'stabilization') + if (options !== undefined) { + if (options === false) { + this.physicsEnabled = false; + this.stopSimulation(); + } + else { + this.physicsEnabled = true; + if (options !== undefined) { + util.selectiveNotDeepExtend(['stabilization'], this.options, options); + util.mergeOptions(this.options, options, 'stabilization') + } + this.init(); } - this.init(); } } @@ -371,12 +373,6 @@ class PhysicsEngine { - - - - - - /** * 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. diff --git a/lib/network/modules/SelectionHandler.js b/lib/network/modules/SelectionHandler.js index e46b3cc4..03645868 100644 --- a/lib/network/modules/SelectionHandler.js +++ b/lib/network/modules/SelectionHandler.js @@ -16,7 +16,7 @@ class SelectionHandler { this.defaultOptions = { select: true, selectConnectedEdges: true - } + }; util.extend(this.options, this.defaultOptions); this.body.emitter.on("_dataChanged", () => { @@ -32,7 +32,6 @@ class SelectionHandler { } - /** * handles the selection part of the tap; * diff --git a/lib/network/modules/components/edges/util/EdgeBase.js b/lib/network/modules/components/edges/util/EdgeBase.js index 298454d0..009b37a1 100644 --- a/lib/network/modules/components/edges/util/EdgeBase.js +++ b/lib/network/modules/components/edges/util/EdgeBase.js @@ -51,7 +51,7 @@ class EdgeBase { _drawDashedLine(ctx) { let via = undefined; // only firefox and chrome support this method, else we use the legacy one. - if (ctx.setLineDash !== undefined) { + if (ctx.setLineDash !== undefined && this.options.dashes.altLength === undefined) { ctx.save(); // configure the dash pattern var pattern = [0]; diff --git a/lib/network/modules/components/physics/BarnesHutSolver.js b/lib/network/modules/components/physics/BarnesHutSolver.js index b2d4c90c..a77ce758 100644 --- a/lib/network/modules/components/physics/BarnesHutSolver.js +++ b/lib/network/modules/components/physics/BarnesHutSolver.js @@ -12,6 +12,7 @@ class BarnesHutSolver { setOptions(options) { this.options = options; + this.thetaInversed = 1/ this.options.theta; } @@ -68,11 +69,11 @@ class BarnesHutSolver { distance = Math.sqrt(dx * dx + dy * dy); // BarnesHutSolver condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed + // original condition : s/d < theta = passed === d/s > 1/theta = passed // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.options.thetaInverted) { + if (distance * parentBranch.calcSize > this.thetaInversed) { // duplicate code to reduce function calls to speed up program - if (distance == 0) { + if (distance === 0) { distance = 0.1 * Math.random(); dx = distance; } @@ -85,7 +86,7 @@ class BarnesHutSolver { } else { // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { + if (parentBranch.childrenCount === 4) { this._getForceContribution(parentBranch.children.NW, node); this._getForceContribution(parentBranch.children.NE, node); this._getForceContribution(parentBranch.children.SW, node); @@ -94,7 +95,7 @@ class BarnesHutSolver { else { // parentBranch must have only one node, if it was empty we wouldnt be here if (parentBranch.children.data.id != node.id) { // if it is not self // duplicate code to reduce function calls to speed up program - if (distance == 0) { + if (distance === 0) { distance = 0.5 * Math.random(); dx = distance; } @@ -270,8 +271,8 @@ class BarnesHutSolver { case 1: // convert into children // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && - parentBranch.children[region].children.data.y == node.y) { + if (parentBranch.children[region].children.data.x === node.x && + parentBranch.children[region].children.data.y === node.y) { node.x += Math.random(); node.y += Math.random(); } @@ -297,7 +298,7 @@ class BarnesHutSolver { _splitBranch(parentBranch) { // if the branch is shaded with a node, replace the node in the new subset. var containedNode = null; - if (parentBranch.childrenCount == 1) { + if (parentBranch.childrenCount === 1) { containedNode = parentBranch.children.data; parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; @@ -406,7 +407,7 @@ class BarnesHutSolver { color = "#FF0000"; } - if (branch.childrenCount == 4) { + if (branch.childrenCount === 4) { this._drawBranch(branch.children.NW, ctx); this._drawBranch(branch.children.NE, ctx); this._drawBranch(branch.children.SE, ctx);