diff --git a/.gitignore b/.gitignore index 7a1537ba..b6a487c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .idea node_modules +.project +.settings/.jsdtscope +.settings/org.eclipse.wst.jsdt.ui.superType.container +.settings/org.eclipse.wst.jsdt.ui.superType.name +npm-debug.log diff --git a/HISTORY.md b/HISTORY.md index 620a76db..7b9763cc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,35 @@ vis.js history http://visjs.org +## 2014-01-31, version 0.4.0 + +### Timeline + +- Implemented functions `on` and `off` to create event listeners for events + `rangechange`, `rangechanged`, and `select`. +- Impelmented function `select` to get and set the selected items. +- Items can be selected by clicking them, muti-select by holding them. +- Fixed non working `start` and `end` options. + +### Graph + +- Fixed longstanding bug in the force calculation, increasing simulation + stability and fluidity. +- Reworked the calculation of the Graph, increasing performance for larger + datasets (up to 10x!). +- Support for automatic clustering in Graph to handle large (>50000) datasets + without losing performance. +- Added automatic intial zooming to Graph, to more easily view large amounts + of data. +- Added local declustering to Graph, freezing the simulation of nodes outside + of the cluster. +- Added support for key-bindings by including mouseTrap in Graph. +- Added navigation controls. +- Added keyboard navigation. +- Implemented functions `on` and `off` to create event listeners for event + `select`. + + ## 2014-01-14, version 0.3.0 - Moved the generated library to folder `./dist` diff --git a/Jakefile.js b/Jakefile.js index f24b9566..4d273152 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -3,7 +3,7 @@ */ var jake = require('jake'), browserify = require('browserify'), - path = require('path'), + wrench = require('wrench'), fs = require('fs'); require('jake-utils'); @@ -29,7 +29,6 @@ task('default', ['build', 'minify'], function () { desc('Build the visualization library vis.js'); task('build', {async: true}, function () { jake.mkdirP(DIST); - // concatenate and stringify the css files concat({ src: [ @@ -83,6 +82,10 @@ task('build', {async: true}, function () { './src/graph/Popup.js', './src/graph/Groups.js', './src/graph/Images.js', + './src/graph/SectorsMixin.js', + './src/graph/ClusterMixin.js', + './src/graph/SelectionMixin.js', + './src/graph/NavigationMixin.js', './src/graph/Graph.js', './src/module/exports.js' @@ -91,6 +94,12 @@ task('build', {async: true}, function () { separator: '\n' }); + // copy images + wrench.copyDirSyncRecursive('./src/graph/img', DIST+ '/img', { + forceDelete: true + }); + + var timeStart = Date.now(); // bundle the concatenated script and dependencies into one file var b = browserify(); b.add(VIS_TMP); @@ -100,13 +109,13 @@ task('build', {async: true}, function () { if(err) { throw err; } - + console.log("browserify",Date.now() - timeStart); timeStart = Date.now(); // add header and footer var lib = read('./src/module/header.js') + code; // write bundled file write(VIS, lib); - console.log('created ' + VIS); + console.log('created js' + VIS); // remove temporary file fs.unlinkSync(VIS_TMP); @@ -133,7 +142,7 @@ task('minify', function () { // update version number and stuff in the javascript files replacePlaceholders(VIS_MIN); - console.log('created ' + VIS_MIN); + console.log('created minified ' + VIS_MIN); }); /** diff --git a/bower.json b/bower.json index 99167912..9ccabd0a 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "0.4.0-SNAPSHOT", + "version": "0.5.0-SNAPSHOT", "description": "A dynamic, browser-based visualization library.", "homepage": "http://visjs.org/", "repository": { diff --git a/dist/img/downarrow.png b/dist/img/downarrow.png new file mode 100644 index 00000000..e77d5e6d Binary files /dev/null and b/dist/img/downarrow.png differ diff --git a/dist/img/leftarrow.png b/dist/img/leftarrow.png new file mode 100644 index 00000000..3823536e Binary files /dev/null and b/dist/img/leftarrow.png differ diff --git a/dist/img/minus.png b/dist/img/minus.png new file mode 100644 index 00000000..30698076 Binary files /dev/null and b/dist/img/minus.png differ diff --git a/dist/img/plus.png b/dist/img/plus.png new file mode 100644 index 00000000..f7ab2a33 Binary files /dev/null and b/dist/img/plus.png differ diff --git a/dist/img/rightarrow.png b/dist/img/rightarrow.png new file mode 100644 index 00000000..c3a209d8 Binary files /dev/null and b/dist/img/rightarrow.png differ diff --git a/dist/img/uparrow.png b/dist/img/uparrow.png new file mode 100644 index 00000000..8aedced7 Binary files /dev/null and b/dist/img/uparrow.png differ diff --git a/dist/img/zoomExtends.png b/dist/img/zoomExtends.png new file mode 100644 index 00000000..74595c63 Binary files /dev/null and b/dist/img/zoomExtends.png differ diff --git a/dist/vis.css b/dist/vis.css index 4f54a95f..32bfe22c 100644 --- a/dist/vis.css +++ b/dist/vis.css @@ -109,6 +109,14 @@ z-index: 999; } +.vis.timeline .item.point.selected { + background-color: #FFF785; + z-index: 999; +} +.vis.timeline .item.point.selected .dot { + border-color: #FFC200; +} + .vis.timeline .item.cluster { /* TODO: use another color or pattern? */ background: #97B0F8 url('img/cluster_bg.png'); diff --git a/dist/vis.js b/dist/vis.js index 7594f2f9..794f3f67 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -4,11 +4,11 @@ * * A dynamic, browser-based visualization library. * - * @version 0.3.0 - * @date 2014-01-14 + * @version 0.4.0 + * @date 2014-01-31 * * @license - * Copyright (C) 2011-2013 Almende B.V, http://almende.com + * Copyright (C) 2011-2014 Almende B.V, http://almende.com * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy @@ -29,6 +29,7 @@ // Try to load dependencies from the global window object. // If not available there, load via require. + var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); var Hammer; @@ -42,6 +43,19 @@ else { } } +var mousetrap; +if (typeof window !== 'undefined') { + // load mousetrap.js only when running in a browser (where window is available) + mousetrap = window['mousetrap'] || require('mousetrap'); +} +else { + mousetrap = function () { + throw Error('mouseTrap is only available in a browser, not in node.js.'); + } +} + + + // Internet Explorer 8 and older does not support Array.indexOf, so we define // it here in that case. @@ -1220,6 +1234,7 @@ function DataSet (options) { this.data = {}; // map with data indexed by id this.fieldId = this.options.fieldId || 'id'; // name of the field containing id this.convert = {}; // field types by field name + this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function if (this.options.convert) { for (var field in this.options.convert) { @@ -1453,6 +1468,7 @@ DataSet.prototype.update = function (data, senderId) { */ DataSet.prototype.get = function (args) { var me = this; + var globalShowInternalIds = this.showInternalIds; // parse the arguments var id, ids, options, data; @@ -1496,6 +1512,13 @@ DataSet.prototype.get = function (args) { type = 'Array'; } + // we allow the setting of this value for a single get request. + if (options != undefined) { + if (options.showInternalIds != undefined) { + this.showInternalIds = options.showInternalIds; + } + } + // build options var convert = options && options.convert || this.options.convert; var filter = options && options.filter; @@ -1530,6 +1553,9 @@ DataSet.prototype.get = function (args) { } } + // restore the global value of showInternalIds + this.showInternalIds = globalShowInternalIds; + // order the results if (options && options.order && id == undefined) { this._sort(items, options.order); @@ -2009,7 +2035,7 @@ DataSet.prototype._getItem = function (id, convert) { if (raw.hasOwnProperty(field)) { value = raw[field]; // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { + if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) { converted[field] = util.convert(value, convert[field]); } } @@ -2021,13 +2047,12 @@ DataSet.prototype._getItem = function (id, convert) { if (raw.hasOwnProperty(field)) { value = raw[field]; // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { + if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) { converted[field] = value; } } } } - return converted; }; @@ -2061,6 +2086,17 @@ DataSet.prototype._updateItem = function (item) { return id; }; +/** + * check if an id is an internal or external id + * @param id + * @returns {boolean} + * @private + */ +DataSet.prototype.isInternalId = function(id) { + return (id in this.internalIds); +}; + + /** * Get an array with the column names of a Google DataTable * @param {DataTable} dataTable @@ -3112,15 +3148,30 @@ Range.prototype.subscribe = function (component, event, direction) { }; /** - * Event handler - * @param {String} event name of the event, for example 'click', 'mousemove' - * @param {function} callback callback handler, invoked with the raw HTML Event - * as parameter. + * Add event listener + * @param {String} event Name of the event. + * Available events: 'rangechange', 'rangechanged' + * @param {function} callback Callback function, invoked as callback({start: Date, end: Date}) */ -Range.prototype.on = function (event, callback) { +Range.prototype.on = function on (event, callback) { + var available = ['rangechange', 'rangechanged']; + + if (available.indexOf(event) == -1) { + throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); + } + events.addListener(this, event, callback); }; +/** + * Remove an event listener + * @param {String} event name of the event + * @param {function} callback callback handler + */ +Range.prototype.off = function off (event, callback) { + events.removeListener(this, event, callback); +}; + /** * Trigger an event * @param {String} event name of the event, available events: 'rangechange', @@ -3157,8 +3208,8 @@ Range.prototype.setRange = function(start, end) { * @private */ Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Number') : this.start, - newEnd = (end != null) ? util.convert(end, 'Number') : this.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; @@ -5142,8 +5193,9 @@ function ItemSet(parent, depends, options) { } }; - this.items = {}; // object with an Item for every data item - this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' + this.items = {}; // object with an Item for every data item + this.selection = []; // list with the ids of all selected nodes + this.queue = {}; // queue with id/actions: 'add', 'update', 'delete' this.stack = new Stack(this, Object.create(this.options)); this.conversion = null; @@ -5199,6 +5251,74 @@ ItemSet.prototype.setRange = function setRange(range) { this.range = range; }; +/** + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ +ItemSet.prototype.setSelection = function setSelection(ids) { + var i, ii, id, item, selection; + + if (ids) { + if (!Array.isArray(ids)) { + throw new TypeError('Array expected'); + } + + // 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(); + } + } + + // trigger a select event + selection = this.selection.concat([]); + events.trigger(this, 'select', { + ids: selection + }); + + if (this.controller) { + this.requestRepaint(); + } + } +}; + +/** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ +ItemSet.prototype.getSelection = function getSelection() { + return this.selection.concat([]); +}; + +/** + * Deselect a selected item + * @param {String | Number} id + * @private + */ +ItemSet.prototype._deselect = function _deselect(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; + } + } +}; + /** * Repaint the component * @return {Boolean} changed @@ -5287,74 +5407,82 @@ ItemSet.prototype.repaint = function repaint() { }; // show/hide added/changed/removed items - Object.keys(queue).forEach(function (id) { - //var entry = queue[id]; - var action = queue[id]; - var item = items[id]; - //var item = entry.item; - //noinspection FallthroughInSwitchStatementJS - switch (action) { - case 'add': - case 'update': - var itemData = itemsData && itemsData.get(id, dataOptions); - - if (itemData) { - var type = itemData.type || - (itemData.start && itemData.end && 'range') || - options.type || - 'box'; - var constructor = ItemSet.types[type]; + for (var id in queue) { + if (queue.hasOwnProperty(id)) { + var entry = queue[id], + item = items[id], + action = entry.action; - // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, hide and delete the item - changed += item.hide(); - item = null; - } - else { - item.data = itemData; // TODO: create a method item.setData ? - changed++; + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + var itemData = itemsData && itemsData.get(id, dataOptions); + + if (itemData) { + var type = itemData.type || + (itemData.start && itemData.end && 'range') || + options.type || + 'box'; + var constructor = ItemSet.types[type]; + + // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, hide and delete the item + changed += item.hide(); + item = null; + } + else { + item.data = itemData; // TODO: create a method item.setData ? + changed++; + } } - } - if (!item) { - // create item - if (constructor) { - item = new constructor(me, itemData, options, defaultOptions); - changed++; + if (!item) { + // create item + if (constructor) { + item = new constructor(me, itemData, options, defaultOptions); + item.id = entry.id; // we take entry.id, as id itself is stringified + changed++; + } + else { + throw new TypeError('Unknown item type "' + type + '"'); + } } - else { - throw new TypeError('Unknown item type "' + type + '"'); - } - } - // force a repaint (not only a reposition) - item.repaint(); + // force a repaint (not only a reposition) + item.repaint(); - items[id] = item; - } + items[id] = item; + } - // update queue - delete queue[id]; - break; + // update queue + delete queue[id]; + break; - case 'remove': - if (item) { - // remove DOM of the item - changed += item.hide(); - } + case 'remove': + if (item) { + // remove the item from the set selected items + if (item.selected) { + me._deselect(id); + } - // update lists - delete items[id]; - delete queue[id]; - break; + // remove DOM of the item + changed += item.hide(); + } - default: - console.log('Error: unknown action "' + action + '"'); + // update lists + delete items[id]; + delete queue[id]; + break; + + default: + console.log('Error: unknown action "' + action + '"'); + } } - }); + } // reposition all items. Show items only when in the visible area util.forEach(this.items, function (item) { @@ -5565,7 +5693,10 @@ ItemSet.prototype._onRemove = function _onRemove(ids) { ItemSet.prototype._toQueue = function _toQueue(action, ids) { var queue = this.queue; ids.forEach(function (id) { - queue[id] = action; + queue[id] = { + id: id, + action: action + }; }); if (this.controller) { @@ -5649,6 +5780,7 @@ function Item (parent, data, options, defaultOptions) { */ Item.prototype.select = function select() { this.selected = true; + if (this.visible) this.repaint(); }; /** @@ -5656,6 +5788,7 @@ Item.prototype.select = function select() { */ Item.prototype.unselect = function unselect() { this.selected = false; + if (this.visible) this.repaint(); }; /** @@ -5731,24 +5864,6 @@ function ItemBox (parent, data, options, defaultOptions) { ItemBox.prototype = new Item (null, null); -/** - * Select the item - * @override - */ -ItemBox.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect -}; - -/** - * Unselect the item - * @override - */ -ItemBox.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect -}; - /** * Repaint the item * @return {Boolean} changed @@ -5980,6 +6095,9 @@ ItemBox.prototype._create = function _create() { // dot on axis dom.dot = document.createElement('DIV'); dom.dot.className = 'dot'; + + // attach this item as attribute + dom.box['timeline-item'] = this; } }; @@ -6046,24 +6164,6 @@ function ItemPoint (parent, data, options, defaultOptions) { ItemPoint.prototype = new Item (null, null); -/** - * Select the item - * @override - */ -ItemPoint.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect -}; - -/** - * Unselect the item - * @override - */ -ItemPoint.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect -}; - /** * Repaint the item * @return {Boolean} changed @@ -6248,6 +6348,9 @@ ItemPoint.prototype._create = function _create() { dom.dot = document.createElement('div'); dom.dot.className = 'dot'; dom.point.appendChild(dom.dot); + + // attach this item as attribute + dom.point['timeline-item'] = this; } }; @@ -6294,24 +6397,6 @@ function ItemRange (parent, data, options, defaultOptions) { ItemRange.prototype = new Item (null, null); -/** - * Select the item - * @override - */ -ItemRange.prototype.select = function select() { - this.selected = true; - // TODO: select and unselect -}; - -/** - * Unselect the item - * @override - */ -ItemRange.prototype.unselect = function unselect() { - this.selected = false; - // TODO: select and unselect -}; - /** * Repaint the item * @return {Boolean} changed @@ -6359,7 +6444,8 @@ ItemRange.prototype.repaint = function repaint() { } // update class - var className = this.data.className ? (' ' + this.data.className) : ''; + var className = (this.data.className? ' ' + this.data.className : '') + + (this.selected ? ' selected' : ''); if (this.className != className) { this.className = className; dom.box.className = 'item range' + className; @@ -6516,6 +6602,9 @@ ItemRange.prototype._create = function _create() { dom.content = document.createElement('div'); dom.content.className = 'content'; dom.box.appendChild(dom.content); + + // attach this item as attribute + dom.box['timeline-item'] = this; } }; @@ -6706,6 +6795,25 @@ Group.prototype.setItems = function setItems(items) { } }; +/** + * Set selected items by their id. Replaces the current selection. + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ +Group.prototype.setSelection = function setSelection(ids) { + if (this.itemset) this.itemset.setSelection(ids); +}; + +/** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ +Group.prototype.getSelection = function getSelection() { + return this.itemset ? this.itemset.getSelection() : []; +}; + /** * Repaint the item * @return {Boolean} changed @@ -6891,6 +6999,47 @@ GroupSet.prototype.getGroups = function getGroups() { return this.groupsData; }; +/** + * Set selected items by their id. Replaces the current selection. + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ +GroupSet.prototype.setSelection = function setSelection(ids) { + var selection = [], + groups = this.groups; + + // iterate over each of the groups + for (var id in groups) { + if (groups.hasOwnProperty(id)) { + var group = groups[id]; + group.setSelection(ids); + } + } + + return selection; +}; + +/** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ +GroupSet.prototype.getSelection = function getSelection() { + var selection = [], + groups = this.groups; + + // iterate over each of the groups + for (var id in groups) { + if (groups.hasOwnProperty(id)) { + var group = groups[id]; + selection = selection.concat(group.getSelection()); + } + } + + return selection; +}; + /** * Repaint the component * @return {Boolean} changed @@ -7331,18 +7480,26 @@ function Timeline (container, items, options) { ); // TODO: reckon with options moveable and zoomable + // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable this.range.subscribe(this.rootPanel, 'move', 'horizontal'); this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); - this.range.on('rangechange', function () { + this.range.on('rangechange', function (properties) { var force = true; me.controller.requestReflow(force); + me._trigger('rangechange', properties); }); - this.range.on('rangechanged', function () { + this.range.on('rangechanged', function (properties) { var force = true; me.controller.requestReflow(force); + me._trigger('rangechanged', properties); }); - // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable + // single select (or unselect) when tapping an item + // TODO: implement ctrl+click + this.rootPanel.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); // time axis var timeaxisOptions = Object.create(rootOptions); @@ -7387,10 +7544,9 @@ function Timeline (container, items, options) { Timeline.prototype.setOptions = function (options) { util.extend(this.options, options); - // force update of range - // options.start and options.end can be undefined - //this.range.setRange(options.start, options.end); - this.range.setRange(); + // force update of range (apply new min/max etc.) + // both start and end are optional + this.range.setRange(options.start, options.end); this.controller.reflow(); this.controller.repaint(); @@ -7446,29 +7602,29 @@ Timeline.prototype.setItems = function(items) { var dataRange = this.getItemRange(); // add 5% space on both sides - var min = dataRange.min; - var max = dataRange.max; - if (min != null && max != null) { - var interval = (max.valueOf() - min.valueOf()); + 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 } - min = new Date(min.valueOf() - interval * 0.05); - max = new Date(max.valueOf() + interval * 0.05); + start = new Date(start.valueOf() - interval * 0.05); + end = new Date(end.valueOf() + interval * 0.05); } // override specified start and/or end date if (this.options.start != undefined) { - min = util.convert(this.options.start, 'Date'); + start = util.convert(this.options.start, 'Date'); } if (this.options.end != undefined) { - max = util.convert(this.options.end, 'Date'); + end = util.convert(this.options.end, 'Date'); } // apply range if there is a min or max available - if (min != null || max != null) { - this.range.setRange(min, max); + if (start != null || end != null) { + this.range.setRange(start, end); } } }; @@ -7589,6 +7745,132 @@ Timeline.prototype.getItemRange = function getItemRange() { }; }; +/** + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ +Timeline.prototype.setSelection = function setSelection (ids) { + if (this.content) this.content.setSelection(ids); +}; + +/** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ +Timeline.prototype.getSelection = function getSelection() { + return this.content ? this.content.getSelection() : []; +}; + +/** + * Add event listener + * @param {String} event Event name. Available events: + * 'rangechange', 'rangechanged', 'select' + * @param {function} callback Callback function, invoked as callback(properties) + * where properties is an optional object containing + * event specific properties. + */ +Timeline.prototype.on = function on (event, callback) { + var available = ['rangechange', 'rangechanged', 'select']; + + if (available.indexOf(event) == -1) { + throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); + } + + events.addListener(this, event, callback); +}; + +/** + * Remove an event listener + * @param {String} event Event name + * @param {function} callback Callback function + */ +Timeline.prototype.off = function off (event, callback) { + events.removeListener(this, event, callback); +}; + +/** + * Trigger an event + * @param {String} event Event name, available events: 'rangechange', + * 'rangechanged', 'select' + * @param {Object} [properties] Event specific properties + * @private + */ +Timeline.prototype._trigger = function _trigger(event, properties) { + events.trigger(this, event, properties || {}); +}; + +/** + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private + */ +Timeline.prototype._onSelectItem = function (event) { + var item = this._itemFromTarget(event); + + var selection = item ? [item.id] : []; + this.setSelection(selection); + + this._trigger('select', { + items: this.getSelection() + }); + + event.stopPropagation(); +}; + +/** + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private + */ +Timeline.prototype._onMultiSelectItem = function (event) { + var selection, + item = this._itemFromTarget(event); + + if (!item) { + // do nothing... + return; + } + + selection = this.getSelection(); // current selection + var index = selection.indexOf(item.id); + if (index == -1) { + // item is not yet selected -> select it + selection.push(item.id); + } + else { + // item is already selected -> deselect it + selection.splice(index, 1); + } + this.setSelection(selection); + + this._trigger('select', { + items: this.getSelection() + }); + + event.stopPropagation(); +}; + +/** + * 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 + * @private + */ +Timeline.prototype._itemFromTarget = function _itemFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } + + return null; +}; (function(exports) { /** * Parse a text source containing data in DOT language into a JSON object. @@ -8673,6 +8955,8 @@ function Node(properties, imagelist, grouplist, constants) { this.selected = false; this.edges = []; // all edges connected to this node + this.dynamicEdges = []; + this.reroutedEdges = {}; this.group = constants.nodes.group; this.fontSize = constants.nodes.fontSize; @@ -8689,24 +8973,53 @@ function Node(properties, imagelist, grouplist, constants) { this.y = 0; this.xFixed = false; this.yFixed = false; + this.horizontalAlignLeft = true; // these are for the navigation controls + this.verticalAlignTop = true; // these are for the navigation controls this.radius = constants.nodes.radius; + this.baseRadiusValue = constants.nodes.radius; this.radiusFixed = false; this.radiusMin = constants.nodes.radiusMin; this.radiusMax = constants.nodes.radiusMax; this.imagelist = imagelist; + this.grouplist = grouplist; this.setProperties(properties, constants); + // creating the variables for clustering + this.resetCluster(); + this.dynamicEdgesLength = 0; + this.clusterSession = 0; + this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width; + this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height; + this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius; + // mass, force, velocity - this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.mass = 1; // kg (mass is adjusted for the number of connected edges) this.fx = 0.0; // external force x this.fy = 0.0; // external force y this.vx = 0.0; // velocity x this.vy = 0.0; // velocity y this.minForce = constants.minForce; - this.damping = 0.9; // damping factor + this.damping = 0.9; + this.dampingFactor = 75; + + this.graphScaleInv = 1; + this.canvasTopLeft = {"x": -300, "y": -300}; + this.canvasBottomRight = {"x": 300, "y": 300}; +} + +/** + * (re)setting the clustering variables and objects + */ +Node.prototype.resetCluster = function() { + // clustering variables + this.formationScale = undefined; // this is used to determine when to open the cluster + this.clusterSize = 1; // this signifies the total amount of nodes in this cluster + this.containedNodes = {}; + this.containedEdges = {}; + this.clusterSessions = []; }; /** @@ -8717,6 +9030,10 @@ Node.prototype.attachEdge = function(edge) { if (this.edges.indexOf(edge) == -1) { this.edges.push(edge); } + if (this.dynamicEdges.indexOf(edge) == -1) { + this.dynamicEdges.push(edge); + } + this.dynamicEdgesLength = this.dynamicEdges.length; this._updateMass(); }; @@ -8728,7 +9045,9 @@ Node.prototype.detachEdge = function(edge) { var index = this.edges.indexOf(edge); if (index != -1) { this.edges.splice(index, 1); + this.dynamicEdges.splice(index, 1); } + this.dynamicEdgesLength = this.dynamicEdges.length; this._updateMass(); }; @@ -8738,7 +9057,7 @@ Node.prototype.detachEdge = function(edge) { * @private */ Node.prototype._updateMass = function() { - this.mass = 50 + 20 * this.edges.length; // kg + this.mass = 1 + 0.6 * this.edges.length; // kg }; /** @@ -8750,15 +9069,20 @@ Node.prototype.setProperties = function(properties, constants) { if (!properties) { return; } - + this.originalLabel = undefined; // basic properties - if (properties.id != undefined) {this.id = properties.id;} - if (properties.label != undefined) {this.label = properties.label;} - if (properties.title != undefined) {this.title = properties.title;} - if (properties.group != undefined) {this.group = properties.group;} - if (properties.x != undefined) {this.x = properties.x;} - if (properties.y != undefined) {this.y = properties.y;} - if (properties.value != undefined) {this.value = properties.value;} + if (properties.id !== undefined) {this.id = properties.id;} + if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;} + if (properties.title !== undefined) {this.title = properties.title;} + if (properties.group !== undefined) {this.group = properties.group;} + if (properties.x !== undefined) {this.x = properties.x;} + if (properties.y !== undefined) {this.y = properties.y;} + if (properties.value !== undefined) {this.value = properties.value;} + + // navigation controls properties + if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} + if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} + if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;} if (this.id === undefined) { throw "Node must have an id"; @@ -8775,17 +9099,16 @@ Node.prototype.setProperties = function(properties, constants) { } // individual shape properties - if (properties.shape != undefined) {this.shape = properties.shape;} - if (properties.image != undefined) {this.image = properties.image;} - if (properties.radius != undefined) {this.radius = properties.radius;} - if (properties.color != undefined) {this.color = Node.parseColor(properties.color);} - - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + if (properties.shape !== undefined) {this.shape = properties.shape;} + if (properties.image !== undefined) {this.image = properties.image;} + if (properties.radius !== undefined) {this.radius = properties.radius;} + if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);} + if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;} - if (this.image != undefined) { + if (this.image !== undefined) { if (this.imagelist) { this.imageObj = this.imagelist.load(this.image); } @@ -8794,9 +9117,9 @@ Node.prototype.setProperties = function(properties, constants) { } } - this.xFixed = this.xFixed || (properties.x != undefined); - this.yFixed = this.yFixed || (properties.y != undefined); - this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + this.xFixed = this.xFixed || (properties.x !== undefined); + this.yFixed = this.yFixed || (properties.y !== undefined); + this.radiusFixed = this.radiusFixed || (properties.radius !== undefined); if (this.shape == 'image') { this.radiusMin = constants.nodes.widthMin; @@ -8819,7 +9142,6 @@ Node.prototype.setProperties = function(properties, constants) { case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; } - // reset the size of the node, this can be changed this._reset(); }; @@ -8847,6 +9169,7 @@ Node.parseColor = function(color) { c = {}; c.background = color.background || 'white'; c.border = color.border || c.background; + if (util.isString(color.highlight)) { c.highlight = { border: color.highlight, @@ -8859,6 +9182,7 @@ Node.parseColor = function(color) { c.highlight.border = color.highlight && color.highlight.border || c.border; } } + return c; }; @@ -8878,13 +9202,21 @@ Node.prototype.unselect = function() { this._reset(); }; + /** * Reset the calculated size of the node, forces it to recalculate its size - * @private */ -Node.prototype._reset = function() { - this.width = undefined; - this.height = undefined; +Node.prototype.clearSizeCache = function() { + this._reset(); +}; + +/** + * Reset the calculated size of the node, forces it to recalculate its size + * @private + */ +Node.prototype._reset = function() { + this.width = undefined; + this.height = undefined; }; /** @@ -8974,15 +9306,15 @@ Node.prototype.discreteStep = function(interval) { if (!this.xFixed) { var dx = -this.damping * this.vx; // damping force var ax = (this.fx + dx) / this.mass; // acceleration - this.vx += ax / interval; // velocity - this.x += this.vx / interval; // position + this.vx += ax * interval; // velocity + this.x += this.vx * interval; // position } if (!this.yFixed) { var dy = -this.damping * this.vy; // damping force var ay = (this.fy + dy) / this.mass; // acceleration - this.vy += ay / interval; // velocity - this.y += this.vy / interval; // position + this.vy += ay * interval; // velocity + this.y += this.vy * interval; // position } }; @@ -9002,9 +9334,16 @@ Node.prototype.isFixed = function() { */ // TODO: replace this method with calculating the kinetic energy Node.prototype.isMoving = function(vmin) { - return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || - (!this.xFixed && Math.abs(this.fx) > this.minForce) || - (!this.yFixed && Math.abs(this.fy) > this.minForce)); + + if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) { +// console.log(vmin,this.vx,this.vy); + return true; + } + else { + this.vx = 0; this.vy = 0; + return false; + } + //return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin); }; /** @@ -9052,6 +9391,7 @@ Node.prototype.setValueRange = function(min, max) { this.radius = (this.value - min) * scale + this.radiusMin; } } + this.baseRadiusValue = this.radius; }; /** @@ -9078,20 +9418,28 @@ Node.prototype.resize = function(ctx) { * @return {boolean} True if location is located on node */ Node.prototype.isOverlappingWith = function(obj) { - return (this.left < obj.right && - this.left + this.width > obj.left && - this.top < obj.bottom && - this.top + this.height > obj.top); + return (this.left < obj.right && + this.left + this.width > obj.left && + this.top < obj.bottom && + this.top + this.height > obj.top); }; Node.prototype._resizeImage = function (ctx) { // TODO: pre calculate the image size - if (!this.width) { // undefined or 0 + + if (!this.width || !this.height) { // undefined or 0 var width, height; if (this.value) { + this.radius = this.baseRadiusValue; var scale = this.imageObj.height / this.imageObj.width; - width = this.radius || this.imageObj.width; - height = this.radius * scale || this.imageObj.height; + if (scale !== undefined) { + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = 0; + height = 0; + } } else { width = this.imageObj.width; @@ -9099,7 +9447,14 @@ Node.prototype._resizeImage = function (ctx) { } this.width = width; this.height = height; + + if (this.width > 0 && this.height > 0) { + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; + } } + }; Node.prototype._drawImage = function (ctx) { @@ -9109,7 +9464,19 @@ Node.prototype._drawImage = function (ctx) { this.top = this.y - this.height / 2; var yLabel; - if (this.imageObj) { + if (this.imageObj.width != 0 ) { + // draw the shade + if (this.clusterSize > 1) { + var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0); + lineWidth *= this.graphScaleInv; + lineWidth = Math.min(0.2 * this.width,lineWidth); + + ctx.globalAlpha = 0.5; + ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth); + } + + // draw the image + ctx.globalAlpha = 1.0; ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); yLabel = this.y + this.height / 2; } @@ -9128,6 +9495,10 @@ Node.prototype._resizeBox = function (ctx) { var textSize = this.getTextSize(ctx); this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; + + this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; +// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -9137,9 +9508,26 @@ Node.prototype._drawBox = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); ctx.fill(); ctx.stroke(); @@ -9155,6 +9543,11 @@ Node.prototype._resizeDatabase = function (ctx) { var size = textSize.width + 2 * margin; this.width = size; this.height = size; + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -9163,9 +9556,25 @@ Node.prototype._drawDatabase = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); ctx.fill(); ctx.stroke(); @@ -9183,6 +9592,11 @@ Node.prototype._resizeCircle = function (ctx) { this.width = diameter; this.height = diameter; + + // scaling used for clustering +// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; +// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -9191,9 +9605,25 @@ Node.prototype._drawCircle = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; ctx.circle(this.x, this.y, this.radius); ctx.fill(); ctx.stroke(); @@ -9210,6 +9640,11 @@ Node.prototype._resizeEllipse = function (ctx) { if (this.width < this.height) { this.width = this.height; } + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -9218,13 +9653,29 @@ Node.prototype._drawEllipse = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.ellipse(this.left, this.top, this.width, this.height); ctx.fill(); ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); }; @@ -9250,9 +9701,15 @@ Node.prototype._drawStar = function (ctx) { Node.prototype._resizeShape = function (ctx) { if (!this.width) { + this.radius = this.baseRadiusValue; var size = 2 * this.radius; this.width = size; this.height = size; + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -9262,9 +9719,35 @@ Node.prototype._drawShape = function (ctx, shape) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + var radiusMultiplier = 2; + + // choose draw method depending on the shape + switch (shape) { + case 'dot': radiusMultiplier = 2; break; + case 'square': radiusMultiplier = 2; break; + case 'triangle': radiusMultiplier = 3; break; + case 'triangleDown': radiusMultiplier = 3; break; + case 'star': radiusMultiplier = 4; break; + } + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; ctx[shape](this.x, this.y, this.radius); ctx.fill(); @@ -9281,6 +9764,11 @@ Node.prototype._resizeText = function (ctx) { var textSize = this.getTextSize(ctx); this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -9314,7 +9802,7 @@ Node.prototype._label = function (ctx, text, x, y, align, baseline) { Node.prototype.getTextSize = function(ctx) { - if (this.label != undefined) { + if (this.label !== undefined) { ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; var lines = this.label.split('\n'), @@ -9332,6 +9820,92 @@ Node.prototype.getTextSize = function(ctx) { } }; +/** + * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. + * there is a safety margin of 0.3 * width; + * + * @returns {boolean} + */ +Node.prototype.inArea = function() { + if (this.width !== undefined) { + return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x && + this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x && + this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y && + this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y); + } + else { + return true; + } +} + +/** + * checks if the core of the node is in the display area, this is used for opening clusters around zoom + * @returns {boolean} + */ +Node.prototype.inView = function() { + return (this.x >= this.canvasTopLeft.x && + this.x < this.canvasBottomRight.x && + this.y >= this.canvasTopLeft.y && + this.y < this.canvasBottomRight.y); +} + +/** + * This allows the zoom level of the graph to influence the rendering + * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas + * + * @param scale + * @param canvasTopLeft + * @param canvasBottomRight + */ +Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { + this.graphScaleInv = 1.0/scale; + this.canvasTopLeft = canvasTopLeft; + this.canvasBottomRight = canvasBottomRight; +}; + + +/** + * This allows the zoom level of the graph to influence the rendering + * + * @param scale + */ +Node.prototype.setScale = function(scale) { + this.graphScaleInv = 1.0/scale; +}; + +/** + * This function updates the damping parameter for clusters, based ont he + * + * @param {Number} numberOfNodes + */ +Node.prototype.updateDamping = function(numberOfNodes) { + this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2))); + this.damping *= this.dampingFactor; +}; + + +/** + * set the velocity at 0. Is called when this node is contained in another during clustering + */ +Node.prototype.clearVelocity = function() { + this.vx = 0; + this.vy = 0; +}; + + +/** + * Basic preservation of (kinectic) energy + * + * @param massBeforeClustering + */ +Node.prototype.updateVelocity = function(massBeforeClustering) { + var energyBefore = this.vx * this.vx * massBeforeClustering; + this.vx = Math.sqrt(energyBefore/this.mass); + energyBefore = this.vy * this.vy * massBeforeClustering; + this.vy = Math.sqrt(energyBefore/this.mass); +}; + + /** * @class Edge * @@ -9360,7 +9934,7 @@ function Edge (properties, graph, constants) { // initialize variables this.id = undefined; this.fromId = undefined; - this.toId = undefined; + this.toId = undefined; this.style = constants.edges.style; this.title = undefined; this.width = constants.edges.width; @@ -9369,6 +9943,12 @@ function Edge (properties, graph, constants) { this.from = null; // a node this.to = null; // a node + + // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster + // by storing the original information we can revert to the original connection when the cluser is opened. + this.originalFromId = []; + this.originalToId = []; + this.connected = false; // Added to support dashed lines @@ -9382,6 +9962,7 @@ function Edge (properties, graph, constants) { this.lengthFixed = false; this.setProperties(properties, constants); + } /** @@ -9394,41 +9975,41 @@ Edge.prototype.setProperties = function(properties, constants) { return; } - if (properties.from != undefined) {this.fromId = properties.from;} - if (properties.to != undefined) {this.toId = properties.to;} + if (properties.from !== undefined) {this.fromId = properties.from;} + if (properties.to !== undefined) {this.toId = properties.to;} - if (properties.id != undefined) {this.id = properties.id;} - if (properties.style != undefined) {this.style = properties.style;} - if (properties.label != undefined) {this.label = properties.label;} + if (properties.id !== undefined) {this.id = properties.id;} + if (properties.style !== undefined) {this.style = properties.style;} + if (properties.label !== undefined) {this.label = properties.label;} if (this.label) { this.fontSize = constants.edges.fontSize; this.fontFace = constants.edges.fontFace; this.fontColor = constants.edges.fontColor; - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;} } - if (properties.title != undefined) {this.title = properties.title;} - if (properties.width != undefined) {this.width = properties.width;} - if (properties.value != undefined) {this.value = properties.value;} - if (properties.length != undefined) {this.length = properties.length;} + if (properties.title !== undefined) {this.title = properties.title;} + if (properties.width !== undefined) {this.width = properties.width;} + if (properties.value !== undefined) {this.value = properties.value;} + if (properties.length !== undefined) {this.length = properties.length;} // Added to support dashed lines // David Jordan // 2012-08-08 if (properties.dash) { - if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;} - if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;} - if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;} + if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;} + if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;} + if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;} } - if (properties.color != undefined) {this.color = properties.color;} + if (properties.color !== undefined) {this.color = properties.color;} // A node is connected when it has a from and to node. this.connect(); - this.widthFixed = this.widthFixed || (properties.width != undefined); - this.lengthFixed = this.lengthFixed || (properties.length != undefined); + this.widthFixed = this.widthFixed || (properties.width !== undefined); + this.lengthFixed = this.lengthFixed || (properties.length !== undefined); this.stiffness = 1 / this.length; // set draw method based on style @@ -9596,10 +10177,10 @@ Edge.prototype._drawLine = function(ctx) { */ Edge.prototype._getLineWidth = function() { if (this.from.selected || this.to.selected) { - return Math.min(this.width * 2, this.widthMax); + return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv; } else { - return this.width; + return this.width*this.graphScaleInv; } }; @@ -9677,12 +10258,12 @@ Edge.prototype._drawDashLine = function(ctx) { // draw dashed line ctx.beginPath(); ctx.lineCap = 'round'; - if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value + if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value { ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]); } - else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value + else if (this.dash.length !== undefined && this.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value { ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, [this.dash.length,this.dash.gap]); @@ -9935,6 +10516,16 @@ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point return Math.sqrt(dx*dx + dy*dy); }; + + +/** + * This allows the zoom level of the graph to influence the rendering + * + * @param scale + */ +Edge.prototype.setScale = function(scale) { + this.graphScaleInv = 1.0/scale; +}; /** * Popup is a class to create a popup window with some text * @param {Element} container The container object. @@ -9975,7 +10566,7 @@ function Popup(container, x, y, text) { style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; style.whiteSpace = "nowrap"; this.container.appendChild(this.frame); -}; +} /** * @param {number} x Horizontal position of the popup window @@ -10165,2371 +10756,5073 @@ Images.prototype.load = function(url) { }; /** - * @constructor Graph - * Create a graph visualization, displaying nodes and edges. + * Creation of the SectorMixin var. * - * @param {Element} container The DOM element in which the Graph will - * be created. Normally a div element. - * @param {Object} data An object containing parameters - * {Array} nodes - * {Array} edges - * @param {Object} options Options + * This contains all the functions the Graph object can use to employ the sector system. + * The sector system is always used by Graph, though the benefits only apply to the use of clustering. + * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. + * + * Alex de Mulder + * 21-01-2013 */ -function Graph (container, data, options) { - // create variables and set default values - this.containerElement = container; - this.width = '100%'; - this.height = '100%'; - this.refreshRate = 50; // milliseconds - this.stabilize = true; // stabilize before displaying the graph - this.selectable = true; - - // set constant values - this.constants = { - nodes: { - radiusMin: 5, - radiusMax: 20, - radius: 5, - distance: 100, // px - shape: 'ellipse', - image: undefined, - widthMin: 16, // px - widthMax: 64, // px - fontColor: 'black', - fontSize: 14, // px - //fontFace: verdana, - fontFace: 'arial', - color: { - border: '#2B7CE9', - background: '#97C2FC', - highlight: { - border: '#2B7CE9', - background: '#D2E5FF' - } - }, - borderColor: '#2B7CE9', - backgroundColor: '#97C2FC', - highlightColor: '#D2E5FF', - group: undefined - }, - edges: { - widthMin: 1, - widthMax: 15, - width: 1, - style: 'line', - color: '#343434', - fontColor: '#343434', - fontSize: 14, // px - fontFace: 'arial', - //distance: 100, //px - length: 100, // px - dash: { - length: 10, - gap: 5, - altLength: undefined - } - }, - minForce: 0.05, - minVelocity: 0.02, // px/s - maxIterations: 1000 // maximum number of iteration to stabilize - }; +var SectorMixin = { - var graph = this; - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects - // TODO: create a counter to keep track on the number of nodes having values - // TODO: create a counter to keep track on the number of nodes currently moving - // TODO: create a counter to keep track on the number of edges having values + /** + * This function is only called by the setData function of the Graph object. + * This loads the global references into the active sector. This initializes the sector. + * + * @private + */ + _putDataInSector : function() { + this.sectors["active"][this._sector()].nodes = this.nodes; + this.sectors["active"][this._sector()].edges = this.edges; + this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; + }, - this.nodesData = null; // A DataSet or DataView - this.edgesData = null; // A DataSet or DataView - // create event listeners used to subscribe on the DataSets of the nodes and edges - var me = this; - this.nodesListeners = { - 'add': function (event, params) { - me._addNodes(params.items); - me.start(); - }, - 'update': function (event, params) { - me._updateNodes(params.items); - me.start(); - }, - 'remove': function (event, params) { - me._removeNodes(params.items); - me.start(); + /** + * /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied (active) sector. If a type is defined, do the specific type + * + * @param {String} sectorId + * @param {String} [sectorType] | "active" or "frozen" + * @private + */ + _switchToSector : function(sectorId, sectorType) { + if (sectorType === undefined || sectorType == "active") { + this._switchToActiveSector(sectorId); } - }; - this.edgesListeners = { - 'add': function (event, params) { - me._addEdges(params.items); - me.start(); - }, - 'update': function (event, params) { - me._updateEdges(params.items); - me.start(); - }, - 'remove': function (event, params) { - me._removeEdges(params.items); - me.start(); + else { + this._switchToFrozenSector(sectorId); } - }; + }, - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - graph._redraw(); - }); - // properties of the data - this.moving = false; // True if any of the nodes have an undefined position + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied active sector. + * + * @param sectorId + * @private + */ + _switchToActiveSector : function(sectorId) { + this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["active"][sectorId]["nodes"]; + this.edges = this.sectors["active"][sectorId]["edges"]; + }, - this.selection = []; - this.timer = undefined; - // create a frame and canvas - this._create(); + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied frozen sector. + * + * @param sectorId + * @private + */ + _switchToFrozenSector : function(sectorId) { + this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["frozen"][sectorId]["nodes"]; + this.edges = this.sectors["frozen"][sectorId]["edges"]; + }, - // apply options - this.setOptions(options); - // draw data - this.setData(data); -} + /** + * This function sets the global references to nodes, edges and nodeIndices to + * those of the navigation controls sector. + * + * @private + */ + _switchToNavigationSector : function() { + this.nodeIndices = this.sectors["navigation"]["nodeIndices"]; + this.nodes = this.sectors["navigation"]["nodes"]; + this.edges = this.sectors["navigation"]["edges"]; + }, -/** - * 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 - * {Options} [options] Object with options - */ -Graph.prototype.setData = function(data) { - 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); + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the currently active sector. + * + * @private + */ + _loadLatestSector : function() { + this._switchToSector(this._sector()); + }, - // set all data - if (data && data.dot) { - // parse DOT file - if(data && data.dot) { - var dotData = vis.util.DOTToGraph(data.dot); - this.setData(dotData); - return; + + /** + * This function returns the currently active sector Id + * + * @returns {String} + * @private + */ + _sector : function() { + return this.activeSector[this.activeSector.length-1]; + }, + + + /** + * This function returns the previously active sector Id + * + * @returns {String} + * @private + */ + _previousSector : function() { + if (this.activeSector.length > 1) { + return this.activeSector[this.activeSector.length-2]; } - } - else { - this._setNodes(data && data.nodes); - this._setEdges(data && data.edges); - } + else { + throw new TypeError('there are not enough sectors in the this.activeSector array.'); + } + }, - // find a stable position or start animating to a stable position - if (this.stabilize) { - this._doStabilize(); - } - this.start(); -}; -/** - * Set options - * @param {Object} options - */ -Graph.prototype.setOptions = function (options) { - if (options) { - // retrieve parameter values - if (options.width != undefined) {this.width = options.width;} - if (options.height != undefined) {this.height = options.height;} - if (options.stabilize != undefined) {this.stabilize = options.stabilize;} - if (options.selectable != undefined) {this.selectable = options.selectable;} + /** + * We add the active sector at the end of the this.activeSector array + * This ensures it is the currently active sector returned by _sector() and it reaches the top + * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. + * + * @param newId + * @private + */ + _setActiveSector : function(newId) { + this.activeSector.push(newId); + }, - // TODO: work out these options and document them - if (options.edges) { - for (var prop in options.edges) { - if (options.edges.hasOwnProperty(prop)) { - this.constants.edges[prop] = options.edges[prop]; - } + + /** + * We remove the currently active sector id from the active sector stack. This happens when + * we reactivate the previously active sector + * + * @private + */ + _forgetLastSector : function() { + this.activeSector.pop(); + }, + + + /** + * This function creates a new active sector with the supplied newId. This newId + * is the expanding node id. + * + * @param {String} newId | Id of the new active sector + * @private + */ + _createNewSector : function(newId) { + // create the new sector + this.sectors["active"][newId] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": this.scale, + "drawingNode": undefined}; + + // create the new sector render node. This gives visual feedback that you are in a new sector. + this.sectors["active"][newId]['drawingNode'] = new Node( + {id:newId, + color: { + background: "#eaefef", + border: "495c5e" + } + },{},{},this.constants); + this.sectors["active"][newId]['drawingNode'].clusterSize = 2; + }, + + + /** + * This function removes the currently active sector. This is called when we create a new + * active sector. + * + * @param {String} sectorId | Id of the active sector that will be removed + * @private + */ + _deleteActiveSector : function(sectorId) { + delete this.sectors["active"][sectorId]; + }, + + + /** + * This function removes the currently active sector. This is called when we reactivate + * the previously active sector. + * + * @param {String} sectorId | Id of the active sector that will be removed + * @private + */ + _deleteFrozenSector : function(sectorId) { + delete this.sectors["frozen"][sectorId]; + }, + + + /** + * Freezing an active sector means moving it from the "active" object to the "frozen" object. + * We copy the references, then delete the active entree. + * + * @param sectorId + * @private + */ + _freezeSector : function(sectorId) { + // we move the set references from the active to the frozen stack. + this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; + + // we have moved the sector data into the frozen set, we now remove it from the active set + this._deleteActiveSector(sectorId); + }, + + + /** + * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" + * object to the "active" object. + * + * @param sectorId + * @private + */ + _activateSector : function(sectorId) { + // we move the set references from the frozen to the active stack. + this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; + + // we have moved the sector data into the active set, we now remove it from the frozen stack + this._deleteFrozenSector(sectorId); + }, + + + /** + * This function merges the data from the currently active sector with a frozen sector. This is used + * in the process of reverting back to the previously active sector. + * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it + * upon the creation of a new active sector. + * + * @param sectorId + * @private + */ + _mergeThisWithFrozen : function(sectorId) { + // copy all nodes + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; } + } - if (options.edges.length != undefined && - options.nodes && options.nodes.distance == undefined) { - this.constants.edges.length = options.edges.length; - this.constants.nodes.distance = options.edges.length * 1.25; + // copy all edges (if not fully clustered, else there are no edges) + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; } + } - if (!options.edges.fontColor) { - this.constants.edges.fontColor = options.edges.color; + // merge the nodeIndices + for (var i = 0; i < this.nodeIndices.length; i++) { + this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); + } + }, + + + /** + * This clusters the sector to one cluster. It was a single cluster before this process started so + * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. + * + * @private + */ + _collapseThisToSingleCluster : function() { + this.clusterToFit(1,false); + }, + + + /** + * We create a new active sector from the node that we want to open. + * + * @param node + * @private + */ + _addSector : function(node) { + // this is the currently active sector + var sector = this._sector(); + +// // this should allow me to select nodes from a frozen set. +// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { +// console.log("the node is part of the active sector"); +// } +// else { +// console.log("I dont know what the fuck happened!!"); +// } + + // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. + delete this.nodes[node.id]; + + var unqiueIdentifier = util.randomUUID(); + + // we fully freeze the currently active sector + this._freezeSector(sector); + + // we create a new active sector. This sector has the Id of the node to ensure uniqueness + this._createNewSector(unqiueIdentifier); + + // we add the active sector to the sectors array to be able to revert these steps later on + this._setActiveSector(unqiueIdentifier); + + // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier + this._switchToSector(this._sector()); + + // finally we add the node we removed from our previous active sector to the new active sector + this.nodes[node.id] = node; + }, + + + /** + * We close the sector that is currently open and revert back to the one before. + * If the active sector is the "default" sector, nothing happens. + * + * @private + */ + _collapseSector : function() { + // the currently active sector + var sector = this._sector(); + + // we cannot collapse the default sector + if (sector != "default") { + if ((this.nodeIndices.length == 1) || + (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + var previousSector = this._previousSector(); + + // we collapse the sector back to a single cluster + this._collapseThisToSingleCluster(); + + // we move the remaining nodes, edges and nodeIndices to the previous sector. + // This previous sector is the one we will reactivate + this._mergeThisWithFrozen(previousSector); + + // the previously active (frozen) sector now has all the data from the currently active sector. + // we can now delete the active sector. + this._deleteActiveSector(sector); + + // we activate the previously active (and currently frozen) sector. + this._activateSector(previousSector); + + // we load the references from the newly active sector into the global references + this._switchToSector(previousSector); + + // we forget the previously active sector because we reverted to the one before + this._forgetLastSector(); + + // finally, we update the node index list. + this._updateNodeIndexList(); } + } + }, - // Added to support dashed lines - // David Jordan - // 2012-08-08 - if (options.edges.dash) { - if (options.edges.dash.length != undefined) { - this.constants.edges.dash.length = options.edges.dash.length; - } - if (options.edges.dash.gap != undefined) { - this.constants.edges.dash.gap = options.edges.dash.gap; + + /** + * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we dont pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInAllActiveSectors : function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + this[runFunction](); } - if (options.edges.dash.altLength != undefined) { - this.constants.edges.dash.altLength = options.edges.dash.altLength; + } + } + else { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } } } } + // we revert the global references back to our active sector + this._loadLatestSector(); + }, - if (options.nodes) { - for (prop in options.nodes) { - if (options.nodes.hasOwnProperty(prop)) { - this.constants.nodes[prop] = options.nodes[prop]; + + /** + * This runs a function in all frozen sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInAllFrozenSectors : function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + this[runFunction](); + } + } + } + else { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } } } + } + this._loadLatestSector(); + }, - if (options.nodes.color) { - this.constants.nodes.color = Node.parseColor(options.nodes.color); + + /** + * This runs a function in the navigation controls sector. + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInNavigationSector : function(runFunction,argument) { + this._switchToNavigationSector(); + if (argument === undefined) { + this[runFunction](); + } + else { + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); } + else { + this[runFunction](argument); + } + } + this._loadLatestSector(); + }, - /* - if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; - if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; - */ + + /** + * This runs a function in all sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInAllSectors : function(runFunction,argument) { + var args = Array.prototype.splice.call(arguments, 1); + if (argument === undefined) { + this._doInAllActiveSectors(runFunction); + this._doInAllFrozenSectors(runFunction); + } + else { + if (args.length > 1) { + this._doInAllActiveSectors(runFunction,args[0],args[1]); + this._doInAllFrozenSectors(runFunction,args[0],args[1]); + } + else { + this._doInAllActiveSectors(runFunction,argument); + this._doInAllFrozenSectors(runFunction,argument); + } } - if (options.groups) { - for (var groupname in options.groups) { - if (options.groups.hasOwnProperty(groupname)) { - var group = options.groups[groupname]; - this.groups.add(groupname, group); + }, + + + /** + * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the + * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. + * + * @private + */ + _clearNodeIndexList : function() { + var sector = this._sector(); + this.sectors["active"][sector]["nodeIndices"] = []; + this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; + }, + + + /** + * Draw the encompassing sector node + * + * @param ctx + * @param sectorType + * @private + */ + _drawSectorNodes : function(ctx,sectorType) { + var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; + for (var sector in this.sectors[sectorType]) { + if (this.sectors[sectorType].hasOwnProperty(sector)) { + if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { + + this._switchToSector(sector,sectorType); + + minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.resize(ctx); + if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} + if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} + if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} + if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} + } + } + node = this.sectors[sectorType][sector]["drawingNode"]; + node.x = 0.5 * (maxX + minX); + node.y = 0.5 * (maxY + minY); + node.width = 2 * (node.x - minX); + node.height = 2 * (node.y - minY); + node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); + node.setScale(this.scale); + node._drawCircle(ctx); } } } - } + }, - this.setSize(this.width, this.height); - this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); - this._setScale(1); + _drawAllSectorNodes : function(ctx) { + this._drawSectorNodes(ctx,"frozen"); + this._drawSectorNodes(ctx,"active"); + this._loadLatestSector(); + } }; - /** - * fire an event - * @param {String} event The name of an event, for example 'select' - * @param {Object} params Optional object with event parameters - * @private + * Creation of the ClusterMixin var. + * + * This contains all the functions the Graph object can use to employ clustering + * + * Alex de Mulder + * 21-01-2013 */ -Graph.prototype._trigger = function (event, params) { - events.trigger(this, event, params); -}; - +var ClusterMixin = { /** - * Create the main frame for the Graph. - * This function is executed once when a Graph object is created. The frame - * contains a canvas, and this canvas contains all objects like the axis and - * nodes. - * @private - */ -Graph.prototype._create = function () { - // remove all elements from the container element. - while (this.containerElement.hasChildNodes()) { - this.containerElement.removeChild(this.containerElement.firstChild); - } + * This is only called in the constructor of the graph object + * */ + startWithClustering : function() { + // cluster if the data set is big + this.clusterToFit(this.constants.clustering.initialMaxNodes, true); - this.frame = document.createElement('div'); - this.frame.className = 'graph-frame'; - this.frame.style.position = 'relative'; - this.frame.style.overflow = 'hidden'; + // updates the lables after clustering + this.updateLabels(); - // 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 is called here because if clusterin is disabled, the start and stabilize are called in + // the setData function. + if (this.stabilize) { + this._doStabilize(); + } + this.start(); + }, - var me = this; - this.drag = {}; - this.pinch = {}; - this.hammer = Hammer(this.frame.canvas, { - prevent_default: true - }); - this.hammer.on('tap', me._onTap.bind(me) ); - this.hammer.on('hold', me._onHold.bind(me) ); - this.hammer.on('pinch', me._onPinch.bind(me) ); - this.hammer.on('touch', me._onTouch.bind(me) ); - this.hammer.on('dragstart', me._onDragStart.bind(me) ); - this.hammer.on('drag', me._onDrag.bind(me) ); - this.hammer.on('dragend', me._onDragEnd.bind(me) ); - this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); - this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF - this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); + /** + * This function clusters until the initialMaxNodes has been reached + * + * @param {Number} maxNumberOfNodes + * @param {Boolean} reposition + */ + clusterToFit : function(maxNumberOfNodes, reposition) { + var numberOfNodes = this.nodeIndices.length; - // add the frame to the container element - this.containerElement.appendChild(this.frame); -}; + var maxLevels = 50; + var level = 0; -/** - * - * @param {{x: Number, y: Number}} pointer - * @return {Number | null} node - * @private - */ -Graph.prototype._getNodeAt = function (pointer) { - var x = this._canvasToX(pointer.x); - var y = this._canvasToY(pointer.y); + // we first cluster the hubs, then we pull in the outliers, repeat + while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { + if (level % 3 == 0) { + this.forceAggregateHubs(); + } + else { + this.increaseClusterLevel(); + } + numberOfNodes = this.nodeIndices.length; + level += 1; + } - var obj = { - left: x, - top: y, - right: x, - bottom: y - }; + // after the clustering we reposition the nodes to reduce the initial chaos + if (level > 1 && reposition == true) { + this.repositionNodes(); + } + }, - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - var overlappingNodes = this._getNodesOverlappingWith(obj); - return (overlappingNodes.length > 0) ? - overlappingNodes[overlappingNodes.length - 1] : null; -}; + /** + * This function can be called to open up a specific cluster. It is only called by + * It will unpack the cluster back one level. + * + * @param node | Node object: cluster to open. + */ + openCluster : function(node) { + var isMovingBeforeClustering = this.moving; + if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && + !(this._sector() == "default" && this.nodeIndices.length == 1)) { + this._addSector(node); + var level = 0; + while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { + this.decreaseClusterLevel(); + level += 1; + } + } + else { + this._expandClusterNode(node,false,true); -/** - * Get the pointer location from a touch location - * @param {{pageX: Number, pageY: Number}} touch - * @return {{x: Number, y: Number}} pointer - * @private - */ -Graph.prototype._getPointer = function (touch) { - return { - x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas), - y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas) - }; -}; + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this.updateLabels(); + } -/** - * On start of a touch gesture, store the pointer - * @param event - * @private - */ -Graph.prototype._onTouch = function (event) { - this.drag.pointer = this._getPointer(event.gesture.touches[0]); - this.drag.pinched = false; - this.pinch.scale = this._getScale(); -}; + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + }, -/** - * handle drag start event - * @private - */ -Graph.prototype._onDragStart = function () { - var drag = this.drag; + /** + * This calls the updateClustes with default arguments + */ + updateClustersDefault : function() { + if (this.constants.clustering.enabled == true) { + this.updateClusters(0,false,false); + } + }, - drag.selection = []; - drag.translation = this._getTranslation(); - drag.nodeId = this._getNodeAt(drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location + /** + * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will + * be clustered with their connected node. This can be repeated as many times as needed. + * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. + */ + increaseClusterLevel : function() { + this.updateClusters(-1,false,true); + }, - var node = this.nodes[drag.nodeId]; - if (node) { - // select the clicked node if not yet selected - if (!node.isSelected()) { - this._selectNodes([drag.nodeId]); - } - // create an array with the selected nodes and their original location and status - var me = this; - this.selection.forEach(function (id) { - var node = me.nodes[id]; - if (node) { - var s = { - id: id, - node: node, - // store original x, y, xFixed and yFixed, make the node temporarily Fixed - x: node.x, - y: node.y, - xFixed: node.xFixed, - yFixed: node.yFixed - }; + /** + * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will + * be unpacked if they are a cluster. This can be repeated as many times as needed. + * This can be called externally (by a key-bind for instance) to look into clusters without zooming. + */ + decreaseClusterLevel : function() { + this.updateClusters(1,false,true); + }, - node.xFixed = true; - node.yFixed = true; - drag.selection.push(s); + /** + * This is the main clustering function. It clusters and declusters on zoom or forced + * This function clusters on zoom, it can be called with a predefined zoom direction + * If out, check if we can form clusters, if in, check if we can open clusters. + * This function is only called from _zoom() + * + * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn + * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters + * @param {Boolean} force | enabled or disable forcing + * + */ + updateClusters : function(zoomDirection,recursive,force) { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; + + // on zoom out collapse the sector if the scale is at the level the sector was made + if (this.previousScale > this.scale && zoomDirection == 0) { + this._collapseSector(); + } + + // check if we zoom in or out + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + // forming clusters when forced pulls outliers in. When not forced, the edge length of the + // outer nodes determines if it is being clustered + this._formClusters(force); + } + else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in + if (force == true) { + // _openClusters checks for each node if the formationScale of the cluster is smaller than + // the current scale and if so, declusters. When forced, all clusters are reduced by one step + this._openClusters(recursive,force); } - }); + else { + // if a cluster takes up a set percentage of the active window + this._openClustersBySize(); + } + } + this._updateNodeIndexList(); - } -}; + // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs + if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { + this._aggregateHubs(force); + this._updateNodeIndexList(); + } -/** - * handle drag event - * @private - */ -Graph.prototype._onDrag = function (event) { - if (this.drag.pinched) { - return; - } + // we now reduce chains. + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + this.handleChains(); + this._updateNodeIndexList(); + } - var pointer = this._getPointer(event.gesture.touches[0]); + this.previousScale = this.scale; - var me = this, - drag = this.drag, - selection = drag.selection; - if (selection && selection.length) { - // calculate delta's and new location - var deltaX = pointer.x - drag.pointer.x, - deltaY = pointer.y - drag.pointer.y; + // rest of the update the index list, dynamic edges and labels + this._updateDynamicEdges(); + this.updateLabels(); - // update position of all selected nodes - selection.forEach(function (s) { - var node = s.node; + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place + this.clusterSession += 1; + } - if (!s.xFixed) { - node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX); - } + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + }, - if (!s.yFixed) { - node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY); - } - }); + /** + * This function handles the chains. It is called on every updateClusters(). + */ + handleChains : function() { + // after clustering we check how many chains there are + var chainPercentage = this._getChainFraction(); + if (chainPercentage > this.constants.clustering.chainThreshold) { + this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) - // start animation if not yet running - if (!this.moving) { - this.moving = true; - this.start(); } - } - else { - // move the graph - var diffX = pointer.x - this.drag.pointer.x; - var diffY = pointer.y - this.drag.pointer.y; + }, - this._setTranslation( - this.drag.translation.x + diffX, - this.drag.translation.y + diffY); - this._redraw(); + /** + * this functions starts clustering by hubs + * The minimum hub threshold is set globally + * + * @private + */ + _aggregateHubs : function(force) { + this._getHubSize(); + this._formClustersByHub(force,false); + }, - this.moved = true; - } -}; -/** - * handle drag start event - * @private - */ -Graph.prototype._onDragEnd = function () { - var selection = this.drag.selection; - if (selection) { - selection.forEach(function (s) { - // restore original xFixed and yFixed - s.node.xFixed = s.xFixed; - s.node.yFixed = s.yFixed; - }); - } -}; + /** + * This function is fired by keypress. It forces hubs to form. + * + */ + forceAggregateHubs : function() { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; -/** - * handle tap/click event: select/unselect a node - * @private - */ -Graph.prototype._onTap = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); + this._aggregateHubs(true); - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - // select this node - this._selectNodes([nodeId]); + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this.updateLabels(); - if (!this.moving) { - this._redraw(); + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length != amountOfNodes) { + this.clusterSession += 1; } - } - else { - // remove selection - this._unselectNodes(); - this._redraw(); - } -}; -/** - * handle long tap event: multi select nodes - * @private - */ -Graph.prototype._onHold = function (event) { - var pointer = this._getPointer(event.gesture.touches[0]); - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - if (!node.isSelected()) { - // select this node, keep previous selection - var append = true; - this._selectNodes([nodeId], append); - } - else { - this._unselectNodes([nodeId]); + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); } + }, - if (!this.moving) { - this._redraw(); + /** + * If a cluster takes up more than a set percentage of the screen, open the cluster + * + * @private + */ + _openClustersBySize : function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.inView() == true) { + if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + this.openCluster(node); + } + } + } } - } - else { - // Do nothing - } -}; - -/** - * Handle pinch event - * @param event - * @private - */ -Graph.prototype._onPinch = function (event) { - var pointer = this._getPointer(event.gesture.center); + }, - this.drag.pinched = true; - if (!('scale' in this.pinch)) { - this.pinch.scale = 1; - } - // TODO: enable moving while pinching? - var scale = this.pinch.scale * event.gesture.scale; - this._zoom(scale, pointer) -}; + /** + * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it + * has to be opened based on the current zoom level. + * + * @private + */ + _openClusters : function(recursive,force) { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + this._expandClusterNode(node,recursive,force); + } + }, -/** - * Zoom the graph in or out - * @param {Number} scale a number around 1, and between 0.01 and 10 - * @param {{x: Number, y: Number}} pointer - * @return {Number} appliedScale scale is limited within the boundaries - * @private - */ -Graph.prototype._zoom = function(scale, pointer) { - var scaleOld = this._getScale(); - if (scale < 0.01) { - scale = 0.01; - } - if (scale > 10) { - scale = 10; - } - - var translation = this._getTranslation(); - var scaleFrac = scale / scaleOld; - var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; - var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + /** + * This function checks if a node has to be opened. This is done by checking the zoom level. + * If the node contains child nodes, this function is recursively called on the child nodes as well. + * This recursive behaviour is optional and can be set by the recursive argument. + * + * @param {Node} parentNode | to check for cluster and expand + * @param {Boolean} recursive | enabled or disable recursive calling + * @param {Boolean} force | enabled or disable forcing + * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released + * @private + */ + _expandClusterNode : function(parentNode, recursive, force, openAll) { + // first check if node is a cluster + if (parentNode.clusterSize > 1) { + // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 + if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) { + openAll = true; + } + recursive = openAll ? true : recursive; + + // if the last child has been added on a smaller scale than current scale decluster + if (parentNode.formationScale < this.scale || force == true) { + // we will check if any of the contained child nodes should be removed from the cluster + for (var containedNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) { + var childNode = parentNode.containedNodes[containedNodeId]; + + // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that + // the largest cluster is the one that comes from outside + if (force == true) { + if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] + || openAll) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } + } + else { + if (this._nodeInActiveArea(parentNode)) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } + } + } + } + } + } + }, - this._setScale(scale); - this._setTranslation(tx, ty); - this._redraw(); + /** + * ONLY CALLED FROM _expandClusterNode + * + * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove + * the child node from the parent contained_node object and put it back into the global nodes object. + * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. + * + * @param {Node} parentNode | the parent node + * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node + * @param {Boolean} recursive | This will also check if the child needs to be expanded. + * With force and recursive both true, the entire cluster is unpacked + * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent + * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released + * @private + */ + _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) { + var childNode = parentNode.containedNodes[containedNodeId]; + + // if child node has been added on smaller scale than current, kick out + if (childNode.formationScale < this.scale || force == true) { + // put the child node back in the global nodes object + this.nodes[containedNodeId] = childNode; + + // release the contained edges from this childNode back into the global edges + this._releaseContainedEdges(parentNode,childNode); + + // reconnect rerouted edges to the childNode + this._connectEdgeBackToChild(parentNode,childNode); + + // validate all edges in dynamicEdges + this._validateEdges(parentNode); + + // undo the changes from the clustering operation on the parent node + parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass; + parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; + parentNode.clusterSize -= childNode.clusterSize; + parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; + + // place the child node near the parent, not at the exact same location to avoid chaos in the system + childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; + childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; + + // remove node from the list + delete parentNode.containedNodes[containedNodeId]; + + // check if there are other childs with this clusterSession in the parent. + var othersPresent = false; + for (var childNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(childNodeId)) { + if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) { + othersPresent = true; + break; + } + } + } + // if there are no others, remove the cluster session from the list + if (othersPresent == false) { + parentNode.clusterSessions.pop(); + } - return scale; -}; + // remove the clusterSession from the child node + childNode.clusterSession = 0; -/** - * 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 - */ -Graph.prototype._onMouseWheel = function(event) { - // retrieve delta - var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta/120; - } else if (event.detail) { /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail/3; - } + // restart the simulation to reorganise all nodes + this.moving = true; - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - if (!('mouswheelScale' in this.pinch)) { - this.pinch.mouswheelScale = 1; + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); } - // calculate the new scale - var scale = this.pinch.mouswheelScale; - var zoom = delta / 10; - if (delta < 0) { - zoom = zoom / (1 - zoom); + // check if a further expansion step is possible if recursivity is enabled + if (recursive == true) { + this._expandClusterNode(childNode,recursive,force,openAll); } - scale *= (1 + zoom); + }, - // calculate the pointer location - var gesture = util.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); - - // apply the new scale - scale = this._zoom(scale, pointer); - - // store the new, applied scale - this.pinch.mouswheelScale = scale; - } - // Prevent default actions caused by mouse wheel. - event.preventDefault(); -}; + /** + * This function checks if any nodes at the end of their trees have edges below a threshold length + * This function is called only from updateClusters() + * forceLevelCollapse ignores the length of the edge and collapses one level + * This means that a node with only one edge will be clustered with its connected node + * + * @private + * @param {Boolean} force + */ + _formClusters : function(force) { + if (force == false) { + this._formClustersByZoom(); + } + else { + this._forceClustersByZoom(); + } + }, + /** + * This function handles the clustering by zooming out, this is based on a minimum edge distance + * + * @private + */ + _formClustersByZoom : function() { + var dx,dy,length, + minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + + // check if any edges are shorter than minLength and start the clustering + // the clustering favours the node with the larger mass + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + var edge = this.edges[edgeId]; + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); + + + if (length < minLength) { + // first check which node is larger + var parentNode = edge.from; + var childNode = edge.to; + if (edge.to.mass > edge.from.mass) { + parentNode = edge.to; + childNode = edge.from; + } + + if (childNode.dynamicEdgesLength == 1) { + this._addToCluster(parentNode,childNode,false); + } + else if (parentNode.dynamicEdgesLength == 1) { + this._addToCluster(childNode,parentNode,false); + } + } + } + } + } + } + }, -/** - * Mouse move handler for checking whether the title moves over a node with a title. - * @param {Event} event - * @private - */ -Graph.prototype._onMouseMoveTitle = function (event) { - var gesture = util.fakeGesture(this, event); - var pointer = this._getPointer(gesture.center); + /** + * This function forces the graph to cluster all nodes with only one connecting edge to their + * connected node. + * + * @private + */ + _forceClustersByZoom : function() { + for (var nodeId in this.nodes) { + // another node could have absorbed this child. + if (this.nodes.hasOwnProperty(nodeId)) { + var childNode = this.nodes[nodeId]; + + // the edges can be swallowed by another decrease + if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { + var edge = childNode.dynamicEdges[0]; + var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; + + // group to the largest node + if (childNode.id != parentNode.id) { + if (parentNode.mass > childNode.mass) { + this._addToCluster(parentNode,childNode,true); + } + else { + this._addToCluster(childNode,parentNode,true); + } + } + } + } + } + }, - // check if the previously selected node is still selected - if (this.popupNode) { - this._checkHidePopup(pointer); - } - // start a timeout that will check if the mouse is positioned above - // an element - var me = this; - var checkShow = function() { - me._checkShowPopup(pointer); - }; - if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running timer - } - if (!this.leftButtonDown) { - this.popupTimer = setTimeout(checkShow, 300); - } -}; -/** - * Check if there is an element on the given position in the graph - * (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 - */ -Graph.prototype._checkShowPopup = function (pointer) { - var obj = { - left: this._canvasToX(pointer.x), - top: this._canvasToY(pointer.y), - right: this._canvasToX(pointer.x), - bottom: this._canvasToY(pointer.y) - }; + /** + * This function forms clusters from hubs, it loops over all nodes + * + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @private + */ + _formClustersByHub : function(force, onlyEqual) { + // we loop over all nodes in the list + for (var nodeId in this.nodes) { + // we check if it is still available since it can be used by the clustering in this loop + if (this.nodes.hasOwnProperty(nodeId)) { + this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); + } + } + }, - var id; - var lastPopupNode = this.popupNode; + /** + * This function forms a cluster from a specific preselected hub node + * + * @param {Node} hubNode | the node we will cluster as a hub + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @param {Number} [absorptionSizeOffset] | + * @private + */ + _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) { + if (absorptionSizeOffset === undefined) { + absorptionSizeOffset = 0; + } + // we decide if the node is a hub + if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || + (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { + // initialize variables + var dx,dy,length; + var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + var allowCluster = false; + + // we create a list of edges because the dynamicEdges change over the course of this loop + var edgesIdarray = []; + var amountOfInitialEdges = hubNode.dynamicEdges.length; + for (var j = 0; j < amountOfInitialEdges; j++) { + edgesIdarray.push(hubNode.dynamicEdges[j].id); + } - if (this.popupNode == undefined) { - // search the nodes for overlap, select the top one in case of multiple nodes - var nodes = this.nodes; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { - this.popupNode = node; - break; + // if the hub clustering is not forces, we check if one of the edges connected + // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold + if (force == false) { + allowCluster = false; + for (j = 0; j < amountOfInitialEdges; j++) { + var edge = this.edges[edgesIdarray[j]]; + if (edge !== undefined) { + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); + + if (length < minLength) { + allowCluster = true; + break; + } + } + } + } } } - } - } - if (this.popupNode == undefined) { - // search the edges for overlap - var edges = this.edges; - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected && (edge.getTitle() != undefined) && - edge.isOverlappingWith(obj)) { - this.popupNode = edge; - break; + // start the clustering if allowed + if ((!force && allowCluster) || force) { + // we loop over all edges INITIALLY connected to this hub + for (j = 0; j < amountOfInitialEdges; j++) { + edge = this.edges[edgesIdarray[j]]; + // the edge can be clustered by this function in a previous loop + if (edge !== undefined) { + var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; + // we do not want hubs to merge with other hubs nor do we want to cluster itself. + if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && + (childNode.id != hubNode.id)) { + this._addToCluster(hubNode,childNode,force); + } + } } } } - } + }, - if (this.popupNode) { - // show popup message window - if (this.popupNode != lastPopupNode) { - var me = this; - if (!me.popup) { - me.popup = new Popup(me.frame); - } - // adjust a small offset such that the mouse cursor is located in the - // bottom left location of the popup, and you can easily move over the - // popup area - me.popup.setPosition(pointer.x - 3, pointer.y - 3); - me.popup.setText(me.popupNode.getTitle()); - me.popup.show(); - } - } - else { - if (this.popup) { - this.popup.hide(); - } - } -}; -/** - * Check if the popup must be hided, which is the case when the mouse is no - * longer hovering on the object - * @param {{x:Number, y:Number}} pointer - * @private - */ -Graph.prototype._checkHidePopup = function (pointer) { - if (!this.popupNode || !this._getNodeAt(pointer) ) { - this.popupNode = undefined; - if (this.popup) { - this.popup.hide(); + /** + * This function adds the child node to the parent node, creating a cluster if it is not already. + * + * @param {Node} parentNode | this is the node that will house the child node + * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node + * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse + * @private + */ + _addToCluster : function(parentNode, childNode, force) { + // join child node in the parent node + parentNode.containedNodes[childNode.id] = childNode; + + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < childNode.dynamicEdges.length; i++) { + var edge = childNode.dynamicEdges[i]; + if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode + this._addToContainedEdges(parentNode,childNode,edge); + } + else { + this._connectEdgeToCluster(parentNode,childNode,edge); + } } - } -}; + // a contained node has no dynamic edges. + childNode.dynamicEdges = []; -/** - * Unselect selected nodes. If no selection array is provided, all nodes - * are unselected - * @param {Object[]} selection Array with selection objects, each selection - * object has a parameter row. Optional - * @param {Boolean} triggerSelect If true (default), the select event - * is triggered when nodes are unselected - * @return {Boolean} changed True if the selection is changed - * @private - */ -Graph.prototype._unselectNodes = function(selection, triggerSelect) { - var changed = false; - var i, iMax, id; + // remove circular edges from clusters + this._containCircularEdgesFromNode(parentNode,childNode); - if (selection) { - // remove provided selections - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - this.nodes[id].unselect(); - var j = 0; - while (j < this.selection.length) { - if (this.selection[j] == id) { - this.selection.splice(j, 1); - changed = true; - } - else { - j++; - } - } + // remove the childNode from the global nodes object + delete this.nodes[childNode.id]; + + // update the properties of the child and parent + var massBefore = parentNode.mass; + childNode.clusterSession = this.clusterSession; + parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass; + parentNode.clusterSize += childNode.clusterSize; + parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; + + // keep track of the clustersessions so we can open the cluster up as it has been formed. + if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { + parentNode.clusterSessions.push(this.clusterSession); } - } - else if (this.selection && this.selection.length) { - // remove all selections - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - changed = true; + + // forced clusters only open from screen size and double tap + if (force == true) { + // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); + parentNode.formationScale = 0; + } + else { + parentNode.formationScale = this.scale; // The latest child has been added on this scale } - this.selection = []; - } - if (changed && (triggerSelect == true || triggerSelect == undefined)) { - // fire the select event - this._trigger('select'); - } + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); - return changed; -}; + // set the pop-out scale for the childnode + parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; -/** - * select all nodes on given location x, y - * @param {Array} selection an array with node ids - * @param {boolean} append If true, the new selection will be appended to the - * current selection (except for duplicate entries) - * @return {Boolean} changed True if the selection is changed - * @private - */ -Graph.prototype._selectNodes = function(selection, append) { - var changed = false; - var i, iMax; + // nullify the movement velocity of the child, this is to avoid hectic behaviour + childNode.clearVelocity(); - // TODO: the selectNodes method is a little messy, rework this + // the mass has altered, preservation of energy dictates the velocity to be updated + parentNode.updateVelocity(massBefore); - // check if the current selection equals the desired selection - var selectionAlreadyThere = true; - if (selection.length != this.selection.length) { - selectionAlreadyThere = false; - } - else { - for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { - if (selection[i] != this.selection[i]) { - selectionAlreadyThere = false; - break; - } - } - } - if (selectionAlreadyThere) { - return changed; - } + // restart the simulation to reorganise all nodes + this.moving = true; + }, - if (append == undefined || append == false) { - // first deselect any selected node - var triggerSelect = false; - changed = this._unselectNodes(undefined, triggerSelect); - } - for (i = 0, iMax = selection.length; i < iMax; i++) { - // add each of the new selections, but only when they are not duplicate - var id = selection[i]; - var isDuplicate = (this.selection.indexOf(id) != -1); - if (!isDuplicate) { - this.nodes[id].select(); - this.selection.push(id); - changed = true; + /** + * This function will apply the changes made to the remainingEdges during the formation of the clusters. + * This is a seperate function to allow for level-wise collapsing of the node tree. + * It has to be called if a level is collapsed. It is called by _formClusters(). + * @private + */ + _updateDynamicEdges : function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + node.dynamicEdgesLength = node.dynamicEdges.length; + + // this corrects for multiple edges pointing at the same other node + var correction = 0; + if (node.dynamicEdgesLength > 1) { + for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { + var edgeToId = node.dynamicEdges[j].toId; + var edgeFromId = node.dynamicEdges[j].fromId; + for (var k = j+1; k < node.dynamicEdgesLength; k++) { + if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || + (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { + correction += 1; + } + } + } + } + node.dynamicEdgesLength -= correction; } - } + }, - if (changed) { - // fire the select event - this._trigger('select'); - } - return changed; -}; + /** + * This adds an edge from the childNode to the contained edges of the parent node + * + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object + * @private + */ + _addToContainedEdges : function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { + parentNode.containedEdges[childNode.id] = [] + } + // add this edge to the list + parentNode.containedEdges[childNode.id].push(edge); -/** - * retrieve all nodes overlapping with given object - * @param {Object} obj An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ -Graph.prototype._getNodesOverlappingWith = function (obj) { - var nodes = this.nodes, - overlappingNodes = []; + // remove the edge from the global edges object + delete this.edges[edge.id]; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].isOverlappingWith(obj)) { - overlappingNodes.push(id); + // remove the edge from the parent object + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + if (parentNode.dynamicEdges[i].id == edge.id) { + parentNode.dynamicEdges.splice(i,1); + break; } } - } + }, - return overlappingNodes; -}; + /** + * This function connects an edge that was connected to a child node to the parent node. + * It keeps track of which nodes it has been connected to with the originalId array. + * + * @param {Node} parentNode | Node object + * @param {Node} childNode | Node object + * @param {Edge} edge | Edge object + * @private + */ + _connectEdgeToCluster : function(parentNode, childNode, edge) { + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); + } + else { + if (edge.toId == childNode.id) { // edge connected to other node on the "to" side + edge.originalToId.push(childNode.id); + edge.to = parentNode; + edge.toId = parentNode.id; + } + else { // edge connected to other node with the "from" side -/** - * retrieve the currently selected nodes - * @return {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ -Graph.prototype.getSelection = function() { - return this.selection.concat([]); -}; + edge.originalFromId.push(childNode.id); + edge.from = parentNode; + edge.fromId = parentNode.id; + } -/** - * select zero or more nodes - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ -Graph.prototype.setSelection = function(selection) { - var i, iMax, id; + this._addToReroutedEdges(parentNode,childNode,edge); + } + }, - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; - // first unselect any selected node - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - } + _containCircularEdgesFromNode : function(parentNode, childNode) { + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); + } + } + }, - this.selection = []; - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; + /** + * This adds an edge from the childNode to the rerouted edges of the parent node + * + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object + * @private + */ + _addToReroutedEdges : function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + // we store the edge in the rerouted edges so we can restore it when the cluster pops open + if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { + parentNode.reroutedEdges[childNode.id] = []; + } + parentNode.reroutedEdges[childNode.id].push(edge); + + // this edge becomes part of the dynamicEdges of the cluster node + parentNode.dynamicEdges.push(edge); + }, - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); + + + /** + * This function connects an edge that was connected to a cluster node back to the child node. + * + * @param parentNode | Node object + * @param childNode | Node object + * @private + */ + _connectEdgeBackToChild : function(parentNode, childNode) { + if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { + for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { + var edge = parentNode.reroutedEdges[childNode.id][i]; + if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) { + edge.originalFromId.pop(); + edge.fromId = childNode.id; + edge.from = childNode; + } + else { + edge.originalToId.pop(); + edge.toId = childNode.id; + edge.to = childNode; + } + + // append this edge to the list of edges connecting to the childnode + childNode.dynamicEdges.push(edge); + + // remove the edge from the parent object + for (var j = 0; j < parentNode.dynamicEdges.length; j++) { + if (parentNode.dynamicEdges[j].id == edge.id) { + parentNode.dynamicEdges.splice(j,1); + break; + } + } + } + // remove the entry from the rerouted edges + delete parentNode.reroutedEdges[childNode.id]; } - node.select(); - this.selection.push(id); - } + }, - this.redraw(); -}; -/** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ -Graph.prototype._updateSelection = function () { - var i = 0; - while (i < this.selection.length) { - var id = this.selection[i]; - if (!this.nodes[id]) { - this.selection.splice(i, 1); + /** + * When loops are clustered, an edge can be both in the rerouted array and the contained array. + * This function is called last to verify that all edges in dynamicEdges are in fact connected to the + * parentNode + * + * @param parentNode | Node object + * @private + */ + _validateEdges : function(parentNode) { + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { + parentNode.dynamicEdges.splice(i,1); + } } - else { - i++; + }, + + + /** + * This function released the contained edges back into the global domain and puts them back into the + * dynamic edges of both parent and child. + * + * @param {Node} parentNode | + * @param {Node} childNode | + * @private + */ + _releaseContainedEdges : function(parentNode, childNode) { + for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) { + var edge = parentNode.containedEdges[childNode.id][i]; + + // put the edge back in the global edges object + this.edges[edge.id] = edge; + + // put the edge back in the dynamic edges of the child and parent + childNode.dynamicEdges.push(edge); + parentNode.dynamicEdges.push(edge); } - } -}; + // remove the entry from the contained edges + delete parentNode.containedEdges[childNode.id]; -/** - * Temporary method to test calculating a hub value for the nodes - * @param {number} level Maximum number edges between two nodes in order - * to call them connected. Optional, 1 by default - * @return {Number[]} connectioncount array with the connection count - * for each node - * @private - */ -Graph.prototype._getConnectionCount = function(level) { - if (level == undefined) { - level = 1; - } + }, - // get the nodes connected to given nodes - function getConnectedNodes(nodes) { - var connectedNodes = []; - for (var j = 0, jMax = nodes.length; j < jMax; j++) { - var node = nodes[j]; - // find all nodes connected to this node - var edges = node.edges; - for (var i = 0, iMax = edges.length; i < iMax; i++) { - var edge = edges[i]; - var other = null; - // check if connected - if (edge.from == node) - other = edge.to; - else if (edge.to == node) - other = edge.from; + // ------------------- UTILITY FUNCTIONS ---------------------------- // - // check if the other node is not already in the list with nodes - var k, kMax; - if (other) { - for (k = 0, kMax = nodes.length; k < kMax; k++) { - if (nodes[k] == other) { - other = null; - break; - } - } + + /** + * This updates the node labels for all nodes (for debugging purposes) + */ + updateLabels : function() { + var nodeId; + // update node labels + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.clusterSize > 1) { + node.label = "[".concat(String(node.clusterSize),"]"); } - if (other) { - for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { - if (connectedNodes[k] == other) { - other = null; - break; - } + } + } + + // update node labels + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.clusterSize == 1) { + if (node.originalLabel !== undefined) { + node.label = node.originalLabel; + } + else { + node.label = String(node.id); } } - - if (other) - connectedNodes.push(other); } } - return connectedNodes; - } + /* Debug Override */ + // for (nodeId in this.nodes) { + // if (this.nodes.hasOwnProperty(nodeId)) { + // node = this.nodes[nodeId]; + // node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale)); + // } + // } - var connections = []; - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - var c = [nodes[id]]; - for (var l = 0; l < level; l++) { - c = c.concat(getConnectedNodes(c)); - } - connections.push(c); - } - } + }, - var hubs = []; - for (var i = 0, len = connections.length; i < len; i++) { - hubs.push(connections[i].length); - } - return hubs; -}; + /** + * This function determines if the cluster we want to decluster is in the active area + * this means around the zoom center + * + * @param {Node} node + * @returns {boolean} + * @private + */ + _nodeInActiveArea : function(node) { + return ( + Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale + && + Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale + ) + }, -/** - * 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%') - */ -Graph.prototype.setSize = function(width, height) { - this.frame.style.width = width; - this.frame.style.height = height; + /** + * This is an adaptation of the original repositioning function. This is called if the system is clustered initially + * It puts large clusters away from the center and randomizes the order. + * + */ + repositionNodes : function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + if (!node.isFixed()) { + var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize); + var angle = 2 * Math.PI * Math.random(); + node.x = radius * Math.cos(angle); + node.y = radius * Math.sin(angle); + } + } + }, - this.frame.canvas.style.width = '100%'; - this.frame.canvas.style.height = '100%'; - this.frame.canvas.width = this.frame.canvas.clientWidth; - this.frame.canvas.height = this.frame.canvas.clientHeight; -}; -/** - * Set a data set with nodes for the graph - * @param {Array | DataSet | DataView} nodes The data containing the nodes. - * @private - */ -Graph.prototype._setNodes = function(nodes) { - var oldNodesData = this.nodesData; - if (nodes instanceof DataSet || nodes instanceof DataView) { - this.nodesData = nodes; - } - else if (nodes instanceof Array) { - this.nodesData = new DataSet(); - this.nodesData.add(nodes); - } - else if (!nodes) { - this.nodesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); - } - if (oldNodesData) { - // unsubscribe from old dataset - util.forEach(this.nodesListeners, function (callback, event) { - oldNodesData.unsubscribe(event, callback); - }); - } + /** + * 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 + */ + _getHubSize : function() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; + + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + if (node.dynamicEdgesLength > largestHub) { + largestHub = node.dynamicEdgesLength; + } + average += node.dynamicEdgesLength; + averageSquared += Math.pow(node.dynamicEdgesLength,2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; - // remove drawn nodes - this.nodes = {}; + var variance = averageSquared - Math.pow(average,2); - if (this.nodesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.nodesListeners, function (callback, event) { - me.nodesData.subscribe(event, callback); - }); + var standardDeviation = Math.sqrt(variance); - // draw all new nodes - var ids = this.nodesData.getIds(); - this._addNodes(ids); - } + this.hubThreshold = Math.floor(average + 2*standardDeviation); - this._updateSelection(); -}; + // always have at least one to cluster + if (this.hubThreshold > largestHub) { + this.hubThreshold = largestHub; + } -/** - * Add nodes - * @param {Number[] | String[]} ids - * @private - */ -Graph.prototype._addNodes = function(ids) { - var id; - for (var i = 0, len = ids.length; i < len; i++) { - id = ids[i]; - var data = this.nodesData.get(id); - var node = new Node(data, this.images, this.groups, this.constants); - this.nodes[id] = node; // note: this may replace an existing node + // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); + // console.log("hubThreshold:",this.hubThreshold); + }, - if (!node.isFixed()) { - // TODO: position new nodes in a smarter way! - var radius = this.constants.edges.length * 2; - var count = ids.length; - var angle = 2 * Math.PI * (i / count); - node.x = radius * Math.cos(angle); - node.y = radius * Math.sin(angle); - // note: no not use node.isMoving() here, as that gives the current - // velocity of the node, which is zero after creation of the node. - this.moving = true; + /** + * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. + * + * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce + * @private + */ + _reduceAmountOfChains : function(fraction) { + this.hubThreshold = 2; + var reduceAmount = Math.floor(this.nodeIndices.length * fraction); + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + if (reduceAmount > 0) { + this._formClusterFromHub(this.nodes[nodeId],true,true,1); + reduceAmount -= 1; + } + } + } } - } + }, - this._reconnectEdges(); - this._updateValueRange(this.nodes); -}; - -/** - * Update existing nodes, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ -Graph.prototype._updateNodes = function(ids) { - var nodes = this.nodes, - nodesData = this.nodesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var node = nodes[id]; - var data = nodesData.get(id); - if (node) { - // update node - node.setProperties(data, this.constants); - } - else { - // create node - node = new Node(properties, this.images, this.groups, this.constants); - nodes[id] = node; - - if (!node.isFixed()) { - this.moving = true; + /** + * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. + * + * @private + */ + _getChainFraction : function() { + var chains = 0; + var total = 0; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + chains += 1; + } + total += 1; } } - } - - this._reconnectEdges(); - this._updateValueRange(nodes); + return chains/total; + } }; -/** - * Remove existing nodes. If nodes do not exist, the method will just ignore it. - * @param {Number[] | String[]} ids - * @private - */ -Graph.prototype._removeNodes = function(ids) { - var nodes = this.nodes; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - delete nodes[id]; - } - - this._reconnectEdges(); - this._updateSelection(); - this._updateValueRange(nodes); -}; -/** - * Load edges by reading the data table - * @param {Array | DataSet | DataView} edges The data containing the edges. - * @private - * @private - */ -Graph.prototype._setEdges = function(edges) { - var oldEdgesData = this.edgesData; +var SelectionMixin = { - if (edges instanceof DataSet || edges instanceof DataView) { - this.edgesData = edges; - } - else if (edges instanceof Array) { - this.edgesData = new DataSet(); - this.edgesData.add(edges); - } - else if (!edges) { - this.edgesData = new DataSet(); - } - else { - throw new TypeError('Array or DataSet expected'); - } + /** + * This function can be called from the _doInAllSectors function + * + * @param object + * @param overlappingNodes + * @private + */ + _getNodesOverlappingWith : function(object, overlappingNodes) { + var nodes = this.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + } + }, - if (oldEdgesData) { - // unsubscribe from old dataset - util.forEach(this.edgesListeners, function (callback, event) { - oldEdgesData.unsubscribe(event, callback); - }); - } + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + _getAllNodesOverlappingWith : function (object) { + var overlappingNodes = []; + this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); + return overlappingNodes; + }, - // remove drawn edges - this.edges = {}; - if (this.edgesData) { - // subscribe to new dataset - var me = this; - util.forEach(this.edgesListeners, function (callback, event) { - me.edgesData.subscribe(event, callback); - }); + /** + * retrieve all nodes in the navigation controls 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 + */ + _getAllNavigationNodesOverlappingWith : function (object) { + var overlappingNodes = []; + this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes); + return overlappingNodes; + }, - // draw all new nodes - var ids = this.edgesData.getIds(); - this._addEdges(ids); - } + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + _pointerToPositionObject : function(pointer) { + var x = this._canvasToX(pointer.x); + var y = this._canvasToY(pointer.y); + + return {left: x, + top: y, + right: x, + bottom: y}; + }, - this._reconnectEdges(); -}; + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + _pointerToScreenPositionObject : function(pointer) { + var x = pointer.x; + var y = pointer.y; + + return {left: x, + top: y, + right: x, + bottom: y}; + }, -/** - * Add edges - * @param {Number[] | String[]} ids - * @private - */ -Graph.prototype._addEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var oldEdge = edges[id]; - if (oldEdge) { - oldEdge.disconnect(); + /** + * Get the top navigation controls node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node + * @private + */ + _getNavigationNodeAt : function (pointer) { + var screenPositionObject = this._pointerToScreenPositionObject(pointer); + var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject); + if (overlappingNodes.length > 0) { + return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]]; } + else { + return null; + } + }, - var data = edgesData.get(id); - edges[id] = new Edge(data, this, this.constants); - } - - this.moving = true; - this._updateValueRange(edges); -}; -/** - * Update existing edges, or create them when not yet existing - * @param {Number[] | String[]} ids - * @private - */ -Graph.prototype._updateEdges = function (ids) { - var edges = this.edges, - edgesData = this.edgesData; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; + /** + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node + * @private + */ + _getNodeAt : function (pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + overlappingNodes = this._getAllNodesOverlappingWith(positionObject); - var data = edgesData.get(id); - var edge = edges[id]; - if (edge) { - // update edge - edge.disconnect(); - edge.setProperties(data, this.constants); - edge.connect(); + // 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.nodes[overlappingNodes[overlappingNodes.length - 1]]; } else { - // create edge - edge = new Edge(data, this, this.constants); - this.edges[id] = edge; + return null; } - } + }, - this.moving = true; - this._updateValueRange(edges); -}; -/** - * Remove existing edges. Non existing ids will be ignored - * @param {Number[] | String[]} ids - * @private - */ -Graph.prototype._removeEdges = function (ids) { - var edges = this.edges; - for (var i = 0, len = ids.length; i < len; i++) { - var id = ids[i]; - var edge = edges[id]; - if (edge) { - edge.disconnect(); - delete edges[id]; - } - } + /** + * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call + * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {null} + * @private + */ + _getEdgeAt : function(pointer) { + return null; + }, - this.moving = true; - this._updateValueRange(edges); -}; -/** - * Reconnect all edges - * @private - */ -Graph.prototype._reconnectEdges = function() { - var id, - nodes = this.nodes, - edges = this.edges; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].edges = []; - } - } + /** + * Add object to the selection array. The this.selection id array may not be needed. + * + * @param obj + * @private + */ + _addToSelection : function(obj) { + this.selection.push(obj.id); + this.selectionObj[obj.id] = obj; + }, - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - edge.from = null; - edge.to = null; - edge.connect(); + + /** + * Remove a single option from selection. + * + * @param obj + * @private + */ + _removeFromSelection : function(obj) { + for (var i = 0; i < this.selection.length; i++) { + if (obj.id == this.selection[i]) { + this.selection.splice(i,1); + break; + } } - } -}; + delete this.selectionObj[obj.id]; + }, -/** - * 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 - */ -Graph.prototype._updateValueRange = function(obj) { - var id; - // determine the range of the objects - var valueMin = undefined; - var valueMax = undefined; - for (id in obj) { - if (obj.hasOwnProperty(id)) { - var value = obj[id].getValue(); - if (value !== undefined) { - valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); - valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); - } + /** + * Unselect all. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + _unselectAll : function(doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; } - } - // adjust the range of all objects - if (valueMin !== undefined && valueMax !== undefined) { - for (id in obj) { - if (obj.hasOwnProperty(id)) { - obj[id].setValueRange(valueMin, valueMax); + this.selection = []; + for (var objId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objId)) { + this.selectionObj[objId].unselect(); } } - } -}; + this.selectionObj = {}; -/** - * Redraw the graph with the current data - * chart will be resized too. - */ -Graph.prototype.redraw = function() { - this.setSize(this.width, this.height); - - this._redraw(); -}; - -/** - * Redraw the graph with the current data - * @private - */ -Graph.prototype._redraw = function() { - var ctx = this.frame.canvas.getContext('2d'); + if (doNotTrigger == false) { + this._trigger('select', { + nodes: this.getSelection() + }); + } + }, - // clear the canvas - var w = this.frame.canvas.width; - var h = this.frame.canvas.height; - ctx.clearRect(0, 0, w, h); - // set scaling and translation - ctx.save(); - ctx.translate(this.translation.x, this.translation.y); - ctx.scale(this.scale, this.scale); + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + _selectionIsEmpty : function() { + if (this.selection.length == 0) { + return true; + } + else { + return false; + } + }, - this._drawEdges(ctx); - this._drawNodes(ctx); - // restore original scaling and translation - ctx.restore(); -}; + /** + * 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} node + * @param {Boolean} append + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + _selectNode : function(node, append, doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } -/** - * Set the translation of the graph - * @param {Number} offsetX Horizontal offset - * @param {Number} offsetY Vertical offset - * @private - */ -Graph.prototype._setTranslation = function(offsetX, offsetY) { - if (this.translation === undefined) { - this.translation = { - x: 0, - y: 0 - }; - } + if (this._selectionIsEmpty() == false && append == false) { + this._unselectAll(true); + } - if (offsetX !== undefined) { - this.translation.x = offsetX; - } - if (offsetY !== undefined) { - this.translation.y = offsetY; - } -}; -/** - * Get the translation of the graph - * @return {Object} translation An object with parameters x and y, both a number - * @private - */ -Graph.prototype._getTranslation = function() { - return { - x: this.translation.x, - y: this.translation.y - }; -}; + if (node.selected == false) { + node.select(); + this._addToSelection(node); + } + else { + node.unselect(); + this._removeFromSelection(node); + } + if (doNotTrigger == false) { + this._trigger('select', { + nodes: this.getSelection() + }); + } + }, -/** - * Scale the graph - * @param {Number} scale Scaling factor 1.0 is unscaled - * @private - */ -Graph.prototype._setScale = function(scale) { - this.scale = scale; -}; -/** - * Get the current scale of the graph - * @return {Number} scale Scaling factor 1.0 is unscaled - * @private - */ -Graph.prototype._getScale = function() { - return this.scale; -}; -/** - * Convert a horizontal point on the HTML canvas to the x-value of the model - * @param {number} x - * @returns {number} - * @private - */ -Graph.prototype._canvasToX = function(x) { - return (x - this.translation.x) / this.scale; -}; + /** + * handles the selection part of the touch, only for navigation controls elements; + * Touch is triggered before tap, also before hold. Hold triggers after a while. + * This is the most responsive solution + * + * @param {Object} pointer + * @private + */ + _handleTouch : function(pointer) { + if (this.constants.navigation.enabled == true) { + var node = this._getNavigationNodeAt(pointer); + if (node != null) { + if (this[node.triggerFunction] !== undefined) { + this[node.triggerFunction](); + } + } + } + }, -/** - * Convert an x-value in the model to a horizontal point on the HTML canvas - * @param {number} x - * @returns {number} - * @private - */ -Graph.prototype._xToCanvas = function(x) { - return x * this.scale + this.translation.x; -}; -/** - * Convert a vertical point on the HTML canvas to the y-value of the model - * @param {number} y - * @returns {number} - * @private - */ -Graph.prototype._canvasToY = function(y) { - return (y - this.translation.y) / this.scale; -}; + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + _handleTap : function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectNode(node,false); + } + else { + this._unselectAll(); + } + this._redraw(); + }, -/** - * Convert an y-value in the model to a vertical point on the HTML canvas - * @param {number} y - * @returns {number} - * @private - */ -Graph.prototype._yToCanvas = function(y) { - return y * this.scale + this.translation.y ; -}; -/** - * Redraw all nodes - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private - */ -Graph.prototype._drawNodes = function(ctx) { - // first draw the unselected nodes - var nodes = this.nodes; - var selected = []; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].isSelected()) { - selected.push(id); - } - else { - nodes[id].draw(ctx); - } + /** + * handles the selection part of the double tap and opens a cluster if needed + * + * @param {Object} pointer + * @private + */ + _handleDoubleTap : function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = {"x" : this._canvasToX(pointer.x), + "y" : this._canvasToY(pointer.y)}; + this.openCluster(node); } - } + }, - // draw the selected nodes on top - for (var s = 0, sMax = selected.length; s < sMax; s++) { - nodes[selected[s]].draw(ctx); - } -}; -/** - * Redraw all edges - * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); - * @param {CanvasRenderingContext2D} ctx - * @private - */ -Graph.prototype._drawEdges = function(ctx) { - var edges = this.edges; - for (var id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected) { - edges[id].draw(ctx); - } + /** + * Handle the onHold selection part + * + * @param pointer + * @private + */ + _handleOnHold : function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectNode(node,true); } - } -}; + this._redraw(); + }, -/** - * Find a stable position for all nodes - * @private - */ -Graph.prototype._doStabilize = function() { - var start = new Date(); - // find stable position - var count = 0; - var vmin = this.constants.minVelocity; - var stable = false; - while (!stable && count < this.constants.maxIterations) { - this._calculateForces(); - this._discreteStepNodes(); - stable = !this._isMoving(vmin); - count++; - } + /** + * handle the onRelease event. These functions are here for the navigation controls module. + * + * @private + */ + _handleOnRelease : function() { + this.xIncrement = 0; + this.yIncrement = 0; + this.zoomIncrement = 0; + this._unHighlightAll(); + }, - var end = new Date(); - // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup -}; -/** - * Calculate the external forces acting on the nodes - * Forces are caused by: edges, repulsing forces between nodes, gravity - * @private - */ -Graph.prototype._calculateForces = function() { - // create a local edge to the nodes and edges, that is faster - var id, dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - nodes = this.nodes, - edges = this.edges; + /** + * + * retrieve the currently selected nodes + * @return {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + getSelection : function() { + return this.selection.concat([]); + }, - // gravity, add a small constant force to pull the nodes towards the center of - // the graph - // Also, the forces are reset to zero in this loop by using _setForce instead - // of _addForce - var gravity = 0.01, - gx = this.frame.canvas.clientWidth / 2, - gy = this.frame.canvas.clientHeight / 2; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - dx = gx - node.x; - dy = gy - node.y; - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; + /** + * + * retrieve the currently selected nodes as objects + * @return {Objects} selection An array with the ids of the + * selected nodes. + */ + getSelectionObjects : function() { + return this.selectionObj; + }, - node._setForce(fx, fy); - } - } + /** + * // TODO: rework this function, it is from the old system + * + * select zero or more nodes + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + setSelection : function(selection) { + var i, iMax, id; - // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; - for (var id1 in nodes) { - if (nodes.hasOwnProperty(id1)) { - var node1 = nodes[id1]; - for (var id2 in nodes) { - if (nodes.hasOwnProperty(id2)) { - var node2 = nodes[id2]; - // calculate normally distributed force - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); + // first unselect any selected node + this._unselectAll(true); - // TODO: correct factor for repulsing force - //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); - } + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); } + this._selectNode(node,true,true); } - } - /* TODO: re-implement repulsion of edges - for (var n = 0; n < nodes.length; n++) { - for (var l = 0; l < edges.length; l++) { - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + this.redraw(); + }, - // calculate normally distributed force - dx = nodes[n].x - lx, - dy = nodes[n].y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), + /** + * TODO: rework this function, it is from the old system + * + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ + _updateSelection : function () { + var i = 0; + while (i < this.selection.length) { + var nodeId = this.selection[i]; + if (!this.nodes.hasOwnProperty(nodeId)) { + this.selection.splice(i, 1); + delete this.selectionObj[nodeId]; + } + else { + i++; + } + } + } - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - nodes[n]._addForce(fx, fy); - edges[l].from._addForce(-fx/2,-fy/2); - edges[l].to._addForce(-fx/2,-fy/2); - } - } + + /** + * Unselect selected nodes. If no selection array is provided, all nodes + * are unselected + * @param {Object[]} selection Array with selection objects, each selection + * object has a parameter row. Optional + * @param {Boolean} triggerSelect If true (default), the select event + * is triggered when nodes are unselected + * @return {Boolean} changed True if the selection is changed + * @private */ + /* _unselectNodes : function(selection, triggerSelect) { + var changed = false; + var i, iMax, id; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } + var j = 0; + while (j < this.selection.length) { + if (this.selection[j] == id) { + this.selection.splice(j, 1); + changed = true; + } + else { + j++; + } + } + } + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } + changed = true; + } + this.selection = []; + } - // forces caused by the edges, modelled as springs - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; - if (edge.connected) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin - //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin - //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; - edgeLength = edge.length; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this._trigger('select', { + nodes: this.getSelection() + }); + } - springForce = edge.stiffness * (edgeLength - length); + return changed; + }, +*/ +/** + * select all nodes on given location x, y + * @param {Array} selection an array with node ids + * @param {boolean} append If true, the new selection will be appended to the + * current selection (except for duplicate entries) + * @return {Boolean} changed True if the selection is changed + * @private + */ +/* _selectNodes : function(selection, append) { + var changed = false; + var i, iMax; - fx = Math.cos(angle) * springForce; - fy = Math.sin(angle) * springForce; + // TODO: the selectNodes method is a little messy, rework this - edge.from._addForce(-fx, -fy); - edge.to._addForce(fx, fy); + // check if the current selection equals the desired selection + var selectionAlreadyThere = true; + if (selection.length != this.selection.length) { + selectionAlreadyThere = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i] != this.selection[i]) { + selectionAlreadyThere = false; + break; + } } } - } - - /* TODO: re-implement repulsion of edges - // repulsing forces between edges - var minimumDistance = this.constants.edges.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - for (var l = 0; l < edges.length; l++) { - //Keep distance from other edge centers - for (var l2 = l + 1; l2 < this.edges.length; l2++) { - //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin - //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin - //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, - l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, - l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + if (selectionAlreadyThere) { + return changed; + } - // calculate normally distributed force - dx = l2x - lx, - dy = l2y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var id = selection[i]; + var isDuplicate = (this.selection.indexOf(id) != -1); + if (!isDuplicate) { + this.nodes[id].select(); + this.selection.push(id); + changed = true; + } + } - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; + if (changed) { + // fire the select event + this._trigger('select', { + nodes: this.getSelection() + }); + } - edges[l].from._addForce(-fx, -fy); - edges[l].to._addForce(-fx, -fy); - edges[l2].from._addForce(fx, fy); - edges[l2].to._addForce(fx, fy); - } - } - */ + return changed; + }, + */ }; + + /** - * Check if any of the nodes is still moving - * @param {number} vmin the minimum velocity considered as 'moving' - * @return {boolean} true if moving, false if non of the nodes is moving - * @private + * Created by Alex on 1/22/14. */ -Graph.prototype._isMoving = function(vmin) { - // TODO: ismoving does not work well: should check the kinetic energy, not its velocity - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { - return true; + +var NavigationMixin = { + + /** + * This function moves the navigation controls if the canvas size has been changed. If the arugments + * verticaAlignTop and horizontalAlignLeft are false, the correction will be made + * + * @private + */ + _relocateNavigation : function() { + if (this.sectors !== undefined) { + var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth; + var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight; + this.navigationClientWidth = this.frame.canvas.clientWidth; + this.navigationClientHeight = this.frame.canvas.clientHeight; + var node = null; + + for (var nodeId in this.sectors["navigation"]["nodes"]) { + if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) { + node = this.sectors["navigation"]["nodes"][nodeId]; + if (!node.horizontalAlignLeft) { + node.x -= xOffset; + } + if (!node.verticalAlignTop) { + node.y -= yOffset; + } + } + } } - } - return false; -}; + }, -/** - * Perform one discrete step for all nodes - * @private - */ -Graph.prototype._discreteStepNodes = function() { - var interval = this.refreshRate / 1000.0; // in seconds - var nodes = this.nodes; - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - nodes[id].discreteStep(interval); + /** + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * + * @private + */ + _loadNavigationElements : function() { + var DIR = this.constants.navigation.iconPath; + this.navigationClientWidth = this.frame.canvas.clientWidth; + this.navigationClientHeight = this.frame.canvas.clientHeight; + if (this.navigationClientWidth === undefined) { + this.navigationClientWidth = 0; + this.navigationClientHeight = 0; + } + var offset = 15; + var intermediateOffset = 7; + var navigationNodes = [ + {id: 'navigation_up', shape: 'image', image: DIR + '/uparrow.png', triggerFunction: "_moveUp", + verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}, + {id: 'navigation_down', shape: 'image', image: DIR + '/downarrow.png', triggerFunction: "_moveDown", + verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_left', shape: 'image', image: DIR + '/leftarrow.png', triggerFunction: "_moveLeft", + verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_right', shape: 'image', image: DIR + '/rightarrow.png',triggerFunction: "_moveRight", + verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset}, + + {id: 'navigation_plus', shape: 'image', image: DIR + '/plus.png', triggerFunction: "_zoomIn", + verticalAlignTop: false, horizontalAlignLeft: false, + x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_min', shape: 'image', image: DIR + '/minus.png', triggerFunction: "_zoomOut", + verticalAlignTop: false, horizontalAlignLeft: false, + x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_zoomExtends', shape: 'image', image: DIR + '/zoomExtends.png', triggerFunction: "zoomToFit", + verticalAlignTop: false, horizontalAlignLeft: false, + x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset} + ]; + + var nodeObj = null; + for (var i = 0; i < navigationNodes.length; i++) { + nodeObj = this.sectors["navigation"]['nodes']; + nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[i], this.images, this.groups, this.constants); } - } -}; + }, -/** - * Start animating nodes and edges - */ -Graph.prototype.start = function() { - if (this.moving) { - this._calculateForces(); - this._discreteStepNodes(); - var vmin = this.constants.minVelocity; - this.moving = this._isMoving(vmin); - } + /** + * By setting the clustersize to be larger than 1, we use the clustering drawing method + * to illustrate the buttons are presed. We call this highlighting. + * + * @param {String} elementId + * @private + */ + _highlightNavigationElement : function(elementId) { + if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { + this.sectors["navigation"]["nodes"][elementId].clusterSize = 2; + } + }, - if (this.moving) { - // start animation. only start timer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - graph.start(); - graph._redraw(); - }, this.refreshRate); + + /** + * Reverting back to a normal button + * + * @param {String} elementId + * @private + */ + _unHighlightNavigationElement : function(elementId) { + if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { + this.sectors["navigation"]["nodes"][elementId].clusterSize = 1; } - } - else { - this._redraw(); - } -}; + }, -/** - * Stop animating nodes and edges. - */ -Graph.prototype.stop = function () { - if (this.timer) { - window.clearInterval(this.timer); - this.timer = undefined; - } -}; + /** + * un-highlight (for lack of a better term) all navigation controls elements + * @private + */ + _unHighlightAll : function() { + for (var nodeId in this.sectors['navigation']['nodes']) { + if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) { + this._unHighlightNavigationElement(nodeId); + } + } + }, -/** - * vis.js module exports - */ -var vis = { - util: util, - events: events, - Controller: Controller, - DataSet: DataSet, - DataView: DataView, - Range: Range, - Stack: Stack, - TimeStep: TimeStep, - EventBus: EventBus, + _preventDefault : function(event) { + if (event !== undefined) { + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + }, - components: { - items: { - Item: Item, - ItemBox: ItemBox, - ItemPoint: ItemPoint, - ItemRange: ItemRange - }, - Component: Component, - Panel: Panel, - RootPanel: RootPanel, - ItemSet: ItemSet, - TimeAxis: TimeAxis + /** + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. + * + * @private + */ + _moveUp : function(event) { + this._highlightNavigationElement("navigation_up"); + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); }, - graph: { - Node: Node, - Edge: Edge, - Popup: Popup, - Groups: Groups, - Images: Images + + /** + * move the screen down + * @private + */ + _moveDown : function(event) { + this._highlightNavigationElement("navigation_down"); + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); }, - Timeline: Timeline, - Graph: Graph -}; -/** - * CommonJS module exports - */ -if (typeof exports !== 'undefined') { - exports = vis; -} -if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = vis; -} + /** + * move the screen left + * @private + */ + _moveLeft : function(event) { + this._highlightNavigationElement("navigation_left"); + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, -/** - * AMD module exports - */ -if (typeof(define) === 'function') { - define(function () { - return vis; - }); -} -/** - * Window exports - */ -if (typeof window !== 'undefined') { - // attach the module to the window, load as a regular javascript file - window['vis'] = vis; -} + /** + * move the screen right + * @private + */ + _moveRight : function(event) { + this._highlightNavigationElement("navigation_right"); + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, -},{"hammerjs":2,"moment":3}],2:[function(require,module,exports){ -/*! Hammer.JS - v1.0.5 - 2013-04-07 - * http://eightmedia.github.com/hammer.js - * - * Copyright (c) 2013 Jorik Tangelder ; - * Licensed under the MIT license */ + /** + * Zoom in, using the same method as the movement. + * @private + */ + _zoomIn : function(event) { + this._highlightNavigationElement("navigation_plus"); + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, -(function(window, undefined) { - 'use strict'; -/** - * Hammer - * use this to create instances - * @param {HTMLElement} element - * @param {Object} options - * @returns {Hammer.Instance} - * @constructor - */ -var Hammer = function(element, options) { - return new Hammer.Instance(element, options || {}); -}; + /** + * Zoom out + * @private + */ + _zoomOut : function() { + this._highlightNavigationElement("navigation_min"); + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, -// default settings -Hammer.defaults = { - // add styles and attributes to the element to prevent the browser from doing - // its native behavior. this doesnt prevent the scrolling, but cancels - // the contextmenu, tap highlighting etc - // set to false to disable this - stop_browser_behavior: { - // this also triggers onselectstart=false for IE - userSelect: 'none', - // this makes the element blocking in IE10 >, you could experiment with the value - // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 - touchAction: 'none', - touchCallout: 'none', - contentZooming: 'none', - userDrag: 'none', - tapHighlightColor: 'rgba(0,0,0,0)' - } - // more settings are defined per gesture at gestures.js -}; + /** + * Stop zooming and unhighlight the zoom controls + * @private + */ + _stopZoom : function() { + this._unHighlightNavigationElement("navigation_plus"); + this._unHighlightNavigationElement("navigation_min"); -// detect touchevents -Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; -Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); + this.zoomIncrement = 0; + }, -// dont use mouseevents on mobile devices -Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; -Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX); -// eventtypes per touchevent (start, move, end) -// are filled by Hammer.event.determineEventTypes on setup -Hammer.EVENT_TYPES = {}; + /** + * Stop moving in the Y direction and unHighlight the up and down + * @private + */ + _yStopMoving : function() { + this._unHighlightNavigationElement("navigation_up"); + this._unHighlightNavigationElement("navigation_down"); -// direction defines -Hammer.DIRECTION_DOWN = 'down'; -Hammer.DIRECTION_LEFT = 'left'; -Hammer.DIRECTION_UP = 'up'; -Hammer.DIRECTION_RIGHT = 'right'; + this.yIncrement = 0; + }, -// pointer type -Hammer.POINTER_MOUSE = 'mouse'; -Hammer.POINTER_TOUCH = 'touch'; -Hammer.POINTER_PEN = 'pen'; -// touch event defines -Hammer.EVENT_START = 'start'; -Hammer.EVENT_MOVE = 'move'; -Hammer.EVENT_END = 'end'; + /** + * Stop moving in the X direction and unHighlight left and right. + * @private + */ + _xStopMoving : function() { + this._unHighlightNavigationElement("navigation_left"); + this._unHighlightNavigationElement("navigation_right"); -// hammer document where the base events are added at -Hammer.DOCUMENT = document; + this.xIncrement = 0; + } -// plugins namespace -Hammer.plugins = {}; -// if the window events are set... -Hammer.READY = false; +}; /** - * setup events to detect gestures on the document + * @constructor Graph + * Create a graph visualization, displaying nodes and edges. + * + * @param {Element} container The DOM element in which the Graph will + * be created. Normally a div element. + * @param {Object} data An object containing parameters + * {Array} nodes + * {Array} edges + * @param {Object} options Options */ -function setup() { - if(Hammer.READY) { - return; - } +function Graph (container, data, options) { + // create variables and set default values + this.containerElement = container; + this.width = '100%'; + this.height = '100%'; + // to give everything a nice fluidity, we seperate the rendering and calculating of the forces + this.renderRefreshRate = 60; // hz (fps) + this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on + this.stabilize = true; // stabilize before displaying the graph + this.selectable = true; - // find what eventtypes we add listeners to - Hammer.event.determineEventTypes(); + this.forceFactor = 50000; - // Register all gestures inside Hammer.gestures - for(var name in Hammer.gestures) { - if(Hammer.gestures.hasOwnProperty(name)) { - Hammer.detection.register(Hammer.gestures[name]); + // set constant values + this.constants = { + nodes: { + radiusMin: 5, + radiusMax: 20, + radius: 5, + distance: 100, // px + shape: 'ellipse', + image: undefined, + widthMin: 16, // px + widthMax: 64, // px + fontColor: 'black', + fontSize: 14, // px + //fontFace: verdana, + fontFace: 'arial', + color: { + border: '#2B7CE9', + background: '#97C2FC', + highlight: { + border: '#2B7CE9', + background: '#D2E5FF' } - } - - // Add touch events on the document - Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect); - Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect); + }, + borderColor: '#2B7CE9', + backgroundColor: '#97C2FC', + highlightColor: '#D2E5FF', + group: undefined + }, + edges: { + widthMin: 1, + widthMax: 15, + width: 1, + style: 'line', + color: '#343434', + fontColor: '#343434', + fontSize: 14, // px + fontFace: 'arial', + //distance: 100, //px + length: 100, // px + dash: { + length: 10, + gap: 5, + altLength: undefined + } + }, + clustering: { // Per Node in Cluster = PNiC + enabled: false, // (Boolean) | global on/off switch for clustering. + initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold. + clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes + reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this + chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains). + clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered. + sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. + screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. + fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). + forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). + distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). + edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. + nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster. + height: 10, // (px PNiC) | growth of the height per node in cluster. + radius: 10}, // (px PNiC) | growth of the radius per node in cluster. + activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open. + massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass + }, + navigation: { + enabled: false, + iconPath: this._getScriptPath() + '/img' + }, + keyboard: { + enabled: false, + speed: {x: 10, y: 10, zoom: 0.02} + }, + minVelocity: 2, // px/s + maxIterations: 1000 // maximum number of iteration to stabilize + }; - // Hammer is ready...! - Hammer.READY = true; -} + // Node variables + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function () { + graph._redraw(); + }); -/** - * create new hammer instance - * all methods should return the instance itself, so it is chainable. - * @param {HTMLElement} element - * @param {Object} [options={}] - * @returns {Hammer.Instance} - * @constructor - */ -Hammer.Instance = function(element, options) { - var self = this; + // navigation variables + this.xIncrement = 0; + this.yIncrement = 0; + this.zoomIncrement = 0; - // setup HammerJS window events and register all gestures - // this also sets up the default options - setup(); + // create a frame and canvas + this._create(); - this.element = element; + // load the sector system. (mandatory, fully integrated with Graph) + this._loadSectorSystem(); - // start/stop detection option - this.enabled = true; + // apply options + this.setOptions(options); - // merge options - this.options = Hammer.utils.extend( - Hammer.utils.extend({}, Hammer.defaults), - options || {}); + // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) + this._loadClusterSystem(); - // add some css to the element to prevent the browser from doing its native behavoir - if(this.options.stop_browser_behavior) { - Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); - } + // load the selection system. (mandatory, required by Graph) + this._loadSelectionSystem(); - // start detection on touchstart - Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) { - if(self.enabled) { - Hammer.detection.startDetect(self, ev); - } - }); + // other vars + var graph = this; + this.freezeSimulation = false;// freeze the simulation - // return instance - return this; -}; + this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation + this.nodes = {}; // object with Node objects + this.edges = {}; // object with Edge objects + this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. + this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw -Hammer.Instance.prototype = { - /** - * bind events to the instance - * @param {String} gesture - * @param {Function} handler - * @returns {Hammer.Instance} - */ - on: function onEvent(gesture, handler){ - var gestures = gesture.split(' '); - for(var t=0; t (node.x - node.width)) {minX = node.x - node.width;} + if (maxX < (node.x + node.width)) {maxX = node.x + node.width;} + if (minY > (node.y - node.height)) {minY = node.y - node.height;} + if (maxY < (node.y + node.height)) {maxY = node.y + node.height;} + } + return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; +}; /** - * when the mouse is hold down, this is true - * @type {Boolean} + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} + * @private */ -var enable_detect = false; +Graph.prototype._findCenter = function(range) { + var center = {x: (0.5 * (range.maxX + range.minX)), + y: (0.5 * (range.maxY + range.minY))}; + return center; +}; /** - * when touch events have been fired, this is true - * @type {Boolean} + * center the graph + * + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; */ -var touch_triggered = false; +Graph.prototype._centerGraph = function(range) { + var center = this._findCenter(range); + center.x *= this.scale; + center.y *= this.scale; + center.x -= 0.5 * this.frame.canvas.clientWidth; + center.y -= 0.5 * this.frame.canvas.clientHeight; -Hammer.event = { - /** - * simple addEventListener - * @param {HTMLElement} element - * @param {String} type - * @param {Function} handler - */ - bindDom: function(element, type, handler) { - var types = type.split(' '); - for(var t=0; t= this.constants.clustering.initialMaxNodes) { + var zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + else { + var zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + } + else { + var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1; + var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1; - // mousebutton must be down or a touch event - else if( sourceEventType.match(/touch/) || // touch events are always on screen - sourceEventType.match(/pointerdown/) || // pointerevents touch - (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed - ){ - enable_detect = true; - } + var xZoomLevel = this.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.frame.canvas.clientHeight / yDistance; - // we are in a touch event, set the touch triggered bool to true, - // this for the conflicts that may occur on ios and android - if(sourceEventType.match(/touch|pointer/)) { - touch_triggered = true; - } + zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; + } - // count the total touches on the screen - var count_touches = 0; + if (zoomLevel > 1.0) { + zoomLevel = 1.0; + } - // when touch has been triggered in this detection session - // and we are now handling a mouse event, we stop that to prevent conflicts - if(enable_detect) { - // update pointerevent - if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) { - count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); - } - // touch - else if(sourceEventType.match(/touch/)) { - count_touches = ev.touches.length; - } - // mouse - else if(!touch_triggered) { - count_touches = sourceEventType.match(/up/) ? 0 : 1; - } + this.pinch.mousewheelScale = zoomLevel; + this._setScale(zoomLevel); + this._centerGraph(range); + this.start(); +}; - // if we are in a end event, but when we remove one touch and - // we still have enough, set eventType to move - if(count_touches > 0 && eventType == Hammer.EVENT_END) { - eventType = Hammer.EVENT_MOVE; - } - // no touches, force the end event - else if(!count_touches) { - eventType = Hammer.EVENT_END; - } - // because touchend has no touches, and we often want to use these in our gestures, - // we send the last move event as our eventData in touchend - if(!count_touches && last_move_event !== null) { - ev = last_move_event; - } - // store the last move event - else { - last_move_event = ev; - } +/** + * Update the this.nodeIndices with the most recent node index list + * @private + */ +Graph.prototype._updateNodeIndexList = function() { + this._clearNodeIndexList(); + for (var idx in this.nodes) { + if (this.nodes.hasOwnProperty(idx)) { + this.nodeIndices.push(idx); + } + } +}; - // trigger the handler - handler.call(Hammer.detection, self.collectEventData(element, eventType, ev)); - // remove pointerevent from list - if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) { - count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); - } - } +/** + * 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 + * {Options} [options] Object with options + * @param {Boolean} [disableStart] | optional: disable the calling of the start function. + */ +Graph.prototype.setData = function(data, disableStart) { + if (disableStart === undefined) { + disableStart = false; + } - //debug(sourceEventType +" "+ eventType); + 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.'); + } - // on the end we reset everything - if(!count_touches) { - last_move_event = null; - enable_detect = false; - touch_triggered = false; - Hammer.PointerEvent.reset(); - } - }); - }, + // set options + this.setOptions(data && data.options); + + // set all data + if (data && data.dot) { + // parse DOT file + if(data && data.dot) { + var dotData = vis.util.DOTToGraph(data.dot); + this.setData(dotData); + return; + } + } + else { + this._setNodes(data && data.nodes); + this._setEdges(data && data.edges); + } + this._putDataInSector(); - /** - * we have different events for each device/browser - * determine what we need and set them in the Hammer.EVENT_TYPES constant - */ - determineEventTypes: function determineEventTypes() { - // determine the eventtype we want to set - var types; + if (!disableStart) { + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.moving = true; + this.start(); + } +}; - // pointerEvents magic - if(Hammer.HAS_POINTEREVENTS) { - types = Hammer.PointerEvent.getEvents(); +/** + * Set options + * @param {Object} options + */ +Graph.prototype.setOptions = function (options) { + if (options) { + // retrieve parameter values + if (options.width !== undefined) {this.width = options.width;} + if (options.height !== undefined) {this.height = options.height;} + if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} + if (options.selectable !== undefined) {this.selectable = options.selectable;} + + if (options.clustering) { + this.constants.clustering.enabled = true; + for (var prop in options.clustering) { + if (options.clustering.hasOwnProperty(prop)) { + this.constants.clustering[prop] = options.clustering[prop]; } - // on Android, iOS, blackberry, windows mobile we dont want any mouseevents - else if(Hammer.NO_MOUSEEVENTS) { - types = [ - 'touchstart', - 'touchmove', - 'touchend touchcancel']; + } + } + else if (options.clustering !== undefined) { + this.constants.clustering.enabled = false; + } + + if (options.navigation) { + this.constants.navigation.enabled = true; + for (var prop in options.navigation) { + if (options.navigation.hasOwnProperty(prop)) { + this.constants.navigation[prop] = options.navigation[prop]; } - // for non pointer events browsers and mixed browsers, - // like chrome on windows8 touch laptop - else { - types = [ - 'touchstart mousedown', - 'touchmove mousemove', - 'touchend touchcancel mouseup']; + } + } + else if (options.navigation !== undefined) { + this.constants.navigation.enabled = false; + } + + if (options.keyboard) { + this.constants.keyboard.enabled = true; + for (var prop in options.keyboard) { + if (options.keyboard.hasOwnProperty(prop)) { + this.constants.keyboard[prop] = options.keyboard[prop]; } + } + } + else if (options.keyboard !== undefined) { + this.constants.keyboard.enabled = false; + } - Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0]; - Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1]; - Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2]; - }, + // TODO: work out these options and document them + if (options.edges) { + for (prop in options.edges) { + if (options.edges.hasOwnProperty(prop)) { + this.constants.edges[prop] = options.edges[prop]; + } + } - /** - * create touchlist depending on the event - * @param {Object} ev - * @param {String} eventType used by the fakemultitouch plugin - */ - getTouchList: function getTouchList(ev/*, eventType*/) { - // get the fake pointerEvent touchlist - if(Hammer.HAS_POINTEREVENTS) { - return Hammer.PointerEvent.getTouchList(); + if (options.edges.length !== undefined && + options.nodes && options.nodes.distance === undefined) { + this.constants.edges.length = options.edges.length; + this.constants.nodes.distance = options.edges.length * 1.25; + } + + if (!options.edges.fontColor) { + this.constants.edges.fontColor = options.edges.color; + } + + // Added to support dashed lines + // David Jordan + // 2012-08-08 + if (options.edges.dash) { + if (options.edges.dash.length !== undefined) { + this.constants.edges.dash.length = options.edges.dash.length; } - // get the touchlist - else if(ev.touches) { - return ev.touches; + if (options.edges.dash.gap !== undefined) { + this.constants.edges.dash.gap = options.edges.dash.gap; } - // make fake touchlist from mouse position - else { - return [{ - identifier: 1, - pageX: ev.pageX, - pageY: ev.pageY, - target: ev.target - }]; + if (options.edges.dash.altLength !== undefined) { + this.constants.edges.dash.altLength = options.edges.dash.altLength; } - }, + } + } + if (options.nodes) { + for (prop in options.nodes) { + if (options.nodes.hasOwnProperty(prop)) { + this.constants.nodes[prop] = options.nodes[prop]; + } + } - /** - * collect event data for Hammer js - * @param {HTMLElement} element - * @param {String} eventType like Hammer.EVENT_MOVE - * @param {Object} eventData - */ - collectEventData: function collectEventData(element, eventType, ev) { - var touches = this.getTouchList(ev, eventType); + if (options.nodes.color) { + this.constants.nodes.color = Node.parseColor(options.nodes.color); + } - // find out pointerType - var pointerType = Hammer.POINTER_TOUCH; - if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) { - pointerType = Hammer.POINTER_MOUSE; + /* + if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin; + if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; + */ + } + if (options.groups) { + for (var groupname in options.groups) { + if (options.groups.hasOwnProperty(groupname)) { + var group = options.groups[groupname]; + this.groups.add(groupname, group); } + } + } + } - return { - center : Hammer.utils.getCenter(touches), - timeStamp : new Date().getTime(), - target : ev.target, - touches : touches, - eventType : eventType, - pointerType : pointerType, - srcEvent : ev, - - /** - * prevent the browser default actions - * mostly used to disable scrolling of the browser - */ - preventDefault: function() { - if(this.srcEvent.preventManipulation) { - this.srcEvent.preventManipulation(); - } + this.setSize(this.width, this.height); + this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); + this._setScale(1); - if(this.srcEvent.preventDefault) { - this.srcEvent.preventDefault(); - } - }, + // load the navigation system. + this._loadNavigationControls(); - /** - * stop bubbling the event up to its parents - */ - stopPropagation: function() { - this.srcEvent.stopPropagation(); - }, + // bind keys. If disabled, this will not do anything; + this._createKeyBinds(); - /** - * immediately stop gesture detection - * might be useful after a swipe was detected - * @return {*} - */ - stopDetect: function() { - return Hammer.detection.stopDetect(); - } - }; - } + this._redraw(); }; -Hammer.PointerEvent = { - /** - * holds all pointers - * @type {Object} - */ - pointers: {}, - - /** - * get a list of pointers - * @returns {Array} touchlist - */ - getTouchList: function() { - var self = this; - var touchlist = []; +/** + * Add event listener + * @param {String} event Event name. Available events: + * 'select' + * @param {function} callback Callback function, invoked as callback(properties) + * where properties is an optional object containing + * event specific properties. + */ +Graph.prototype.on = function on (event, callback) { + var available = ['select']; - // we can use forEach since pointerEvents only is in IE10 - Object.keys(self.pointers).sort().forEach(function(id) { - touchlist.push(self.pointers[id]); - }); - return touchlist; - }, + if (available.indexOf(event) == -1) { + throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); + } - /** - * update the position of a pointer - * @param {String} type Hammer.EVENT_END - * @param {Object} pointerEvent - */ - updatePointer: function(type, pointerEvent) { - if(type == Hammer.EVENT_END) { - this.pointers = {}; - } - else { - pointerEvent.identifier = pointerEvent.pointerId; - this.pointers[pointerEvent.pointerId] = pointerEvent; - } + events.addListener(this, event, callback); +}; - return Object.keys(this.pointers).length; - }, +/** + * Remove an event listener + * @param {String} event Event name + * @param {function} callback Callback function + */ +Graph.prototype.off = function off (event, callback) { + events.removeListener(this, event, callback); +}; - /** - * check if ev matches pointertype - * @param {String} pointerType Hammer.POINTER_MOUSE - * @param {PointerEvent} ev - */ - matchType: function(pointerType, ev) { - if(!ev.pointerType) { - return false; - } +/** + * fire an event + * @param {String} event The name of an event, for example 'select' + * @param {Object} params Optional object with event parameters + * @private + */ +Graph.prototype._trigger = function (event, params) { + events.trigger(this, event, params); +}; - var types = {}; - types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE); - types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH); - types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN); - return types[pointerType]; - }, +/** + * Create the main frame for the Graph. + * This function is executed once when a Graph object is created. The frame + * contains a canvas, and this canvas contains all objects like the axis and + * nodes. + * @private + */ +Graph.prototype._create = function () { + // remove all elements from the container element. + while (this.containerElement.hasChildNodes()) { + this.containerElement.removeChild(this.containerElement.firstChild); + } - /** - * get events - */ - getEvents: function() { - return [ - 'pointerdown MSPointerDown', - 'pointermove MSPointerMove', - 'pointerup pointercancel MSPointerUp MSPointerCancel' - ]; - }, + this.frame = document.createElement('div'); + this.frame.className = 'graph-frame'; + this.frame.style.position = 'relative'; + this.frame.style.overflow = 'hidden'; - /** - * reset the list - */ - reset: function() { - this.pointers = {}; - } -}; + // 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); + } + var me = this; + this.drag = {}; + this.pinch = {}; + this.hammer = Hammer(this.frame.canvas, { + prevent_default: true + }); + this.hammer.on('tap', me._onTap.bind(me) ); + this.hammer.on('doubletap', me._onDoubleTap.bind(me) ); + this.hammer.on('hold', me._onHold.bind(me) ); + this.hammer.on('pinch', me._onPinch.bind(me) ); + this.hammer.on('touch', me._onTouch.bind(me) ); + this.hammer.on('dragstart', me._onDragStart.bind(me) ); + this.hammer.on('drag', me._onDrag.bind(me) ); + this.hammer.on('dragend', me._onDragEnd.bind(me) ); + this.hammer.on('release', me._onRelease.bind(me) ); + this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); + this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF + this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); -Hammer.utils = { - /** - * extend method, - * also used for cloning when dest is an empty object - * @param {Object} dest - * @param {Object} src - * @parm {Boolean} merge do a merge - * @returns {Object} dest - */ - extend: function extend(dest, src, merge) { - for (var key in src) { - if(dest[key] !== undefined && merge) { - continue; - } - dest[key] = src[key]; - } - return dest; - }, + // add the frame to the container element + this.containerElement.appendChild(this.frame); +}; + + +/** + * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin + * @private + */ +Graph.prototype._createKeyBinds = function() { + var me = this; + this.mousetrap = mousetrap; + + this.mousetrap.reset(); + + if (this.constants.keyboard.enabled == true) { + this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown"); + this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup"); + this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown"); + this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup"); + this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown"); + this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup"); + this.mousetrap.bind("right",this._moveRight.bind(me), "keydown"); + this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup"); + this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown"); + this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown"); + this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown"); + this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown"); + this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown"); + this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); + this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); + } + /* + this.mousetrap.bind("=",this.decreaseClusterLevel.bind(me)); + this.mousetrap.bind("-",this.increaseClusterLevel.bind(me)); + this.mousetrap.bind("s",this.singleStep.bind(me)); + this.mousetrap.bind("h",this.updateClustersDefault.bind(me)); + this.mousetrap.bind("c",this._collapseSector.bind(me)); + this.mousetrap.bind("f",this.toggleFreeze.bind(me)); + */ +} + +/** + * Get the pointer location from a touch location + * @param {{pageX: Number, pageY: Number}} touch + * @return {{x: Number, y: Number}} pointer + * @private + */ +Graph.prototype._getPointer = function (touch) { + return { + x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas), + y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas) + }; +}; + +/** + * On start of a touch gesture, store the pointer + * @param event + * @private + */ +Graph.prototype._onTouch = function (event) { + this.drag.pointer = this._getPointer(event.gesture.touches[0]); + this.drag.pinched = false; + this.pinch.scale = this._getScale(); + + this._handleTouch(this.drag.pointer); +}; + +/** + * handle drag start event + * @private + */ +Graph.prototype._onDragStart = function () { + var drag = this.drag; + var node = this._getNodeAt(drag.pointer); + // note: drag.pointer is set in _onTouch to get the initial touch location + + drag.dragging = true; + drag.selection = []; + drag.translation = this._getTranslation(); + drag.nodeId = null; + + if (node != null) { + drag.nodeId = node.id; + // select the clicked node if not yet selected + if (!node.isSelected()) { + this._selectNode(node,false); + } + + // create an array with the selected nodes and their original location and status + var me = this; + this.selection.forEach(function (id) { + var node = me.nodes[id]; + if (node) { + var s = { + id: id, + node: node, + + // store original x, y, xFixed and yFixed, make the node temporarily Fixed + x: node.x, + y: node.y, + xFixed: node.xFixed, + yFixed: node.yFixed + }; + + node.xFixed = true; + node.yFixed = true; + + drag.selection.push(s); + } + }); + } +}; + +/** + * handle drag event + * @private + */ +Graph.prototype._onDrag = function (event) { + if (this.drag.pinched) { + return; + } + + var pointer = this._getPointer(event.gesture.touches[0]); + + var me = this, + drag = this.drag, + selection = drag.selection; + if (selection && selection.length) { + // calculate delta's and new location + var deltaX = pointer.x - drag.pointer.x, + deltaY = pointer.y - drag.pointer.y; + + // update position of all selected nodes + selection.forEach(function (s) { + var node = s.node; + + if (!s.xFixed) { + node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX); + } + + if (!s.yFixed) { + node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY); + } + }); + + // start animation if not yet running + if (!this.moving) { + this.moving = true; + this.start(); + } + } + else { + // move the graph + var diffX = pointer.x - this.drag.pointer.x; + var diffY = pointer.y - this.drag.pointer.y; + + this._setTranslation( + this.drag.translation.x + diffX, + this.drag.translation.y + diffY); + this._redraw(); + this.moved = true; + } +}; + +/** + * handle drag start event + * @private + */ +Graph.prototype._onDragEnd = function () { + this.drag.dragging = false; + var selection = this.drag.selection; + if (selection) { + selection.forEach(function (s) { + // restore original xFixed and yFixed + s.node.xFixed = s.xFixed; + s.node.yFixed = s.yFixed; + }); + } +}; + +/** + * handle tap/click event: select/unselect a node + * @private + */ +Graph.prototype._onTap = function (event) { + var pointer = this._getPointer(event.gesture.touches[0]); + this._handleTap(pointer); +}; + + +/** + * handle doubletap event + * @private + */ +Graph.prototype._onDoubleTap = function (event) { + var pointer = this._getPointer(event.gesture.touches[0]); + this._handleDoubleTap(pointer); + +}; + + +/** + * handle long tap event: multi select nodes + * @private + */ +Graph.prototype._onHold = function (event) { + var pointer = this._getPointer(event.gesture.touches[0]); + this._handleOnHold(pointer); +}; + +/** + * handle the release of the screen + * + * @param event + * @private + */ +Graph.prototype._onRelease = function (event) { + this._handleOnRelease(); +}; + +/** + * Handle pinch event + * @param event + * @private + */ +Graph.prototype._onPinch = function (event) { + var pointer = this._getPointer(event.gesture.center); + + this.drag.pinched = true; + if (!('scale' in this.pinch)) { + this.pinch.scale = 1; + } + + // TODO: enabled moving while pinching? + var scale = this.pinch.scale * event.gesture.scale; + this._zoom(scale, pointer) +}; + +/** + * Zoom the graph 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 + */ +Graph.prototype._zoom = function(scale, pointer) { + var scaleOld = this._getScale(); + if (scale < 0.00001) { + scale = 0.00001; + } + if (scale > 10) { + scale = 10; + } +// + this.frame.canvas.clientHeight / 2 + var translation = this._getTranslation(); + + var scaleFrac = scale / scaleOld; + var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; + var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + + this.areaCenter = {"x" : this._canvasToX(pointer.x), + "y" : this._canvasToY(pointer.y)}; + + // this.areaCenter = {"x" : pointer.x,"y" : pointer.y }; +// console.log(translation.x,translation.y,pointer.x,pointer.y,scale); + this.pinch.mousewheelScale = scale; + this._setScale(scale); + this._setTranslation(tx, ty); + this.updateClustersDefault(); + this._redraw(); + + return scale; +}; + +/** + * Event handler for mouse wheel event, used to zoom the timeline + * See http://adomas.org/javascript-mouse-wheel/ + * https://github.com/EightMedia/hammer.js/issues/256 + * @param {MouseEvent} event + * @private + */ +Graph.prototype._onMouseWheel = function(event) { + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta/120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail/3; + } + + // 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) { + if (!('mousewheelScale' in this.pinch)) { + this.pinch.mousewheelScale = 1; + } + + // calculate the new scale + var scale = this.pinch.mousewheelScale; + var zoom = delta / 10; + if (delta < 0) { + zoom = zoom / (1 - zoom); + } + scale *= (1 + zoom); + + // calculate the pointer location + var gesture = util.fakeGesture(this, event); + var pointer = this._getPointer(gesture.center); + + // apply the new scale + scale = this._zoom(scale, pointer); + + // store the new, applied scale -- this is now done in _zoom +// this.pinch.mousewheelScale = scale; + } + + // Prevent default actions caused by mouse wheel. + event.preventDefault(); +}; + + +/** + * Mouse move handler for checking whether the title moves over a node with a title. + * @param {Event} event + * @private + */ +Graph.prototype._onMouseMoveTitle = function (event) { + var gesture = util.fakeGesture(this, event); + var pointer = this._getPointer(gesture.center); + + // check if the previously selected node is still selected + if (this.popupNode) { + this._checkHidePopup(pointer); + } + + // start a timeout that will check if the mouse is positioned above + // an element + var me = this; + var checkShow = function() { + me._checkShowPopup(pointer); + }; + if (this.popupTimer) { + clearInterval(this.popupTimer); // stop any running calculationTimer + } + if (!this.drag.dragging) { + this.popupTimer = setTimeout(checkShow, 300); + } +}; + +/** + * Check if there is an element on the given position in the graph + * (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 + */ +Graph.prototype._checkShowPopup = function (pointer) { + var obj = { + left: this._canvasToX(pointer.x), + top: this._canvasToY(pointer.y), + right: this._canvasToX(pointer.x), + bottom: this._canvasToY(pointer.y) + }; + + var id; + var lastPopupNode = this.popupNode; + + if (this.popupNode == undefined) { + // search the nodes for overlap, select the top one in case of multiple nodes + var nodes = this.nodes; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) { + this.popupNode = node; + break; + } + } + } + } + + if (this.popupNode === undefined) { + // search the edges for overlap + var edges = this.edges; + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + if (edge.connected && (edge.getTitle() !== undefined) && + edge.isOverlappingWith(obj)) { + this.popupNode = edge; + break; + } + } + } + } + + if (this.popupNode) { + // show popup message window + if (this.popupNode != lastPopupNode) { + var me = this; + if (!me.popup) { + me.popup = new Popup(me.frame); + } + + // adjust a small offset such that the mouse cursor is located in the + // bottom left location of the popup, and you can easily move over the + // popup area + me.popup.setPosition(pointer.x - 3, pointer.y - 3); + me.popup.setText(me.popupNode.getTitle()); + me.popup.show(); + } + } + else { + if (this.popup) { + this.popup.hide(); + } + } +}; + +/** + * Check if the popup must be hided, which is the case when the mouse is no + * longer hovering on the object + * @param {{x:Number, y:Number}} pointer + * @private + */ +Graph.prototype._checkHidePopup = function (pointer) { + if (!this.popupNode || !this._getNodeAt(pointer) ) { + this.popupNode = undefined; + if (this.popup) { + this.popup.hide(); + } + } +}; + + +/** + * Temporary method to test calculating a hub value for the nodes + * @param {number} level Maximum number edges between two nodes in order + * to call them connected. Optional, 1 by default + * @return {Number[]} connectioncount array with the connection count + * for each node + * @private + */ +Graph.prototype._getConnectionCount = function(level) { + if (level == undefined) { + level = 1; + } + + // get the nodes connected to given nodes + function getConnectedNodes(nodes) { + var connectedNodes = []; + + for (var j = 0, jMax = nodes.length; j < jMax; j++) { + var node = nodes[j]; + + // find all nodes connected to this node + var edges = node.edges; + for (var i = 0, iMax = edges.length; i < iMax; i++) { + var edge = edges[i]; + var other = null; + + // check if connected + if (edge.from == node) + other = edge.to; + else if (edge.to == node) + other = edge.from; + + // check if the other node is not already in the list with nodes + var k, kMax; + if (other) { + for (k = 0, kMax = nodes.length; k < kMax; k++) { + if (nodes[k] == other) { + other = null; + break; + } + } + } + if (other) { + for (k = 0, kMax = connectedNodes.length; k < kMax; k++) { + if (connectedNodes[k] == other) { + other = null; + break; + } + } + } + + if (other) + connectedNodes.push(other); + } + } + + return connectedNodes; + } + + var connections = []; + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + var c = [nodes[id]]; + for (var l = 0; l < level; l++) { + c = c.concat(getConnectedNodes(c)); + } + connections.push(c); + } + } + + var hubs = []; + for (var i = 0, len = connections.length; i < len; i++) { + hubs.push(connections[i].length); + } + + return hubs; +}; + +/** + * 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%') + */ +Graph.prototype.setSize = function(width, height) { + this.frame.style.width = width; + this.frame.style.height = height; + + this.frame.canvas.style.width = '100%'; + this.frame.canvas.style.height = '100%'; + + this.frame.canvas.width = this.frame.canvas.clientWidth; + this.frame.canvas.height = this.frame.canvas.clientHeight; + + if (this.constants.navigation.enabled == true) { + this._relocateNavigation(); + } +}; + +/** + * Set a data set with nodes for the graph + * @param {Array | DataSet | DataView} nodes The data containing the nodes. + * @private + */ +Graph.prototype._setNodes = function(nodes) { + var oldNodesData = this.nodesData; + + if (nodes instanceof DataSet || nodes instanceof DataView) { + this.nodesData = nodes; + } + else if (nodes instanceof Array) { + this.nodesData = new DataSet(); + this.nodesData.add(nodes); + } + else if (!nodes) { + this.nodesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } + + if (oldNodesData) { + // unsubscribe from old dataset + util.forEach(this.nodesListeners, function (callback, event) { + oldNodesData.unsubscribe(event, callback); + }); + } + + // remove drawn nodes + this.nodes = {}; + + if (this.nodesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.nodesListeners, function (callback, event) { + me.nodesData.subscribe(event, callback); + }); + + // draw all new nodes + var ids = this.nodesData.getIds(); + this._addNodes(ids); + } + this._updateSelection(); +}; + +/** + * Add nodes + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._addNodes = function(ids) { + var id; + for (var i = 0, len = ids.length; i < len; i++) { + id = ids[i]; + var data = this.nodesData.get(id); + var node = new Node(data, this.images, this.groups, this.constants); + this.nodes[id] = node; // note: this may replace an existing node + + if (!node.isFixed()) { + // TODO: position new nodes in a smarter way! + var radius = this.constants.edges.length * 2; + var count = ids.length; + var angle = 2 * Math.PI * (i / count); + node.x = radius * Math.cos(angle); + node.y = radius * Math.sin(angle); + + // note: no not use node.isMoving() here, as that gives the current + // velocity of the node, which is zero after creation of the node. + this.moving = true; + } + } + this._updateNodeIndexList(); + this._reconnectEdges(); + this._updateValueRange(this.nodes); +}; + +/** + * Update existing nodes, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._updateNodes = function(ids) { + var nodes = this.nodes, + nodesData = this.nodesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var node = nodes[id]; + var data = nodesData.get(id); + if (node) { + // update node + node.setProperties(data, this.constants); + } + else { + // create node + node = new Node(properties, this.images, this.groups, this.constants); + nodes[id] = node; + + if (!node.isFixed()) { + this.moving = true; + } + } + } + this._updateNodeIndexList(); + this._reconnectEdges(); + this._updateValueRange(nodes); +}; + +/** + * Remove existing nodes. If nodes do not exist, the method will just ignore it. + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._removeNodes = function(ids) { + var nodes = this.nodes; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + delete nodes[id]; + } + this._updateNodeIndexList(); + this._reconnectEdges(); + this._updateSelection(); + this._updateValueRange(nodes); +}; + +/** + * Load edges by reading the data table + * @param {Array | DataSet | DataView} edges The data containing the edges. + * @private + * @private + */ +Graph.prototype._setEdges = function(edges) { + var oldEdgesData = this.edgesData; + + if (edges instanceof DataSet || edges instanceof DataView) { + this.edgesData = edges; + } + else if (edges instanceof Array) { + this.edgesData = new DataSet(); + this.edgesData.add(edges); + } + else if (!edges) { + this.edgesData = new DataSet(); + } + else { + throw new TypeError('Array or DataSet expected'); + } + + if (oldEdgesData) { + // unsubscribe from old dataset + util.forEach(this.edgesListeners, function (callback, event) { + oldEdgesData.unsubscribe(event, callback); + }); + } + + // remove drawn edges + this.edges = {}; + + if (this.edgesData) { + // subscribe to new dataset + var me = this; + util.forEach(this.edgesListeners, function (callback, event) { + me.edgesData.subscribe(event, callback); + }); + + // draw all new nodes + var ids = this.edgesData.getIds(); + this._addEdges(ids); + } + + this._reconnectEdges(); +}; + +/** + * Add edges + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._addEdges = function (ids) { + var edges = this.edges, + edgesData = this.edgesData; + + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + + var oldEdge = edges[id]; + if (oldEdge) { + oldEdge.disconnect(); + } + + var data = edgesData.get(id, {"showInternalIds" : true}); + edges[id] = new Edge(data, this, this.constants); + } + + this.moving = true; + this._updateValueRange(edges); +}; + +/** + * Update existing edges, or create them when not yet existing + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._updateEdges = function (ids) { + var edges = this.edges, + edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + + var data = edgesData.get(id); + var edge = edges[id]; + if (edge) { + // update edge + edge.disconnect(); + edge.setProperties(data, this.constants); + edge.connect(); + } + else { + // create edge + edge = new Edge(data, this, this.constants); + this.edges[id] = edge; + } + } + + this.moving = true; + this._updateValueRange(edges); +}; + +/** + * Remove existing edges. Non existing ids will be ignored + * @param {Number[] | String[]} ids + * @private + */ +Graph.prototype._removeEdges = function (ids) { + var edges = this.edges; + for (var i = 0, len = ids.length; i < len; i++) { + var id = ids[i]; + var edge = edges[id]; + if (edge) { + edge.disconnect(); + delete edges[id]; + } + } + + this.moving = true; + this._updateValueRange(edges); +}; + +/** + * Reconnect all edges + * @private + */ +Graph.prototype._reconnectEdges = function() { + var id, + nodes = this.nodes, + edges = this.edges; + for (id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].edges = []; + } + } + + for (id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.from = null; + edge.to = null; + edge.connect(); + } + } +}; + +/** + * 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 + */ +Graph.prototype._updateValueRange = function(obj) { + var id; + + // determine the range of the objects + var valueMin = undefined; + var valueMax = undefined; + for (id in obj) { + if (obj.hasOwnProperty(id)) { + var value = obj[id].getValue(); + if (value !== undefined) { + valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); + valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); + } + } + } + + // adjust the range of all objects + if (valueMin !== undefined && valueMax !== undefined) { + for (id in obj) { + if (obj.hasOwnProperty(id)) { + obj[id].setValueRange(valueMin, valueMax); + } + } + } +}; + +/** + * Redraw the graph with the current data + * chart will be resized too. + */ +Graph.prototype.redraw = function() { + this.setSize(this.width, this.height); + + this._redraw(); +}; + +/** + * Redraw the graph with the current data + * @private + */ +Graph.prototype._redraw = function() { + var ctx = this.frame.canvas.getContext('2d'); + // clear the canvas + var w = this.frame.canvas.width; + var h = this.frame.canvas.height; + ctx.clearRect(0, 0, w, h); + + // set scaling and translation + ctx.save(); + ctx.translate(this.translation.x, this.translation.y); + ctx.scale(this.scale, this.scale); + + this.canvasTopLeft = { + "x": this._canvasToX(0), + "y": this._canvasToY(0) + }; + this.canvasBottomRight = { + "x": this._canvasToX(this.frame.canvas.clientWidth), + "y": this._canvasToY(this.frame.canvas.clientHeight) + }; + + this._doInAllSectors("_drawAllSectorNodes",ctx); + this._doInAllSectors("_drawEdges",ctx); + this._doInAllSectors("_drawNodes",ctx); + + // restore original scaling and translation + ctx.restore(); + + if (this.constants.navigation.enabled == true) { + this._doInNavigationSector("_drawNodes",ctx,true); + } +}; + +/** + * Set the translation of the graph + * @param {Number} offsetX Horizontal offset + * @param {Number} offsetY Vertical offset + * @private + */ +Graph.prototype._setTranslation = function(offsetX, offsetY) { + if (this.translation === undefined) { + this.translation = { + x: 0, + y: 0 + }; + } + + if (offsetX !== undefined) { + this.translation.x = offsetX; + } + if (offsetY !== undefined) { + this.translation.y = offsetY; + } +}; + +/** + * Get the translation of the graph + * @return {Object} translation An object with parameters x and y, both a number + * @private + */ +Graph.prototype._getTranslation = function() { + return { + x: this.translation.x, + y: this.translation.y + }; +}; + +/** + * Scale the graph + * @param {Number} scale Scaling factor 1.0 is unscaled + * @private + */ +Graph.prototype._setScale = function(scale) { + this.scale = scale; +}; + +/** + * Get the current scale of the graph + * @return {Number} scale Scaling factor 1.0 is unscaled + * @private + */ +Graph.prototype._getScale = function() { + return this.scale; +}; + +/** + * Convert a horizontal point on the HTML canvas to the x-value of the model + * @param {number} x + * @returns {number} + * @private + */ +Graph.prototype._canvasToX = function(x) { + return (x - this.translation.x) / this.scale; +}; + +/** + * Convert an x-value in the model to a horizontal point on the HTML canvas + * @param {number} x + * @returns {number} + * @private + */ +Graph.prototype._xToCanvas = function(x) { + return x * this.scale + this.translation.x; +}; + +/** + * Convert a vertical point on the HTML canvas to the y-value of the model + * @param {number} y + * @returns {number} + * @private + */ +Graph.prototype._canvasToY = function(y) { + return (y - this.translation.y) / this.scale; +}; + +/** + * Convert an y-value in the model to a vertical point on the HTML canvas + * @param {number} y + * @returns {number} + * @private + */ +Graph.prototype._yToCanvas = function(y) { + return y * this.scale + this.translation.y ; +}; + +/** + * Redraw all nodes + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] + * @private + */ +Graph.prototype._drawNodes = function(ctx,alwaysShow) { + if (alwaysShow === undefined) { + alwaysShow = false; + } + + // first draw the unselected nodes + var nodes = this.nodes; + var selected = []; + + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight); + if (nodes[id].isSelected()) { + selected.push(id); + } + else { + if (nodes[id].inArea() || alwaysShow) { + nodes[id].draw(ctx); + } + } + } + } + + // draw the selected nodes on top + for (var s = 0, sMax = selected.length; s < sMax; s++) { + if (nodes[selected[s]].inArea() || alwaysShow) { + nodes[selected[s]].draw(ctx); + } + } +}; + +/** + * Redraw all edges + * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); + * @param {CanvasRenderingContext2D} ctx + * @private + */ +Graph.prototype._drawEdges = function(ctx) { + var edges = this.edges; + for (var id in edges) { + if (edges.hasOwnProperty(id)) { + var edge = edges[id]; + edge.setScale(this.scale); + if (edge.connected) { + edges[id].draw(ctx); + } + } + } +}; + +/** + * Find a stable position for all nodes + * @private + */ +Graph.prototype._doStabilize = function() { + //var start = new Date(); + + // find stable position + var count = 0; + var vmin = this.constants.minVelocity; + var stable = false; + while (!stable && count < this.constants.maxIterations) { + this._initializeForceCalculation(); + this._discreteStepNodes(); + stable = !this._isMoving(vmin); + count++; + } + this.zoomToFit(); + + // var end = new Date(); + + // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup +}; + + +/** + * Before calculating the forces, we check if we need to cluster to keep up performance and we check + * if there is more than one node. If it is just one node, we dont calculate anything. + * + * @private + */ +Graph.prototype._initializeForceCalculation = function() { + // stop calculation if there is only one node + if (this.nodeIndices.length == 1) { + this.nodes[this.nodeIndices[0]]._setForce(0,0); + } + else { + // if there are too many nodes on screen, we cluster without repositioning + if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { + this.clusterToFit(this.constants.clustering.reduceToNodes, false); + } + + // we now start the force calculation + this._calculateForces(); + } +}; + + +/** + * Calculate the external forces acting on the nodes + * Forces are caused by: edges, repulsing forces between nodes, gravity + * @private + */ +Graph.prototype._calculateForces = function() { +// var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)), +// "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))} + // create a local edge to the nodes and edges, that is faster + var dx, dy, angle, distance, fx, fy, + repulsingForce, springForce, length, edgeLength, + node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; + var clusterSize; + var nodes = this.nodes; + var edges = this.edges; + + // Gravity is required to keep separated groups from floating off + // the forces are reset to zero in this loop by using _setForce instead + // of _addForce + var gravity = 0.08 * this.forceFactor; + for (i = 0; i < this.nodeIndices.length; i++) { + node = nodes[this.nodeIndices[i]]; + // gravity does not apply when we are in a pocket sector + if (this._sector() == "default") { + dx = -node.x;// + screenCenterPos.x; + dy = -node.y;// + screenCenterPos.y; + + angle = Math.atan2(dy, dx); + fx = Math.cos(angle) * gravity; + fy = Math.sin(angle) * gravity; + } + else { + fx = 0; + fy = 0; + } + node._setForce(fx, fy); + + node.updateDamping(this.nodeIndices.length); + } + + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + + + // 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 < this.nodeIndices.length-1; i++) { + node1 = nodes[this.nodeIndices[i]]; + for (j = i+1; j < this.nodeIndices.length; j++) { + node2 = nodes[this.nodeIndices[j]]; + clusterSize = (node1.clusterSize + node2.clusterSize - 2); + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + + // clusters have a larger region of influence + minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); + + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; + } + else { + // TODO: correct factor for repulsing force + //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + } + // amplify the repulsion for clusters. + repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; + repulsingForce *= this.forceFactor; + + + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce ; + + node1._addForce(-fx, -fy); + node2._addForce(fx, fy); + } + } + } + +/* + // repulsion of the edges on the nodes and + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + node = nodes[nodeId]; + for(var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + + // get the center of the edge + xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; + yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; + + // calculate normally distributed force + dx = node.x - xCenter; + dy = node.y - yCenter; + distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); + + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; + } + else { + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force + } + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; + node._addForce(fx, fy); + edge.from._addForce(-fx/2,-fy/2); + edge.to._addForce(-fx/2,-fy/2); + } + } + } + } + } +*/ + + // forces caused by the edges, modelled as springs + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + if (edge.connected) { + // only calculate forces if nodes are in the same sector + if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { + clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; + edgeLength = edge.length; + // this implies that the edges between big clusters are longer + edgeLength += clusterSize * this.constants.clustering.edgeGrowth; + length = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); + + springForce = edge.stiffness * (edgeLength - length) * this.forceFactor; + + fx = Math.cos(angle) * springForce; + fy = Math.sin(angle) * springForce; + + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } + } + } + } +/* + // TODO: re-implement repulsion of edges + + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } +*/ +}; + + +/** + * Check if any of the nodes is still moving + * @param {number} vmin the minimum velocity considered as 'moving' + * @return {boolean} true if moving, false if non of the nodes is moving + * @private + */ +Graph.prototype._isMoving = function(vmin) { + var vminCorrected = vmin / this.scale; + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) { + return true; + } + } + return false; +}; + + +/** + * /** + * Perform one discrete step for all nodes + * + * @param interval + * @private + */ +Graph.prototype._discreteStepNodes = function() { + var interval = 0.01; + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + nodes[id].discreteStep(interval); + } + } + + var vmin = this.constants.minVelocity; + this.moving = this._isMoving(vmin); +}; + + + +/** + * Start animating nodes and edges + * + * @poram {Boolean} runCalculationStep + */ +Graph.prototype.start = function() { + if (!this.freezeSimulation) { + + if (this.moving) { + this._doInAllActiveSectors("_initializeForceCalculation"); + this._doInAllActiveSectors("_discreteStepNodes"); + this._findCenter(this._getRange()) + } + + if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { + // start animation. only start calculationTimer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + + // keyboad movement + if (graph.xIncrement != 0 || graph.yIncrement != 0) { + var translation = graph._getTranslation(); + graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement); + } + if (graph.zoomIncrement != 0) { + var center = { + x: graph.frame.canvas.clientWidth / 2, + y: graph.frame.canvas.clientHeight / 2 + }; + graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); + } + + graph.start(); + graph._redraw(); + + //this.end = window.performance.now(); + //this.time = this.end - this.startTime; + //console.log('refresh time: ' + this.time); + //this.startTime = window.performance.now(); + + }, this.renderTimestep); + } + } + else { + this._redraw(); + } + } +}; + + + + +Graph.prototype.singleStep = function() { + if (this.moving) { + this._initializeForceCalculation(); + this._discreteStepNodes(); + + var vmin = this.constants.minVelocity; + this.moving = this._isMoving(vmin); + this._redraw(); + } +}; + + + +/** + * Freeze the animation + */ +Graph.prototype.toggleFreeze = function() { + if (this.freezeSimulation == false) { + this.freezeSimulation = true; + } + else { + this.freezeSimulation = false; + this.start(); + } +}; + +/** + * Mixin the cluster system and initialize the parameters required. + * + * @private + */ +Graph.prototype._loadClusterSystem = function() { + this.clusterSession = 0; + this.hubThreshold = 5; + + for (var mixinFunction in ClusterMixin) { + if (ClusterMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction]; + } + } +} + +/** + * Mixin the sector system and initialize the parameters required + * + * @private + */ +Graph.prototype._loadSectorSystem = function() { + this.sectors = {}; + this.activeSector = ["default"]; + this.sectors["active"] = {}; + this.sectors["active"]["default"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; + this.sectors["frozen"] = {}; + this.sectors["navigation"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; + + this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields + for (var mixinFunction in SectorMixin) { + if (SectorMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = SectorMixin[mixinFunction]; + } + } +}; + + +/** + * Mixin the selection system and initialize the parameters required + * + * @private + */ +Graph.prototype._loadSelectionSystem = function() { + this.selection = []; + this.selectionObj = {}; + + for (var mixinFunction in SelectionMixin) { + if (SelectionMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction]; + } + } +} + + +/** + * Mixin the navigation (User Interface) system and initialize the parameters required + * + * @private + */ +Graph.prototype._loadNavigationControls = function() { + for (var mixinFunction in NavigationMixin) { + if (NavigationMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction]; + } + } + + if (this.constants.navigation.enabled == true) { + this._loadNavigationElements(); + } +} + +/** + * this function exists to avoid errors when not loading the navigation system + */ +Graph.prototype._relocateNavigation = function() { + // empty, is overloaded by navigation system +} + +/** + * * this function exists to avoid errors when not loading the navigation system + */ +Graph.prototype._unHighlightAll = function() { + // empty, is overloaded by the navigation system +} + + + + + + + + + + + + + + + + + + + + + + + + + +/** + * vis.js module exports + */ +var vis = { + util: util, + events: events, + + Controller: Controller, + DataSet: DataSet, + DataView: DataView, + Range: Range, + Stack: Stack, + TimeStep: TimeStep, + EventBus: EventBus, + + components: { + items: { + Item: Item, + ItemBox: ItemBox, + ItemPoint: ItemPoint, + ItemRange: ItemRange + }, + + Component: Component, + Panel: Panel, + RootPanel: RootPanel, + ItemSet: ItemSet, + TimeAxis: TimeAxis + }, + + graph: { + Node: Node, + Edge: Edge, + Popup: Popup, + Groups: Groups, + Images: Images + }, + + Timeline: Timeline, + Graph: Graph +}; + +/** + * CommonJS module exports + */ +if (typeof exports !== 'undefined') { + exports = vis; +} +if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = vis; +} + +/** + * AMD module exports + */ +if (typeof(define) === 'function') { + define(function () { + return vis; + }); +} + +/** + * Window exports + */ +if (typeof window !== 'undefined') { + // attach the module to the window, load as a regular javascript file + window['vis'] = vis; +} + + +},{"hammerjs":2,"moment":3,"mousetrap":4}],2:[function(require,module,exports){ +/*! Hammer.JS - v1.0.5 - 2013-04-07 + * http://eightmedia.github.com/hammer.js + * + * Copyright (c) 2013 Jorik Tangelder ; + * Licensed under the MIT license */ + +(function(window, undefined) { + 'use strict'; + +/** + * Hammer + * use this to create instances + * @param {HTMLElement} element + * @param {Object} options + * @returns {Hammer.Instance} + * @constructor + */ +var Hammer = function(element, options) { + return new Hammer.Instance(element, options || {}); +}; + +// default settings +Hammer.defaults = { + // add styles and attributes to the element to prevent the browser from doing + // its native behavior. this doesnt prevent the scrolling, but cancels + // the contextmenu, tap highlighting etc + // set to false to disable this + stop_browser_behavior: { + // this also triggers onselectstart=false for IE + userSelect: 'none', + // this makes the element blocking in IE10 >, you could experiment with the value + // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241 + touchAction: 'none', + touchCallout: 'none', + contentZooming: 'none', + userDrag: 'none', + tapHighlightColor: 'rgba(0,0,0,0)' + } + + // more settings are defined per gesture at gestures.js +}; + +// detect touchevents +Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; +Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); + +// dont use mouseevents on mobile devices +Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i; +Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX); + +// eventtypes per touchevent (start, move, end) +// are filled by Hammer.event.determineEventTypes on setup +Hammer.EVENT_TYPES = {}; + +// direction defines +Hammer.DIRECTION_DOWN = 'down'; +Hammer.DIRECTION_LEFT = 'left'; +Hammer.DIRECTION_UP = 'up'; +Hammer.DIRECTION_RIGHT = 'right'; + +// pointer type +Hammer.POINTER_MOUSE = 'mouse'; +Hammer.POINTER_TOUCH = 'touch'; +Hammer.POINTER_PEN = 'pen'; + +// touch event defines +Hammer.EVENT_START = 'start'; +Hammer.EVENT_MOVE = 'move'; +Hammer.EVENT_END = 'end'; + +// hammer document where the base events are added at +Hammer.DOCUMENT = document; + +// plugins namespace +Hammer.plugins = {}; + +// if the window events are set... +Hammer.READY = false; + +/** + * setup events to detect gestures on the document + */ +function setup() { + if(Hammer.READY) { + return; + } + + // find what eventtypes we add listeners to + Hammer.event.determineEventTypes(); + + // Register all gestures inside Hammer.gestures + for(var name in Hammer.gestures) { + if(Hammer.gestures.hasOwnProperty(name)) { + Hammer.detection.register(Hammer.gestures[name]); + } + } + + // Add touch events on the document + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect); + Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect); + + // Hammer is ready...! + Hammer.READY = true; +} + +/** + * create new hammer instance + * all methods should return the instance itself, so it is chainable. + * @param {HTMLElement} element + * @param {Object} [options={}] + * @returns {Hammer.Instance} + * @constructor + */ +Hammer.Instance = function(element, options) { + var self = this; + + // setup HammerJS window events and register all gestures + // this also sets up the default options + setup(); + + this.element = element; + + // start/stop detection option + this.enabled = true; + + // merge options + this.options = Hammer.utils.extend( + Hammer.utils.extend({}, Hammer.defaults), + options || {}); + + // add some css to the element to prevent the browser from doing its native behavoir + if(this.options.stop_browser_behavior) { + Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior); + } + + // start detection on touchstart + Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) { + if(self.enabled) { + Hammer.detection.startDetect(self, ev); + } + }); + + // return instance + return this; +}; + + +Hammer.Instance.prototype = { + /** + * bind events to the instance + * @param {String} gesture + * @param {Function} handler + * @returns {Hammer.Instance} + */ + on: function onEvent(gesture, handler){ + var gestures = gesture.split(' '); + for(var t=0; t 0 && eventType == Hammer.EVENT_END) { + eventType = Hammer.EVENT_MOVE; + } + // no touches, force the end event + else if(!count_touches) { + eventType = Hammer.EVENT_END; + } + + // because touchend has no touches, and we often want to use these in our gestures, + // we send the last move event as our eventData in touchend + if(!count_touches && last_move_event !== null) { + ev = last_move_event; + } + // store the last move event + else { + last_move_event = ev; + } + + // trigger the handler + handler.call(Hammer.detection, self.collectEventData(element, eventType, ev)); + + // remove pointerevent from list + if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) { + count_touches = Hammer.PointerEvent.updatePointer(eventType, ev); + } + } + + //debug(sourceEventType +" "+ eventType); + + // on the end we reset everything + if(!count_touches) { + last_move_event = null; + enable_detect = false; + touch_triggered = false; + Hammer.PointerEvent.reset(); + } + }); + }, + + + /** + * we have different events for each device/browser + * determine what we need and set them in the Hammer.EVENT_TYPES constant + */ + determineEventTypes: function determineEventTypes() { + // determine the eventtype we want to set + var types; + + // pointerEvents magic + if(Hammer.HAS_POINTEREVENTS) { + types = Hammer.PointerEvent.getEvents(); + } + // on Android, iOS, blackberry, windows mobile we dont want any mouseevents + else if(Hammer.NO_MOUSEEVENTS) { + types = [ + 'touchstart', + 'touchmove', + 'touchend touchcancel']; + } + // for non pointer events browsers and mixed browsers, + // like chrome on windows8 touch laptop + else { + types = [ + 'touchstart mousedown', + 'touchmove mousemove', + 'touchend touchcancel mouseup']; + } + + Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0]; + Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1]; + Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2]; + }, + + + /** + * create touchlist depending on the event + * @param {Object} ev + * @param {String} eventType used by the fakemultitouch plugin + */ + getTouchList: function getTouchList(ev/*, eventType*/) { + // get the fake pointerEvent touchlist + if(Hammer.HAS_POINTEREVENTS) { + return Hammer.PointerEvent.getTouchList(); + } + // get the touchlist + else if(ev.touches) { + return ev.touches; + } + // make fake touchlist from mouse position + else { + return [{ + identifier: 1, + pageX: ev.pageX, + pageY: ev.pageY, + target: ev.target + }]; + } + }, + + + /** + * collect event data for Hammer js + * @param {HTMLElement} element + * @param {String} eventType like Hammer.EVENT_MOVE + * @param {Object} eventData + */ + collectEventData: function collectEventData(element, eventType, ev) { + var touches = this.getTouchList(ev, eventType); + + // find out pointerType + var pointerType = Hammer.POINTER_TOUCH; + if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) { + pointerType = Hammer.POINTER_MOUSE; + } + + return { + center : Hammer.utils.getCenter(touches), + timeStamp : new Date().getTime(), + target : ev.target, + touches : touches, + eventType : eventType, + pointerType : pointerType, + srcEvent : ev, + + /** + * prevent the browser default actions + * mostly used to disable scrolling of the browser + */ + preventDefault: function() { + if(this.srcEvent.preventManipulation) { + this.srcEvent.preventManipulation(); + } + + if(this.srcEvent.preventDefault) { + this.srcEvent.preventDefault(); + } + }, + + /** + * stop bubbling the event up to its parents + */ + stopPropagation: function() { + this.srcEvent.stopPropagation(); + }, + + /** + * immediately stop gesture detection + * might be useful after a swipe was detected + * @return {*} + */ + stopDetect: function() { + return Hammer.detection.stopDetect(); + } + }; + } +}; + +Hammer.PointerEvent = { + /** + * holds all pointers + * @type {Object} + */ + pointers: {}, + + /** + * get a list of pointers + * @returns {Array} touchlist + */ + getTouchList: function() { + var self = this; + var touchlist = []; + + // we can use forEach since pointerEvents only is in IE10 + Object.keys(self.pointers).sort().forEach(function(id) { + touchlist.push(self.pointers[id]); + }); + return touchlist; + }, + + /** + * update the position of a pointer + * @param {String} type Hammer.EVENT_END + * @param {Object} pointerEvent + */ + updatePointer: function(type, pointerEvent) { + if(type == Hammer.EVENT_END) { + this.pointers = {}; + } + else { + pointerEvent.identifier = pointerEvent.pointerId; + this.pointers[pointerEvent.pointerId] = pointerEvent; + } + + return Object.keys(this.pointers).length; + }, + + /** + * check if ev matches pointertype + * @param {String} pointerType Hammer.POINTER_MOUSE + * @param {PointerEvent} ev + */ + matchType: function(pointerType, ev) { + if(!ev.pointerType) { + return false; + } + + var types = {}; + types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE); + types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH); + types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN); + return types[pointerType]; + }, + + + /** + * get events + */ + getEvents: function() { + return [ + 'pointerdown MSPointerDown', + 'pointermove MSPointerMove', + 'pointerup pointercancel MSPointerUp MSPointerCancel' + ]; + }, + + /** + * reset the list + */ + reset: function() { + this.pointers = {}; + } +}; + + +Hammer.utils = { + /** + * extend method, + * also used for cloning when dest is an empty object + * @param {Object} dest + * @param {Object} src + * @parm {Boolean} merge do a merge + * @returns {Object} dest + */ + extend: function extend(dest, src, merge) { + for (var key in src) { + if(dest[key] !== undefined && merge) { + continue; + } + dest[key] = src[key]; + } + return dest; + }, /** @@ -13397,7 +16690,7 @@ else { })(this); },{}],3:[function(require,module,exports){ //! moment.js -//! version : 2.5.0 +//! version : 2.5.1 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors //! license : MIT //! momentjs.com @@ -13409,7 +16702,7 @@ else { ************************************/ var moment, - VERSION = "2.5.0", + VERSION = "2.5.1", global = this, round = Math.round, i, @@ -13425,6 +16718,19 @@ else { // internal storage for language config files languages = {}, + // moment internal properties + momentProperties = { + _isAMomentObject: null, + _i : null, + _f : null, + _l : null, + _strict : null, + _isUTC : null, + _offset : null, // optional. Combine with _isUTC + _pf : null, + _lang : null // optional + }, + // check for nodeJS hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'), @@ -13456,19 +16762,21 @@ else { parseTokenTwoDigits = /\d\d/, // 00 - 99 parseTokenThreeDigits = /\d{3}/, // 000 - 999 parseTokenFourDigits = /\d{4}/, // 0000 - 9999 - parseTokenSixDigits = /[+\-]?\d{6}/, // -999,999 - 999,999 + parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 + parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf // 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{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)?)?$/, + 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)?)?$/, isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', isoDates = [ - 'YYYY-MM-DD', - 'GGGG-[W]WW', - 'GGGG-[W]WW-E', - 'YYYY-DDD' + ['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}/] ], // iso time formats and regexes @@ -13578,7 +16886,7 @@ else { return leftZeroFill(this.weekYear() % 100, 2); }, gggg : function () { - return this.weekYear(); + return leftZeroFill(this.weekYear(), 4); }, ggggg : function () { return leftZeroFill(this.weekYear(), 5); @@ -13587,7 +16895,7 @@ else { return leftZeroFill(this.isoWeekYear() % 100, 2); }, GGGG : function () { - return this.isoWeekYear(); + return leftZeroFill(this.isoWeekYear(), 4); }, GGGGG : function () { return leftZeroFill(this.isoWeekYear(), 5); @@ -13662,6 +16970,23 @@ else { lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; + function defaultParsingFlags() { + // We need to deep clone this object, and es5 standard is not very + // helpful. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso: false + }; + } + function padToken(func, count) { return function (a) { return leftZeroFill(func.call(this, a), count); @@ -13753,6 +17078,17 @@ else { return a; } + function cloneMoment(m) { + var result = {}, i; + for (i in m) { + if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) { + result[i] = m[i]; + } + } + + return result; + } + function absRound(number) { if (number < 0) { return Math.ceil(number); @@ -13764,7 +17100,7 @@ else { // 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) + '', + var output = '' + Math.abs(number), sign = number >= 0; while (output.length < targetLength) { @@ -13944,21 +17280,6 @@ else { } } - function initializeParsingFlags(config) { - config._pf = { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso: false - }; - } - function isValid(m) { if (m._isValid == null) { m._isValid = !isNaN(m._d.getTime()) && @@ -14327,6 +17648,10 @@ else { case 'GGGG': case 'gggg': return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; + case 'Y': + case 'G': + case 'g': + return parseTokenSignedNumber; case 'YYYYYY': case 'YYYYY': case 'GGGGG': @@ -14339,8 +17664,10 @@ else { if (strict) { return parseTokenTwoDigits; } /* falls through */ case 'SSS': + if (strict) { return parseTokenThreeDigits; } + /* falls through */ case 'DDD': - return strict ? parseTokenThreeDigits : parseTokenOneToThreeDigits; + return parseTokenOneToThreeDigits; case 'MMM': case 'MMMM': case 'dd': @@ -14382,7 +17709,7 @@ else { case 'W': case 'e': case 'E': - return strict ? parseTokenOneDigit : parseTokenOneOrTwoDigits; + return parseTokenOneOrTwoDigits; default : a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i")); return a; @@ -14510,1255 +17837,2059 @@ else { } } - // 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, fixYear, w, temp, lang, weekday, week; + // 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, fixYear, w, temp, lang, weekday, week; + + if (config._d) { + return; + } + + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + fixYear = function (val) { + var int_val = parseInt(val, 10); + return val ? + (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) : + (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]); + }; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1); + } + else { + lang = getLangDefinition(config._l); + weekday = w.d != null ? parseWeekday(w.d, lang) : + (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0); + + week = parseInt(w.w, 10) || 1; + + //if we're parsing 'd', then the low day numbers may be next week + if (w.d != null && weekday < lang._week.dow) { + week++; + } + + temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow); + } + + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear) { + yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR]; + + if (config._dayOfYear > daysInYear(yearToUse)) { + config._pf._overflowDayOfYear = true; + } + + date = makeUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // 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]; + } + + // 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]; + } + + // add the offsets to the time to be parsed so that we can have a clean array for checking isValid + input[HOUR] += toInt((config._tzm || 0) / 60); + input[MINUTE] += toInt((config._tzm || 0) % 60); + + config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + } + + function dateFromObject(config) { + var normalizedInput; + + if (config._d) { + return; + } + + normalizedInput = normalizeObjectUnits(config._i); + config._a = [ + normalizedInput.year, + normalizedInput.month, + normalizedInput.day, + normalizedInput.hour, + normalizedInput.minute, + normalizedInput.second, + normalizedInput.millisecond + ]; + + dateFromConfig(config); + } + + function currentDateArray(config) { + var now = new Date(); + if (config._useUTC) { + return [ + now.getUTCFullYear(), + now.getUTCMonth(), + now.getUTCDate() + ]; + } else { + return [now.getFullYear(), now.getMonth(), now.getDate()]; + } + } + + // date from string and format string + function makeDateFromStringAndFormat(config) { + + config._a = []; + config._pf.empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var lang = getLangDefinition(config._l), + string = '' + config._i, + i, parsedInput, tokens, token, skipped, + stringLength = string.length, + totalParsedInputLength = 0; + + tokens = expandFormat(config._f, lang).match(formattingTokens) || []; + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + config._pf.unusedInput.push(skipped); + } + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + config._pf.empty = false; + } + else { + config._pf.unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } + else if (config._strict && !parsedInput) { + config._pf.unusedTokens.push(token); + } + } + + // add remaining unparsed input length to the string + config._pf.charsLeftOver = stringLength - totalParsedInputLength; + if (string.length > 0) { + config._pf.unusedInput.push(string); + } + + // handle am pm + if (config._isPm && config._a[HOUR] < 12) { + config._a[HOUR] += 12; + } + // if is 12 am, change hours to 0 + if (config._isPm === false && config._a[HOUR] === 12) { + config._a[HOUR] = 0; + } + + dateFromConfig(config); + checkOverflow(config); + } + + function unescapeFormat(s) { + return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + }); + } - if (config._d) { + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function regexpEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + bestMoment, + + scoreToBeat, + i, + currentScore; + + if (config._f.length === 0) { + config._pf.invalidFormat = true; + config._d = new Date(NaN); return; } - currentDate = currentDateArray(config); + for (i = 0; i < config._f.length; i++) { + currentScore = 0; + tempConfig = extend({}, config); + tempConfig._pf = defaultParsingFlags(); + tempConfig._f = config._f[i]; + makeDateFromStringAndFormat(tempConfig); - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - fixYear = function (val) { - var int_val = parseInt(val, 10); - return val ? - (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) : - (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]); - }; + if (!isValid(tempConfig)) { + continue; + } - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1); + // if there is any input that was not parsed add a penalty for that format + currentScore += tempConfig._pf.charsLeftOver; + + //or tokens + currentScore += tempConfig._pf.unusedTokens.length * 10; + + tempConfig._pf.score = currentScore; + + if (scoreToBeat == null || currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; } - else { - lang = getLangDefinition(config._l); - weekday = w.d != null ? parseWeekday(w.d, lang) : - (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0); + } - week = parseInt(w.w, 10) || 1; + extend(config, bestMoment || tempConfig); + } - //if we're parsing 'd', then the low day numbers may be next week - if (w.d != null && weekday < lang._week.dow) { - week++; - } + // date from iso format + function makeDateFromString(config) { + var i, l, + string = config._i, + match = isoRegex.exec(string); - temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow); + 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._d = new Date(string); + } + } - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; + function makeDateFromInput(config) { + var input = config._i, + matched = aspNetJsonRegex.exec(input); + + if (input === undefined) { + config._d = new Date(); + } else if (matched) { + config._d = new Date(+matched[1]); + } else if (typeof input === 'string') { + makeDateFromString(config); + } else if (isArray(input)) { + config._a = input.slice(0); + dateFromConfig(config); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof(input) === 'object') { + dateFromObject(config); + } else { + config._d = new Date(input); } + } - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR]; + 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); - if (config._dayOfYear > daysInYear(yearToUse)) { - config._pf._overflowDayOfYear = true; - } + //the date constructor doesn't accept years < 1970 + if (y < 1970) { + date.setFullYear(y); + } + return date; + } - date = makeUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); + function makeUTCDate(y) { + var date = new Date(Date.UTC.apply(null, arguments)); + if (y < 1970) { + date.setUTCFullYear(y); } + return date; + } - // 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]; + function parseWeekday(input, language) { + if (typeof input === 'string') { + if (!isNaN(input)) { + input = parseInt(input, 10); + } + else { + input = language.weekdaysParse(input); + if (typeof input !== 'number') { + return null; + } + } } + return input; + } - // 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]; + /************************************ + Relative Time + ************************************/ + + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { + return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function relativeTime(milliseconds, withoutSuffix, lang) { + var seconds = round(Math.abs(milliseconds) / 1000), + minutes = round(seconds / 60), + hours = round(minutes / 60), + days = round(hours / 24), + years = round(days / 365), + args = seconds < 45 && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < 45 && ['mm', minutes] || + hours === 1 && ['h'] || + hours < 22 && ['hh', hours] || + days === 1 && ['d'] || + days <= 25 && ['dd', days] || + days <= 45 && ['M'] || + days < 345 && ['MM', round(days / 30)] || + years === 1 && ['y'] || ['yy', years]; + args[2] = withoutSuffix; + args[3] = milliseconds > 0; + args[4] = lang; + return substituteTimeAgo.apply({}, args); + } + + + /************************************ + Week of Year + ************************************/ + + + // 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 (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; } - // add the offsets to the time to be parsed so that we can have a clean array for checking isValid - input[HOUR] += toInt((config._tzm || 0) / 60); - input[MINUTE] += toInt((config._tzm || 0) % 60); + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } - config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); + adjustedMoment = moment(mom).add('d', daysToDayOfWeek); + return { + week: Math.ceil(adjustedMoment.dayOfYear() / 7), + year: adjustedMoment.year() + }; } - function dateFromObject(config) { - var normalizedInput; + //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; + + weekday = weekday != null ? weekday : firstDayOfWeek; + daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); + dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + + return { + year: dayOfYear > 0 ? year : year - 1, + dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear + }; + } + + /************************************ + Top Level Functions + ************************************/ + + function makeMoment(config) { + var input = config._i, + format = config._f; - if (config._d) { - return; + if (input === null) { + return moment.invalid({nullInput: true}); } - normalizedInput = normalizeObjectUnits(config._i); - config._a = [ - normalizedInput.year, - normalizedInput.month, - normalizedInput.day, - normalizedInput.hour, - normalizedInput.minute, - normalizedInput.second, - normalizedInput.millisecond - ]; + if (typeof input === 'string') { + config._i = input = getLangDefinition().preparse(input); + } - dateFromConfig(config); - } + if (moment.isMoment(input)) { + config = cloneMoment(input); - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [ - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate() - ]; + config._d = new Date(+input._d); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } } else { - return [now.getFullYear(), now.getMonth(), now.getDate()]; + makeDateFromInput(config); } + + return new Moment(config); } - // date from string and format string - function makeDateFromStringAndFormat(config) { + moment = function (input, format, lang, strict) { + var c; - config._a = []; - config._pf.empty = true; + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._i = input; + c._f = format; + c._l = lang; + c._strict = strict; + c._isUTC = false; + c._pf = defaultParsingFlags(); + + return makeMoment(c); + }; - // This array is used to make a Date, either with `new Date` or `Date.UTC` - var lang = getLangDefinition(config._l), - string = '' + config._i, - i, parsedInput, tokens, token, skipped, - stringLength = string.length, - totalParsedInputLength = 0; + // creating with utc + moment.utc = function (input, format, lang, strict) { + var c; - tokens = expandFormat(config._f, lang).match(formattingTokens) || []; + if (typeof(lang) === "boolean") { + strict = lang; + lang = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c = {}; + c._isAMomentObject = true; + c._useUTC = true; + c._isUTC = true; + c._l = lang; + c._i = input; + c._f = format; + c._strict = strict; + c._pf = defaultParsingFlags(); + + return makeMoment(c).utc(); + }; - 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); + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; + + // duration + moment.duration = function (input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + parseIso; + + 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]) + }; } - // add remaining unparsed input length to the string - config._pf.charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - config._pf.unusedInput.push(string); + ret = new Duration(duration); + + if (moment.isDuration(input) && input.hasOwnProperty('_lang')) { + ret._lang = input._lang; } - // handle am pm - if (config._isPm && config._a[HOUR] < 12) { - config._a[HOUR] += 12; + return ret; + }; + + // version number + moment.version = VERSION; + + // default format + moment.defaultFormat = isoFormat; + + // 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 () {}; + + // This function will load languages and then set the global language. If + // no arguments are passed in, it will simply return the current global + // language key. + moment.lang = function (key, values) { + var r; + if (!key) { + return moment.fn._lang._abbr; } - // if is 12 am, change hours to 0 - if (config._isPm === false && config._a[HOUR] === 12) { - config._a[HOUR] = 0; + if (values) { + loadLang(normalizeLanguage(key), values); + } else if (values === null) { + unloadLang(key); + key = 'en'; + } else if (!languages[key]) { + getLangDefinition(key); } + r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); + return r._abbr; + }; - dateFromConfig(config); - checkOverflow(config); - } + // returns language data + moment.langData = function (key) { + if (key && key._lang && key._lang._abbr) { + key = key._lang._abbr; + } + return getLangDefinition(key); + }; - function unescapeFormat(s) { - return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }); - } + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment || + (obj != null && obj.hasOwnProperty('_isAMomentObject')); + }; - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function regexpEscape(s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; - // date from string and array of format strings - function makeDateFromStringAndArray(config) { - var tempConfig, - bestMoment, + for (i = lists.length - 1; i >= 0; --i) { + makeList(lists[i]); + } - scoreToBeat, - i, - currentScore; + moment.normalizeUnits = function (units) { + return normalizeUnits(units); + }; - if (config._f.length === 0) { - config._pf.invalidFormat = true; - config._d = new Date(NaN); - return; + moment.invalid = function (flags) { + var m = moment.utc(NaN); + if (flags != null) { + extend(m._pf, flags); + } + else { + m._pf.userInvalidated = true; } - for (i = 0; i < config._f.length; i++) { - currentScore = 0; - tempConfig = extend({}, config); - initializeParsingFlags(tempConfig); - tempConfig._f = config._f[i]; - makeDateFromStringAndFormat(tempConfig); + return m; + }; + + moment.parseZone = function (input) { + return moment(input).parseZone(); + }; + + /************************************ + Moment Prototype + ************************************/ + + + extend(moment.fn = Moment.prototype, { + + clone : function () { + return moment(this); + }, - if (!isValid(tempConfig)) { - continue; - } + valueOf : function () { + return +this._d + ((this._offset || 0) * 60000); + }, - // if there is any input that was not parsed add a penalty for that format - currentScore += tempConfig._pf.charsLeftOver; + unix : function () { + return Math.floor(+this / 1000); + }, - //or tokens - currentScore += tempConfig._pf.unusedTokens.length * 10; + toString : function () { + return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + }, - tempConfig._pf.score = currentScore; + toDate : function () { + return this._offset ? new Date(+this) : this._d; + }, - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; + toISOString : function () { + var m = moment(this).utc(); + if (0 < m.year() && m.year() <= 9999) { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); } - } + }, - extend(config, bestMoment || tempConfig); - } + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, - // date from iso format - function makeDateFromString(config) { - var i, - string = config._i, - match = isoRegex.exec(string); + isValid : function () { + return isValid(this); + }, - if (match) { - config._pf.iso = true; - for (i = 4; i > 0; i--) { - if (match[i]) { - // match[5] should be "T" or undefined - config._f = isoDates[i - 1] + (match[6] || " "); - break; - } - } - for (i = 0; i < 4; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(parseTokenTimezone)) { - config._f += "Z"; - } - makeDateFromStringAndFormat(config); - } - else { - config._d = new Date(string); - } - } + isDSTShifted : function () { - function makeDateFromInput(config) { - var input = config._i, - matched = aspNetJsonRegex.exec(input); + if (this._a) { + return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + } - if (input === undefined) { - config._d = new Date(); - } else if (matched) { - config._d = new Date(+matched[1]); - } else if (typeof input === 'string') { - makeDateFromString(config); - } else if (isArray(input)) { - config._a = input.slice(0); - dateFromConfig(config); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if (typeof(input) === 'object') { - dateFromObject(config); - } else { - config._d = new Date(input); - } - } + return false; + }, - 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); + parsingFlags : function () { + return extend({}, this._pf); + }, - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } + invalidAt: function () { + return this._pf.overflow; + }, - function makeUTCDate(y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } + utc : function () { + return this.zone(0); + }, - function parseWeekday(input, language) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = language.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } + local : function () { + this.zone(0); + this._isUTC = false; + return this; + }, - /************************************ - Relative Time - ************************************/ + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.lang().postformat(output); + }, + add : function (input, val) { + var dur; + // switch args to support add('s', 1) and add(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, 1); + return this; + }, - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { - return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture); - } + subtract : function (input, val) { + var dur; + // switch args to support subtract('s', 1) and subtract(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, -1); + return this; + }, - function relativeTime(milliseconds, withoutSuffix, lang) { - var seconds = round(Math.abs(milliseconds) / 1000), - minutes = round(seconds / 60), - hours = round(minutes / 60), - days = round(hours / 24), - years = round(days / 365), - args = seconds < 45 && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < 45 && ['mm', minutes] || - hours === 1 && ['h'] || - hours < 22 && ['hh', hours] || - days === 1 && ['d'] || - days <= 25 && ['dd', days] || - days <= 45 && ['M'] || - days < 345 && ['MM', round(days / 30)] || - years === 1 && ['y'] || ['yy', years]; - args[2] = withoutSuffix; - args[3] = milliseconds > 0; - args[4] = lang; - return substituteTimeAgo.apply({}, args); - } + diff : function (input, units, asFloat) { + var that = makeAs(input, this), + zoneDiff = (this.zone() - that.zone()) * 6e4, + diff, output; + units = normalizeUnits(units); - /************************************ - Week of Year - ************************************/ + if (units === 'year' || units === 'month') { + // average number of days in the months in the given dates + diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 + // difference in months + output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); + // adjust by taking difference in days, average number of days + // and dst in the given months. + output += ((this - moment(this).startOf('month')) - + (that - moment(that).startOf('month'))) / diff; + // same as above but with zones, to negate all dst + output -= ((this.zone() - moment(this).startOf('month').zone()) - + (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff; + if (units === 'year') { + output = output / 12; + } + } else { + diff = (this - that); + output = units === 'second' ? diff / 1e3 : // 1000 + units === 'minute' ? diff / 6e4 : // 1000 * 60 + units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + diff; + } + return asFloat ? output : absRound(output); + }, + from : function (time, withoutSuffix) { + return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix); + }, - // 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; + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, + calendar : function () { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're zone'd or not. + var sod = makeAs(moment(), this).startOf('day'), + diff = this.diff(sod, 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + return this.format(this.lang().calendar(format, this)); + }, - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; - } + isLeapYear : function () { + return isLeapYear(this.year()); + }, - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; - } + isDST : function () { + return (this.zone() < this.clone().month(0).zone() || + this.zone() < this.clone().month(5).zone()); + }, - adjustedMoment = moment(mom).add('d', daysToDayOfWeek); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; - } + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.lang()); + return this.add({ d : input - day }); + } else { + return day; + } + }, - //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) { - // The only solid way to create an iso date from year is to use - // a string format (Date.UTC handles only years > 1900). Don't ask why - // it doesn't need Z at the end. - var d = new Date(leftZeroFill(year, 6, true) + '-01-01').getUTCDay(), - daysToAdd, dayOfYear; + month : function (input) { + var utc = this._isUTC ? 'UTC' : '', + dayOfMonth; - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; + if (input != null) { + if (typeof input === 'string') { + input = this.lang().monthsParse(input); + if (typeof input !== 'number') { + return this; + } + } - return { - year: dayOfYear > 0 ? year : year - 1, - dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } + dayOfMonth = this.date(); + this.date(1); + this._d['set' + utc + 'Month'](input); + this.date(Math.min(dayOfMonth, this.daysInMonth())); - /************************************ - Top Level Functions - ************************************/ + moment.updateOffset(this); + return this; + } else { + return this._d['get' + utc + 'Month'](); + } + }, - function makeMoment(config) { - var input = config._i, - format = config._f; + 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 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + /* falls through */ + } - if (typeof config._pf === 'undefined') { - initializeParsingFlags(config); - } + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } - if (input === null) { - return moment.invalid({nullInput: true}); - } + return this; + }, - if (typeof input === 'string') { - config._i = input = getLangDefinition().preparse(input); - } + endOf: function (units) { + units = normalizeUnits(units); + return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1); + }, - if (moment.isMoment(input)) { - config = extend({}, input); + isAfter: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) > +moment(input).startOf(units); + }, - config._d = new Date(+input._d); - } else if (format) { - if (isArray(format)) { - makeDateFromStringAndArray(config); - } else { - makeDateFromStringAndFormat(config); - } - } else { - makeDateFromInput(config); - } + isBefore: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) < +moment(input).startOf(units); + }, - return new Moment(config); - } + isSame: function (input, units) { + units = units || 'ms'; + return +this.clone().startOf(units) === +makeAs(input, this).startOf(units); + }, - moment = function (input, format, lang, strict) { - if (typeof(lang) === "boolean") { - strict = lang; - lang = undefined; - } - return makeMoment({ - _i : input, - _f : format, - _l : lang, - _strict : strict, - _isUTC : false - }); - }; + min: function (other) { + other = moment.apply(null, arguments); + return other < this ? this : other; + }, - // creating with utc - moment.utc = function (input, format, lang, strict) { - var m; + max: function (other) { + other = moment.apply(null, arguments); + return other > this ? this : other; + }, - if (typeof(lang) === "boolean") { - strict = lang; - lang = undefined; - } - m = makeMoment({ - _useUTC : true, - _isUTC : true, - _l : lang, - _i : input, - _f : format, - _strict : strict - }).utc(); + zone : function (input) { + var offset = this._offset || 0; + if (input != null) { + if (typeof input === "string") { + input = timezoneMinutesFromString(input); + } + if (Math.abs(input) < 16) { + input = input * 60; + } + this._offset = input; + this._isUTC = true; + if (offset !== input) { + addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true); + } + } else { + return this._isUTC ? offset : this._d.getTimezoneOffset(); + } + return this; + }, - return m; - }; + zoneAbbr : function () { + return this._isUTC ? "UTC" : ""; + }, - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); - }; + zoneName : function () { + return this._isUTC ? "Coordinated Universal Time" : ""; + }, - // duration - moment.duration = function (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - parseIso; + parseZone : function () { + if (this._tzm) { + this.zone(this._tzm); + } else if (typeof this._i === 'string') { + this.zone(this._i); + } + return this; + }, - 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; + hasAlignedHourOffset : function (input) { + if (!input) { + input = 0; } - } 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 { + input = moment(input).zone(); + } + + return (this.zone() - input) % 60 === 0; + }, - ret = new Duration(duration); + daysInMonth : function () { + return daysInMonth(this.year(), this.month()); + }, - if (moment.isDuration(input) && input.hasOwnProperty('_lang')) { - ret._lang = input._lang; - } + dayOfYear : function (input) { + var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); + }, - return ret; - }; + quarter : function () { + return Math.ceil((this.month() + 1.0) / 3.0); + }, - // version number - moment.version = VERSION; + weekYear : function (input) { + var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year; + return input == null ? year : this.add("y", (input - year)); + }, - // default format - moment.defaultFormat = isoFormat; + isoWeekYear : function (input) { + var year = weekOfYear(this, 1, 4).year; + return input == null ? year : this.add("y", (input - year)); + }, - // 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 () {}; + week : function (input) { + var week = this.lang().week(this); + return input == null ? week : this.add("d", (input - week) * 7); + }, - // This function will load languages and then set the global language. If - // no arguments are passed in, it will simply return the current global - // language key. - moment.lang = function (key, values) { - var r; - if (!key) { - return moment.fn._lang._abbr; - } - if (values) { - loadLang(normalizeLanguage(key), values); - } else if (values === null) { - unloadLang(key); - key = 'en'; - } else if (!languages[key]) { - getLangDefinition(key); - } - r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); - return r._abbr; - }; + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add("d", (input - week) * 7); + }, - // returns language data - moment.langData = function (key) { - if (key && key._lang && key._lang._abbr) { - key = key._lang._abbr; - } - return getLangDefinition(key); - }; + weekday : function (input) { + var weekday = (this.day() + 7 - this.lang()._week.dow) % 7; + return input == null ? weekday : this.add("d", input - weekday); + }, - // compare moment object - moment.isMoment = function (obj) { - return obj instanceof Moment; - }; + 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); + }, - // for typechecking Duration objects - moment.isDuration = function (obj) { - return obj instanceof Duration; - }; + get : function (units) { + units = normalizeUnits(units); + return this[units](); + }, - for (i = lists.length - 1; i >= 0; --i) { - makeList(lists[i]); + set : function (units, value) { + units = normalizeUnits(units); + if (typeof this[units] === 'function') { + this[units](value); + } + return this; + }, + + // If passed a language key, it will set the language for this + // instance. Otherwise, it will return the language configuration + // variables for this instance. + lang : function (key) { + if (key === undefined) { + return this._lang; + } else { + this._lang = getLangDefinition(key); + return this; + } + } + }); + + // helper for adding shortcuts + function makeGetterAndSetter(name, key) { + moment.fn[name] = moment.fn[name + 's'] = function (input) { + var utc = this._isUTC ? 'UTC' : ''; + if (input != null) { + this._d['set' + utc + key](input); + moment.updateOffset(this); + return this; + } else { + return this._d['get' + utc + key](); + } + }; } - moment.normalizeUnits = function (units) { - return normalizeUnits(units); - }; + // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) + for (i = 0; i < proxyGettersAndSetters.length; i ++) { + makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]); + } - moment.invalid = function (flags) { - var m = moment.utc(NaN); - if (flags != null) { - extend(m._pf, flags); - } - else { - m._pf.userInvalidated = true; - } + // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') + makeGetterAndSetter('year', 'FullYear'); - return m; - }; + // 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.parseZone = function (input) { - return moment(input).parseZone(); - }; + // add aliased format methods + moment.fn.toJSON = moment.fn.toISOString; /************************************ - Moment Prototype + Duration Prototype ************************************/ - extend(moment.fn = Moment.prototype, { + extend(moment.duration.fn = Duration.prototype, { - clone : function () { - return moment(this); + _bubble : function () { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, minutes, hours, years; + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absRound(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absRound(seconds / 60); + data.minutes = minutes % 60; + + hours = absRound(minutes / 60); + data.hours = hours % 24; + + days += absRound(hours / 24); + data.days = days % 30; + + months += absRound(days / 30); + data.months = months % 12; + + years = absRound(months / 12); + data.years = years; }, - valueOf : function () { - return +this._d + ((this._offset || 0) * 60000); + weeks : function () { + return absRound(this.days() / 7); }, - unix : function () { - return Math.floor(+this / 1000); + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6; }, - toString : function () { - return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + humanize : function (withSuffix) { + var difference = +this, + output = relativeTime(difference, !withSuffix, this.lang()); + + if (withSuffix) { + output = this.lang().pastFuture(difference, output); + } + + return this.lang().postformat(output); }, - toDate : function () { - return this._offset ? new Date(+this) : this._d; + 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; }, - toISOString : function () { - var m = moment(this).utc(); - if (0 < m.year() && m.year() <= 9999) { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } + subtract : function (input, val) { + var dur = moment.duration(input, val); + + this._milliseconds -= dur._milliseconds; + this._days -= dur._days; + this._months -= dur._months; + + this._bubble(); + + return this; }, - toArray : function () { - var m = this; - return [ - m.year(), - m.month(), - m.date(), - m.hours(), - m.minutes(), - m.seconds(), - m.milliseconds() - ]; + get : function (units) { + units = normalizeUnits(units); + return this[units.toLowerCase() + 's'](); }, - isValid : function () { - return isValid(this); + as : function (units) { + units = normalizeUnits(units); + return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's'](); }, - isDSTShifted : function () { + lang : moment.fn.lang, - if (this._a) { - return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; + toIsoString : function () { + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var years = Math.abs(this.years()), + months = Math.abs(this.months()), + days = Math.abs(this.days()), + hours = Math.abs(this.hours()), + minutes = Math.abs(this.minutes()), + seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); + + if (!this.asSeconds()) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; } - return false; - }, + 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' : ''); + } + }); - parsingFlags : function () { - return extend({}, this._pf); - }, + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } - invalidAt: function () { - return this._pf.overflow; - }, + function makeDurationAsGetter(name, factor) { + moment.duration.fn['as' + name] = function () { + return +this / factor; + }; + } - utc : function () { - return this.zone(0); - }, + for (i in unitMillisecondFactors) { + if (unitMillisecondFactors.hasOwnProperty(i)) { + makeDurationAsGetter(i, unitMillisecondFactors[i]); + makeDurationGetter(i.toLowerCase()); + } + } - local : function () { - this.zone(0); - this._isUTC = false; - return this; - }, + makeDurationAsGetter('Weeks', 6048e5); + moment.duration.fn.asMonths = function () { + return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12; + }; - format : function (inputString) { - var output = formatMoment(this, inputString || moment.defaultFormat); - return this.lang().postformat(output); - }, - add : function (input, val) { - var dur; - // switch args to support add('s', 1) and add(1, 's') - if (typeof input === 'string') { - dur = moment.duration(+val, input); - } else { - dur = moment.duration(input, val); - } - addOrSubtractDurationFromMoment(this, dur, 1); - return this; - }, + /************************************ + Default Lang + ************************************/ - subtract : function (input, val) { - var dur; - // switch args to support subtract('s', 1) and subtract(1, 's') - if (typeof input === 'string') { - dur = moment.duration(+val, input); - } else { - dur = moment.duration(input, val); - } - addOrSubtractDurationFromMoment(this, dur, -1); - return this; - }, - diff : function (input, units, asFloat) { - var that = makeAs(input, this), - zoneDiff = (this.zone() - that.zone()) * 6e4, - diff, output; + // Set default language, other languages will inherit from English. + moment.lang('en', { + ordinal : function (number) { + var b = number % 10, + output = (toInt(number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); - units = normalizeUnits(units); + /* EMBED_LANGUAGES */ - if (units === 'year' || units === 'month') { - // average number of days in the months in the given dates - diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 - // difference in months - output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); - // adjust by taking difference in days, average number of days - // and dst in the given months. - output += ((this - moment(this).startOf('month')) - - (that - moment(that).startOf('month'))) / diff; - // same as above but with zones, to negate all dst - output -= ((this.zone() - moment(this).startOf('month').zone()) - - (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff; - if (units === 'year') { - output = output / 12; + /************************************ + Exposing Moment + ************************************/ + + function makeGlobal(deprecate) { + var warned = false, local_moment = moment; + /*global ender:false */ + if (typeof ender !== 'undefined') { + return; + } + // here, `this` means `window` in the browser, or `global` on the server + // add `moment` as a global object via a string identifier, + // for Closure Compiler "advanced" mode + if (deprecate) { + global.moment = function () { + if (!warned && console && console.warn) { + warned = true; + console.warn( + "Accessing Moment through the global scope is " + + "deprecated, and will be removed in an upcoming " + + "release."); } - } 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 local_moment.apply(null, arguments); + }; + extend(global.moment, local_moment); + } else { + global['moment'] = moment; + } + } + + // CommonJS module is defined + if (hasModule) { + module.exports = moment; + makeGlobal(true); + } else if (typeof define === "function" && define.amd) { + define("moment", function (require, exports, module) { + if (module.config && module.config() && module.config().noGlobal !== true) { + // If user provided noGlobal, he is aware of global + makeGlobal(module.config().noGlobal === undefined); } - return asFloat ? output : absRound(output); - }, - from : function (time, withoutSuffix) { - return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix); - }, + return moment; + }); + } else { + makeGlobal(); + } +}).call(this); - fromNow : function (withoutSuffix) { - return this.from(moment(), withoutSuffix); - }, +},{}],4:[function(require,module,exports){ +/** + * Copyright 2012 Craig Campbell + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Mousetrap is a simple keyboard shortcut library for Javascript with + * no external dependencies + * + * @version 1.1.2 + * @url craig.is/killing/mice + */ - calendar : function () { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're zone'd or not. - var sod = makeAs(moment(), this).startOf('day'), - diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; - return this.format(this.lang().calendar(format, this)); - }, + /** + * mapping of special keycodes to their corresponding keys + * + * everything in this dictionary cannot use keypress events + * so it has to be here to map to the correct keycodes for + * keyup/keydown events + * + * @type {Object} + */ + var _MAP = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 20: 'capslock', + 27: 'esc', + 32: 'space', + 33: 'pageup', + 34: 'pagedown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'ins', + 46: 'del', + 91: 'meta', + 93: 'meta', + 224: 'meta' + }, - isLeapYear : function () { - return isLeapYear(this.year()); - }, + /** + * mapping for special characters so they can support + * + * this dictionary is only used incase you want to bind a + * keyup or keydown event to one of these keys + * + * @type {Object} + */ + _KEYCODE_MAP = { + 106: '*', + 107: '+', + 109: '-', + 110: '.', + 111 : '/', + 186: ';', + 187: '=', + 188: ',', + 189: '-', + 190: '.', + 191: '/', + 192: '`', + 219: '[', + 220: '\\', + 221: ']', + 222: '\'' + }, - isDST : function () { - return (this.zone() < this.clone().month(0).zone() || - this.zone() < this.clone().month(5).zone()); - }, + /** + * this is a mapping of keys that require shift on a US keypad + * back to the non shift equivelents + * + * this is so you can use keyup events with these keys + * + * note that this will only work reliably on US keyboards + * + * @type {Object} + */ + _SHIFT_MAP = { + '~': '`', + '!': '1', + '@': '2', + '#': '3', + '$': '4', + '%': '5', + '^': '6', + '&': '7', + '*': '8', + '(': '9', + ')': '0', + '_': '-', + '+': '=', + ':': ';', + '\"': '\'', + '<': ',', + '>': '.', + '?': '/', + '|': '\\' + }, - day : function (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.lang()); - return this.add({ d : input - day }); - } else { - return day; - } - }, + /** + * this is a list of special strings you can use to map + * to modifier keys when you specify your keyboard shortcuts + * + * @type {Object} + */ + _SPECIAL_ALIASES = { + 'option': 'alt', + 'command': 'meta', + 'return': 'enter', + 'escape': 'esc' + }, - month : function (input) { - var utc = this._isUTC ? 'UTC' : '', - dayOfMonth; + /** + * variable to store the flipped version of _MAP from above + * needed to check if we should use keypress or not when no action + * is specified + * + * @type {Object|undefined} + */ + _REVERSE_MAP, + + /** + * a list of all the callbacks setup via Mousetrap.bind() + * + * @type {Object} + */ + _callbacks = {}, + + /** + * direct map of string combinations to callbacks used for trigger() + * + * @type {Object} + */ + _direct_map = {}, + + /** + * keeps track of what level each sequence is at since multiple + * sequences can start out with the same sequence + * + * @type {Object} + */ + _sequence_levels = {}, + + /** + * variable to store the setTimeout call + * + * @type {null|number} + */ + _reset_timer, + + /** + * temporary state where we will ignore the next keyup + * + * @type {boolean|string} + */ + _ignore_next_keyup = false, - if (input != null) { - if (typeof input === 'string') { - input = this.lang().monthsParse(input); - if (typeof input !== 'number') { - return this; - } - } + /** + * are we currently inside of a sequence? + * type of action ("keyup" or "keydown" or "keypress") or false + * + * @type {boolean|string} + */ + _inside_sequence = false; - dayOfMonth = this.date(); - this.date(1); - this._d['set' + utc + 'Month'](input); - this.date(Math.min(dayOfMonth, this.daysInMonth())); + /** + * loop through the f keys, f1 to f19 and add them to the map + * programatically + */ + for (var i = 1; i < 20; ++i) { + _MAP[111 + i] = 'f' + i; + } - moment.updateOffset(this); - return this; - } else { - return this._d['get' + utc + 'Month'](); - } - }, + /** + * loop through to map numbers on the numeric keypad + */ + for (i = 0; i <= 9; ++i) { + _MAP[i + 96] = i; + } - 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 '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 */ - } + /** + * cross browser add event method + * + * @param {Element|HTMLDocument} object + * @param {string} type + * @param {Function} callback + * @returns void + */ + function _addEvent(object, type, callback) { + if (object.addEventListener) { + return object.addEventListener(type, callback, false); + } - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } + object.attachEvent('on' + type, callback); + } - return this; - }, + /** + * takes the event and returns the key character + * + * @param {Event} e + * @return {string} + */ + function _characterFromEvent(e) { - endOf: function (units) { - units = normalizeUnits(units); - return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1); - }, + // for keypress events we should return the character as is + if (e.type == 'keypress') { + return String.fromCharCode(e.which); + } - isAfter: function (input, units) { - units = typeof units !== 'undefined' ? units : 'millisecond'; - return +this.clone().startOf(units) > +moment(input).startOf(units); - }, + // for non keypress events the special maps are needed + if (_MAP[e.which]) { + return _MAP[e.which]; + } - isBefore: function (input, units) { - units = typeof units !== 'undefined' ? units : 'millisecond'; - return +this.clone().startOf(units) < +moment(input).startOf(units); - }, + if (_KEYCODE_MAP[e.which]) { + return _KEYCODE_MAP[e.which]; + } - isSame: function (input, units) { - units = units || 'ms'; - return +this.clone().startOf(units) === +makeAs(input, this).startOf(units); - }, + // if it is not in the special map + return String.fromCharCode(e.which).toLowerCase(); + } - min: function (other) { - other = moment.apply(null, arguments); - return other < this ? this : other; - }, + /** + * should we stop this event before firing off callbacks + * + * @param {Event} e + * @return {boolean} + */ + function _stop(e) { + var element = e.target || e.srcElement, + tag_name = element.tagName; - max: function (other) { - other = moment.apply(null, arguments); - return other > this ? this : other; - }, + // if the element has the class "mousetrap" then no need to stop + if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { + return false; + } - zone : function (input) { - var offset = this._offset || 0; - if (input != null) { - if (typeof input === "string") { - input = timezoneMinutesFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - this._offset = input; - this._isUTC = true; - if (offset !== input) { - addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true); - } - } else { - return this._isUTC ? offset : this._d.getTimezoneOffset(); - } - return this; - }, + // stop for input, select, and textarea + return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); + } - zoneAbbr : function () { - return this._isUTC ? "UTC" : ""; - }, + /** + * checks if two arrays are equal + * + * @param {Array} modifiers1 + * @param {Array} modifiers2 + * @returns {boolean} + */ + function _modifiersMatch(modifiers1, modifiers2) { + return modifiers1.sort().join(',') === modifiers2.sort().join(','); + } - zoneName : function () { - return this._isUTC ? "Coordinated Universal Time" : ""; - }, + /** + * resets all sequence counters except for the ones passed in + * + * @param {Object} do_not_reset + * @returns void + */ + function _resetSequences(do_not_reset) { + do_not_reset = do_not_reset || {}; - parseZone : function () { - if (this._tzm) { - this.zone(this._tzm); - } else if (typeof this._i === 'string') { - this.zone(this._i); - } - return this; - }, + var active_sequences = false, + key; - hasAlignedHourOffset : function (input) { - if (!input) { - input = 0; - } - else { - input = moment(input).zone(); - } + for (key in _sequence_levels) { + if (do_not_reset[key]) { + active_sequences = true; + continue; + } + _sequence_levels[key] = 0; + } - return (this.zone() - input) % 60 === 0; - }, + if (!active_sequences) { + _inside_sequence = false; + } + } - daysInMonth : function () { - return daysInMonth(this.year(), this.month()); - }, + /** + * finds all callbacks that match based on the keycode, modifiers, + * and action + * + * @param {string} character + * @param {Array} modifiers + * @param {string} action + * @param {boolean=} remove - should we remove any matches + * @param {string=} combination + * @returns {Array} + */ + function _getMatches(character, modifiers, action, remove, combination) { + var i, + callback, + matches = []; + + // if there are no events related to this keycode + if (!_callbacks[character]) { + return []; + } - dayOfYear : function (input) { - var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); - }, + // if a modifier key is coming up on its own we should allow it + if (action == 'keyup' && _isModifier(character)) { + modifiers = [character]; + } - quarter : function () { - return Math.ceil((this.month() + 1.0) / 3.0); - }, + // loop through all callbacks for the key that was pressed + // and see if any of them match + for (i = 0; i < _callbacks[character].length; ++i) { + callback = _callbacks[character][i]; - weekYear : function (input) { - var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year; - return input == null ? year : this.add("y", (input - year)); - }, + // if this is a sequence but it is not at the right level + // then move onto the next match + if (callback.seq && _sequence_levels[callback.seq] != callback.level) { + continue; + } - isoWeekYear : function (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add("y", (input - year)); - }, + // if the action we are looking for doesn't match the action we got + // then we should keep going + if (action != callback.action) { + continue; + } - week : function (input) { - var week = this.lang().week(this); - return input == null ? week : this.add("d", (input - week) * 7); - }, + // if this is a keypress event that means that we need to only + // look at the character, otherwise check the modifiers as + // well + if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) { - isoWeek : function (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add("d", (input - week) * 7); - }, + // remove is used so if you change your mind and call bind a + // second time with a new function the first one is overwritten + if (remove && callback.combo == combination) { + _callbacks[character].splice(i, 1); + } - weekday : function (input) { - var weekday = (this.day() + 7 - this.lang()._week.dow) % 7; - return input == null ? weekday : this.add("d", input - weekday); - }, + matches.push(callback); + } + } - 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 matches; + } - get : function (units) { - units = normalizeUnits(units); - return this[units](); - }, + /** + * takes a key event and figures out what the modifiers are + * + * @param {Event} e + * @returns {Array} + */ + function _eventModifiers(e) { + var modifiers = []; - set : function (units, value) { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - this[units](value); - } - return this; - }, + if (e.shiftKey) { + modifiers.push('shift'); + } - // If passed a language key, it will set the language for this - // instance. Otherwise, it will return the language configuration - // variables for this instance. - lang : function (key) { - if (key === undefined) { - return this._lang; - } else { - this._lang = getLangDefinition(key); - return this; - } - } - }); + if (e.altKey) { + modifiers.push('alt'); + } - // helper for adding shortcuts - function makeGetterAndSetter(name, key) { - moment.fn[name] = moment.fn[name + 's'] = function (input) { - var utc = this._isUTC ? 'UTC' : ''; - if (input != null) { - this._d['set' + utc + key](input); - moment.updateOffset(this); - return this; - } else { - return this._d['get' + utc + key](); - } - }; - } + if (e.ctrlKey) { + modifiers.push('ctrl'); + } - // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) - for (i = 0; i < proxyGettersAndSetters.length; i ++) { - makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]); - } + if (e.metaKey) { + modifiers.push('meta'); + } - // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') - makeGetterAndSetter('year', 'FullYear'); + return modifiers; + } + + /** + * actually calls the callback function + * + * if your callback function returns false this will use the jquery + * convention - prevent default and stop propogation on the event + * + * @param {Function} callback + * @param {Event} e + * @returns void + */ + function _fireCallback(callback, e) { + if (callback(e) === false) { + if (e.preventDefault) { + e.preventDefault(); + } - // 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; + if (e.stopPropagation) { + e.stopPropagation(); + } - // add aliased format methods - moment.fn.toJSON = moment.fn.toISOString; + e.returnValue = false; + e.cancelBubble = true; + } + } - /************************************ - Duration Prototype - ************************************/ + /** + * handles a character key event + * + * @param {string} character + * @param {Event} e + * @returns void + */ + function _handleCharacter(character, e) { + // if this event should not happen stop here + if (_stop(e)) { + return; + } - extend(moment.duration.fn = Duration.prototype, { + var callbacks = _getMatches(character, _eventModifiers(e), e.type), + i, + do_not_reset = {}, + processed_sequence_callback = false; + + // loop through matching callbacks for this key event + for (i = 0; i < callbacks.length; ++i) { + + // fire for all sequence callbacks + // this is because if for example you have multiple sequences + // bound such as "g i" and "g t" they both need to fire the + // callback for matching g cause otherwise you can only ever + // match the first one + if (callbacks[i].seq) { + processed_sequence_callback = true; + + // keep a list of which sequences were matches for later + do_not_reset[callbacks[i].seq] = 1; + _fireCallback(callbacks[i].callback, e); + continue; + } - _bubble : function () { - var milliseconds = this._milliseconds, - days = this._days, - months = this._months, - data = this._data, - seconds, minutes, hours, years; + // if there were no sequence matches but we are still here + // that means this is a regular match so we should fire that + if (!processed_sequence_callback && !_inside_sequence) { + _fireCallback(callbacks[i].callback, e); + } + } - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + // if you are inside of a sequence and the key you are pressing + // is not a modifier key then we should reset all sequences + // that were not matched by this key event + if (e.type == _inside_sequence && !_isModifier(character)) { + _resetSequences(do_not_reset); + } + } - seconds = absRound(milliseconds / 1000); - data.seconds = seconds % 60; + /** + * handles a keydown event + * + * @param {Event} e + * @returns void + */ + function _handleKey(e) { - minutes = absRound(seconds / 60); - data.minutes = minutes % 60; + // normalize e.which for key events + // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion + e.which = typeof e.which == "number" ? e.which : e.keyCode; - hours = absRound(minutes / 60); - data.hours = hours % 24; + var character = _characterFromEvent(e); - days += absRound(hours / 24); - data.days = days % 30; + // no character found then stop + if (!character) { + return; + } - months += absRound(days / 30); - data.months = months % 12; + if (e.type == 'keyup' && _ignore_next_keyup == character) { + _ignore_next_keyup = false; + return; + } - years = absRound(months / 12); - data.years = years; - }, + _handleCharacter(character, e); + } - weeks : function () { - return absRound(this.days() / 7); - }, + /** + * determines if the keycode specified is a modifier key or not + * + * @param {string} key + * @returns {boolean} + */ + function _isModifier(key) { + return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; + } - valueOf : function () { - return this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6; - }, + /** + * called to set a 1 second timeout on the specified sequence + * + * this is so after each key press in the sequence you have 1 second + * to press the next key before you have to start over + * + * @returns void + */ + function _resetSequenceTimer() { + clearTimeout(_reset_timer); + _reset_timer = setTimeout(_resetSequences, 1000); + } - humanize : function (withSuffix) { - var difference = +this, - output = relativeTime(difference, !withSuffix, this.lang()); + /** + * reverses the map lookup so that we can look for specific keys + * to see what can and can't use keypress + * + * @return {Object} + */ + function _getReverseMap() { + if (!_REVERSE_MAP) { + _REVERSE_MAP = {}; + for (var key in _MAP) { + + // pull out the numeric keypad from here cause keypress should + // be able to detect the keys from the character + if (key > 95 && key < 112) { + continue; + } + + if (_MAP.hasOwnProperty(key)) { + _REVERSE_MAP[_MAP[key]] = key; + } + } + } + return _REVERSE_MAP; + } - if (withSuffix) { - output = this.lang().pastFuture(difference, output); - } + /** + * picks the best action based on the key combination + * + * @param {string} key - character for key + * @param {Array} modifiers + * @param {string=} action passed in + */ + function _pickBestAction(key, modifiers, action) { - return this.lang().postformat(output); - }, + // if no action was picked in we should try to pick the one + // that we think would work best for this key + if (!action) { + action = _getReverseMap()[key] ? 'keydown' : 'keypress'; + } - add : function (input, val) { - // supports only 2.0-style add(1, 's') or add(moment) - var dur = moment.duration(input, val); + // modifier keys don't work as expected with keypress, + // switch to keydown + if (action == 'keypress' && modifiers.length) { + action = 'keydown'; + } - this._milliseconds += dur._milliseconds; - this._days += dur._days; - this._months += dur._months; + return action; + } - this._bubble(); + /** + * binds a key sequence to an event + * + * @param {string} combo - combo specified in bind call + * @param {Array} keys + * @param {Function} callback + * @param {string=} action + * @returns void + */ + function _bindSequence(combo, keys, callback, action) { - return this; - }, + // start off by adding a sequence level record for this combination + // and setting the level to 0 + _sequence_levels[combo] = 0; - subtract : function (input, val) { - var dur = moment.duration(input, val); + // if there is no action pick the best one for the first key + // in the sequence + if (!action) { + action = _pickBestAction(keys[0], []); + } - this._milliseconds -= dur._milliseconds; - this._days -= dur._days; - this._months -= dur._months; + /** + * callback to increase the sequence level for this sequence and reset + * all other sequences that were active + * + * @param {Event} e + * @returns void + */ + var _increaseSequence = function(e) { + _inside_sequence = action; + ++_sequence_levels[combo]; + _resetSequenceTimer(); + }, + + /** + * wraps the specified callback inside of another function in order + * to reset all sequence counters as soon as this sequence is done + * + * @param {Event} e + * @returns void + */ + _callbackAndReset = function(e) { + _fireCallback(callback, e); + + // we should ignore the next key up if the action is key down + // or keypress. this is so if you finish a sequence and + // release the key the final key will not trigger a keyup + if (action !== 'keyup') { + _ignore_next_keyup = _characterFromEvent(e); + } + + // weird race condition if a sequence ends with the key + // another sequence begins with + setTimeout(_resetSequences, 10); + }, + i; + + // loop through keys one at a time and bind the appropriate callback + // function. for any key leading up to the final one it should + // increase the sequence. after the final, it should reset all sequences + for (i = 0; i < keys.length; ++i) { + _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i); + } + } - this._bubble(); + /** + * binds a single keyboard combination + * + * @param {string} combination + * @param {Function} callback + * @param {string=} action + * @param {string=} sequence_name - name of sequence if part of sequence + * @param {number=} level - what part of the sequence the command is + * @returns void + */ + function _bindSingle(combination, callback, action, sequence_name, level) { - return this; - }, + // make sure multiple spaces in a row become a single space + combination = combination.replace(/\s+/g, ' '); - get : function (units) { - units = normalizeUnits(units); - return this[units.toLowerCase() + 's'](); - }, + var sequence = combination.split(' '), + i, + key, + keys, + modifiers = []; - as : function (units) { - units = normalizeUnits(units); - return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's'](); - }, + // if this pattern is a sequence of keys then run through this method + // to reprocess each pattern one key at a time + if (sequence.length > 1) { + return _bindSequence(combination, sequence, callback, action); + } - lang : moment.fn.lang, + // take the keys from this pattern and figure out what the actual + // pattern is all about + keys = combination === '+' ? ['+'] : combination.split('+'); - 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); + for (i = 0; i < keys.length; ++i) { + key = keys[i]; - if (!this.asSeconds()) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } + // normalize key names + if (_SPECIAL_ALIASES[key]) { + key = _SPECIAL_ALIASES[key]; + } - 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' : ''); - } - }); + // if this is not a keypress event then we should + // be smart about using shift keys + // this will only work for US keyboards however + if (action && action != 'keypress' && _SHIFT_MAP[key]) { + key = _SHIFT_MAP[key]; + modifiers.push('shift'); + } - function makeDurationGetter(name) { - moment.duration.fn[name] = function () { - return this._data[name]; - }; - } + // if this key is a modifier then add it to the list of modifiers + if (_isModifier(key)) { + modifiers.push(key); + } + } - function makeDurationAsGetter(name, factor) { - moment.duration.fn['as' + name] = function () { - return +this / factor; - }; - } + // depending on what the key combination is + // we will try to pick the best event for it + action = _pickBestAction(key, modifiers, action); - for (i in unitMillisecondFactors) { - if (unitMillisecondFactors.hasOwnProperty(i)) { - makeDurationAsGetter(i, unitMillisecondFactors[i]); - makeDurationGetter(i.toLowerCase()); - } - } + // make sure to initialize array if this is the first time + // a callback is added for this key + if (!_callbacks[key]) { + _callbacks[key] = []; + } - makeDurationAsGetter('Weeks', 6048e5); - moment.duration.fn.asMonths = function () { - return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12; - }; + // remove an existing match if there is one + _getMatches(key, modifiers, action, !sequence_name, combination); + + // add this call back to the array + // if it is a sequence put it at the beginning + // if not put it at the end + // + // this is important because the way these are processed expects + // the sequence ones to come first + _callbacks[key][sequence_name ? 'unshift' : 'push']({ + callback: callback, + modifiers: modifiers, + action: action, + seq: sequence_name, + level: level, + combo: combination + }); + } + /** + * binds multiple combinations to the same callback + * + * @param {Array} combinations + * @param {Function} callback + * @param {string|undefined} action + * @returns void + */ + function _bindMultiple(combinations, callback, action) { + for (var i = 0; i < combinations.length; ++i) { + _bindSingle(combinations[i], callback, action); + } + } - /************************************ - Default Lang - ************************************/ + // start! + _addEvent(document, 'keypress', _handleKey); + _addEvent(document, 'keydown', _handleKey); + _addEvent(document, 'keyup', _handleKey); + var mousetrap = { - // Set default language, other languages will inherit from English. - moment.lang('en', { - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; - } - }); + /** + * binds an event to mousetrap + * + * can be a single key, a combination of keys separated with +, + * a comma separated list of keys, an array of keys, or + * a sequence of keys separated by spaces + * + * be sure to list the modifier keys first to make sure that the + * correct key ends up getting bound (the last key in the pattern) + * + * @param {string|Array} keys + * @param {Function} callback + * @param {string=} action - 'keypress', 'keydown', or 'keyup' + * @returns void + */ + bind: function(keys, callback, action) { + _bindMultiple(keys instanceof Array ? keys : [keys], callback, action); + _direct_map[keys + ':' + action] = callback; + return this; + }, - /* EMBED_LANGUAGES */ + /** + * unbinds an event to mousetrap + * + * the unbinding sets the callback function of the specified key combo + * to an empty function and deletes the corresponding key in the + * _direct_map dict. + * + * the keycombo+action has to be exactly the same as + * it was defined in the bind method + * + * TODO: actually remove this from the _callbacks dictionary instead + * of binding an empty function + * + * @param {string|Array} keys + * @param {string} action + * @returns void + */ + unbind: function(keys, action) { + if (_direct_map[keys + ':' + action]) { + delete _direct_map[keys + ':' + action]; + this.bind(keys, function() {}, action); + } + return this; + }, - /************************************ - Exposing Moment - ************************************/ + /** + * triggers an event that has already been bound + * + * @param {string} keys + * @param {string=} action + * @returns void + */ + trigger: function(keys, action) { + _direct_map[keys + ':' + action](); + return this; + }, - function makeGlobal(deprecate) { - var warned = false, local_moment = moment; - /*global ender:false */ - if (typeof ender !== 'undefined') { - return; - } - // here, `this` means `window` in the browser, or `global` on the server - // add `moment` as a global object via a string identifier, - // for Closure Compiler "advanced" mode - if (deprecate) { - global.moment = function () { - if (!warned && console && console.warn) { - warned = true; - console.warn( - "Accessing Moment through the global scope is " + - "deprecated, and will be removed in an upcoming " + - "release."); - } - return local_moment.apply(null, arguments); - }; - extend(global.moment, local_moment); - } else { - global['moment'] = moment; - } - } + /** + * resets the library back to its initial state. this is useful + * if you want to clear out the current keyboard shortcuts and bind + * new ones - for example if you switch to another page + * + * @returns void + */ + reset: function() { + _callbacks = {}; + _direct_map = {}; + return this; + } + }; - // CommonJS module is defined - if (hasModule) { - module.exports = moment; - makeGlobal(true); - } else if (typeof define === "function" && define.amd) { - define("moment", function (require, exports, module) { - if (module.config && module.config() && module.config().noGlobal !== true) { - // If user provided noGlobal, he is aware of global - makeGlobal(module.config().noGlobal === undefined); - } +module.exports = mousetrap; - return moment; - }); - } else { - makeGlobal(); - } -}).call(this); },{}]},{},[1]) (1) diff --git a/dist/vis.min.js b/dist/vis.min.js index 8ca53878..083f0f1f 100644 --- a/dist/vis.min.js +++ b/dist/vis.min.js @@ -4,11 +4,11 @@ * * A dynamic, browser-based visualization library. * - * @version 0.3.0 - * @date 2014-01-14 + * @version 0.4.0 + * @date 2014-01-31 * * @license - * Copyright (C) 2011-2013 Almende B.V, http://almende.com + * Copyright (C) 2011-2014 Almende B.V, http://almende.com * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy @@ -22,8 +22,10 @@ * License for the specific language governing permissions and limitations under * the License. */ -!function(t){if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.vis=t()}}(function(){var t;return function e(t,i,n){function s(o,a){if(!i[o]){if(!t[o]){var h="function"==typeof require&&require;if(!a&&h)return h(o,!0);if(r)return r(o,!0);throw new Error("Cannot find module '"+o+"'")}var d=i[o]={exports:{}};t[o][0].call(d.exports,function(e){var i=t[o][1][e];return s(i?i:e)},d,d.exports,e,t,i,n)}return i[o].exports}for(var r="function"==typeof require&&require,o=0;oi;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,s;if(null==this)throw new TypeError(" this is null or not defined");var r=Object(this),o=r.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=new Array(o),s=0;o>s;){var a,h;s in r&&(a=r[s],h=t.call(i,a,s,r),n[s]=h),s++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],s=arguments[1],r=0;i>r;r++)if(r in e){var o=e[r];t.call(s,o,r,e)&&n.push(o)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(s){if("object"!=typeof s&&"function"!=typeof s||null===s)throw new TypeError("Object.keys called on non-object");var r=[];for(var o in s)t.call(s,o)&&r.push(o);if(e)for(var a=0;n>a;a++)t.call(s,i[a])&&r.push(i[a]);return r}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s});var A={};A.isNumber=function(t){return t instanceof Number||"number"==typeof t},A.isString=function(t){return t instanceof String||"string"==typeof t},A.isDate=function(t){if(t instanceof Date)return!0;if(A.isString(t)){var e=P.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},A.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},A.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},A.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var s in n)n.hasOwnProperty(s)&&void 0!==n[s]&&(t[s]=n[s])}return t},A.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(A.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(I.isMoment(t))return new Date(t.valueOf());if(A.isString(t))return i=P.exec(t),i?new Date(Number(i[1])):I(t).toDate();throw new Error("Cannot convert object of type "+A.getType(t)+" to type Date");case"Moment":if(A.isNumber(t))return I(t);if(t instanceof Date)return I(t.valueOf());if(I.isMoment(t))return I(t);if(A.isString(t))return i=P.exec(t),i?I(Number(i[1])):I(t);throw new Error("Cannot convert object of type "+A.getType(t)+" to type Date");case"ISODate":if(A.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(I.isMoment(t))return t.toDate().toISOString();if(A.isString(t))return i=P.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+A.getType(t)+" to type ISODate");case"ASPDate":if(A.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(A.isString(t)){i=P.exec(t);var n;return n=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+n+")/"}throw new Error("Cannot convert object of type "+A.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+A.getType(t)+' to type "'+e+'"')}};var P=/^\/?Date\((\-?\d+)/i;A.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},A.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetLeft,n-=s.scrollLeft,s=s.offsetParent;return n},A.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetTop,n-=s.scrollTop,s=s.offsetParent;return n},A.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)},A.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0)},A.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},A.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},A.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},A.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},A.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},A.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},A.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},A.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},A.fakeGesture=function(t,e){var i=null;return L.event.collectEventData(this,i,e)},A.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},A.option={},A.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},A.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},A.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},A.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),A.isString(t)?t:A.isNumber(t)?t+"px":e||null},A.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null};var Y={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var s=e[i];if(s&&s.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];s||(s={object:t,events:{}},this.listeners.push(s));var r=s.events[e];r||(r=[],s.events[e]=r),-1==r.indexOf(i)&&r.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var r=s.events[e];r&&(n=r.indexOf(i),-1!=n&&r.splice(n,1),0==r.length&&delete s.events[e]);var o=0,a=s.events;for(var h in a)a.hasOwnProperty(h)&&o++;0==o&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var r=s.events[e];if(r)for(var o=0,a=r.length;a>o;o++)r[o](i)}}};s.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:new RegExp(t.replace("*","\\w+")),s={id:A.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(s),s.id},s.prototype.off=function(t){for(var e=0;er;r++)i=s._addItem(t[r]),n.push(i);else if(A.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var c={},u=0,l=a.length;l>u;u++){var p=a[u];c[p]=t.getValue(h,u)}i=s._addItem(c),n.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=s._addItem(t),n.push(i)}return n.length&&this._trigger("add",{items:n},e),n},r.prototype.update=function(t,e){var i=[],n=[],s=this,r=s.fieldId,o=function(t){var e=t[r];s.data[e]?(e=s._updateItem(t),n.push(e)):(e=s._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)o(t[a]);else if(A.isDataTable(t))for(var d=this._getColumnNames(t),c=0,u=t.getNumberOfRows();u>c;c++){for(var l={},p=0,f=d.length;f>p;p++){var m=d[p];l[m]=t.getValue(c,p)}o(l)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");o(t)}return i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e),i.concat(n)},r.prototype.get=function(){var t,e,i,n,s=this,r=A.getType(arguments[0]);"String"==r||"Number"==r?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==r?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var o;if(i&&i.type){if(o="DataTable"==i.type?"DataTable":"Array",n&&o!=A.getType(n))throw new Error('Type of parameter "data" ('+A.getType(n)+") does not correspond with specified options.type ("+i.type+")");if("DataTable"==o&&!A.isDataTable(n))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else o=n?"DataTable"==A.getType(n)?"DataTable":"Array":"Array";var a,h,d,c,u=i&&i.convert||this.options.convert,l=i&&i.filter,p=[];if(void 0!=t)a=s._getItem(t,u),l&&!l(a)&&(a=null);else if(void 0!=e)for(d=0,c=e.length;c>d;d++)a=s._getItem(e[d],u),(!l||l(a))&&p.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=s._getItem(h,u),(!l||l(a))&&p.push(a));if(i&&i.order&&void 0==t&&this._sort(p,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,c=p.length;c>d;d++)p[d]=this._filterFields(p[d],f)}if("DataTable"==o){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,a);else for(d=0,c=p.length;c>d;d++)s._appendRow(n,m,p[d]);return n}if(void 0!=t)return a;if(n){for(d=0,c=p.length;c>d;d++)n.push(p[d]);return n}return p},r.prototype.getIds=function(t){var e,i,n,s,r,o=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,c=[];if(a)if(h){r=[];for(n in o)o.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&r.push(s));for(this._sort(r,h),e=0,i=r.length;i>e;e++)c[e]=r[e][this.fieldId]}else for(n in o)o.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&c.push(s[this.fieldId]));else if(h){r=[];for(n in o)o.hasOwnProperty(n)&&r.push(o[n]);for(this._sort(r,h),e=0,i=r.length;i>e;e++)c[e]=r[e][this.fieldId]}else for(n in o)o.hasOwnProperty(n)&&(s=o[n],c.push(s[this.fieldId]));return c},r.prototype.forEach=function(t,e){var i,n,s=e&&e.filter,r=e&&e.convert||this.options.convert,o=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in o)o.hasOwnProperty(n)&&(i=this._getItem(n,r),(!s||s(i))&&t(i,n))},r.prototype.map=function(t,e){var i,n=e&&e.filter,s=e&&e.convert||this.options.convert,r=[],o=this.data;for(var a in o)o.hasOwnProperty(a)&&(i=this._getItem(a,s),(!n||n(i))&&r.push(t(i,a)));return e&&e.order&&this._sort(r,e.order),r},r.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},r.prototype._sort=function(t,e){if(A.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],s=e[i];return n>s?1:s>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},r.prototype.remove=function(t,e){var i,n,s,r=[];if(t instanceof Array)for(i=0,n=t.length;n>i;i++)s=this._remove(t[i]),null!=s&&r.push(s);else s=this._remove(t),null!=s&&r.push(s);return r.length&&this._trigger("remove",{items:r},e),r},r.prototype._remove=function(t){if(A.isNumber(t)||A.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},r.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},r.prototype.max=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var r=e[s],o=r[t];null!=o&&(!i||o>n)&&(i=r,n=o)}return i},r.prototype.min=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var r=e[s],o=r[t];null!=o&&(!i||n>o)&&(i=r,n=o)}return i},r.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.convert[t],s=0;for(var r in e)if(e.hasOwnProperty(r)){for(var o=e[r],a=A.convert(o[t],n),h=!1,d=0;s>d;d++)if(i[d]==a){h=!0;break}h||(i[s]=a,s++)}return i},r.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=A.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=A.convert(t[n],s)}return this.data[e]=i,e},r.prototype._getItem=function(t,e){var i,n,s=this.data[t];if(!s)return null;var r={},o=this.fieldId,a=this.internalIds;if(e)for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==o&&n in a||(r[i]=A.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==o&&n in a||(r[i]=n));return r},r.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=A.convert(t[n],s)}return e},r.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},r.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),s=0,r=e.length;r>s;s++){var o=e[s];t.setValue(n,s,i[o])}},o.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var s in this.ids)this.ids.hasOwnProperty(s)&&e.push(s);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)s=e[i],this.ids[s]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},o.prototype.get=function(){var t,e,i,n=this,s=A.getType(arguments[0]);"String"==s||"Number"==s||"Array"==s?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var r=A.extend({},this.options,e);this.options.filter&&e&&e.filter&&(r.filter=function(t){return n.options.filter(t)&&e.filter(t)});var o=[];return void 0!=t&&o.push(t),o.push(r),o.push(i),this.data&&this.data.get.apply(this.data,o)},o.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},o.prototype._onEvent=function(t,e,i){var n,s,r,o,a=e&&e.items,h=this.data,d=[],c=[],u=[];if(a&&h){switch(t){case"add":for(n=0,s=a.length;s>n;n++)r=a[n],o=this.get(r),o&&(this.ids[r]=!0,d.push(r));break;case"update":for(n=0,s=a.length;s>n;n++)r=a[n],o=this.get(r),o?this.ids[r]?c.push(r):(this.ids[r]=!0,d.push(r)):this.ids[r]&&(delete this.ids[r],u.push(r));break;case"remove":for(n=0,s=a.length;s>n;n++)r=a[n],this.ids[r]&&(delete this.ids[r],u.push(r))}d.length&&this._trigger("add",{items:d},i),c.length&&this._trigger("update",{items:c},i),u.length&&this._trigger("remove",{items:u},i)}},o.prototype.subscribe=r.prototype.subscribe,o.prototype.unsubscribe=r.prototype.unsubscribe,o.prototype._trigger=r.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+1e3*this.step*60);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+1e3*this.step*60*60);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,s=36e5,r=6e4,o=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),r>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),o>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return I(t).format("SSS");case TimeStep.SCALE.SECOND:return I(t).format("s");case TimeStep.SCALE.MINUTE:return I(t).format("HH:mm");case TimeStep.SCALE.HOUR:return I(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return I(t).format("ddd D");case TimeStep.SCALE.DAY:return I(t).format("D");case TimeStep.SCALE.MONTH:return I(t).format("MMM");case TimeStep.SCALE.YEAR:return I(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return I(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return I(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return I(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return I(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return I(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){A.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw new Error("Cannot stack items: parent does not contain items");var e=[],i=0;A.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw new Error("Option order must be a function");e.sort(n),this.ordered=e},a.prototype._stack=function(){var t,e,i,n=this.ordered,s=this.options,r=s.orientation||this.defaultOptions.orientation,o="top"==r;for(i=s.margin&&void 0!==s.margin.item?s.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=o?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,n,s){for(var r=this.collision,o=t[e],a=n;a>=i;a--){var h=t[a];if(r(o,h,s)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){A.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},h.prototype.subscribe=function(t,e,i){function n(e){s._onMouseWheel(e,t,i)}var s=this;if("move"==e)t.on("dragstart",function(e){s._onDragStart(e,t)}),t.on("drag",function(e){s._onDrag(e,t,i)}),t.on("dragend",function(e){s._onDragEnd(e,t)});else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". Choose "move" or "zoom".');t.on("mousewheel",n),t.on("DOMMouseScroll",n),t.on("touch",function(){s._onTouch()}),t.on("pinch",function(e){s._onPinch(e,t,i)})}},h.prototype.on=function(t,e){Y.addListener(this,t,e)},h.prototype._trigger=function(t){Y.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,n=null!=t?A.convert(t,"Number"):this.start,s=null!=e?A.convert(e,"Number"):this.end,r=null!=this.options.max?A.convert(this.options.max,"Date").valueOf():null,o=null!=this.options.min?A.convert(this.options.min,"Date").valueOf():null;if(isNaN(n)||null===n)throw new Error('Invalid start "'+t+'"');if(isNaN(s)||null===s)throw new Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!==o&&o>n&&(i=o-n,n+=i,s+=i,null!=r&&s>r&&(s=r)),null!==r&&s>r&&(i=s-r,n-=i,s-=i,null!=o&&o>n&&(n=o)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>s-n&&(this.end-this.start===a?(n=this.start,s=this.end):(i=a-(s-n),n-=i/2,s+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),s-n>h&&(this.end-this.start===h?(n=this.start,s=this.end):(i=s-n-h,n+=i/2,s-=i/2))}var d=this.start!=n||this.end!=s;return this.start=n,this.end=s,d},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&e-t!=0?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var F={};h.prototype._onDragStart=function(t,e){if(!F.pinching){F.start=this.start,F.end=this.end;var i=e.frame;i&&(i.style.cursor="move")}},h.prototype._onDrag=function(t,e,i){if(d(i),!F.pinching){var n="horizontal"==i?t.gesture.deltaX:t.gesture.deltaY,s=F.end-F.start,r="horizontal"==i?e.width:e.height,o=-n/r*s;this._applyRange(F.start+o,F.end+o),this._trigger("rangechange")}},h.prototype._onDragEnd=function(t,e){F.pinching||(e.frame&&(e.frame.style.cursor="auto"),this._trigger("rangechanged"))},h.prototype._onMouseWheel=function(t,e,i){d(i);var n=0;if(t.wheelDelta?n=t.wheelDelta/120:t.detail&&(n=-t.detail/3),n){var s;s=0>n?1-n/5:1/(1+n/5);var r=A.fakeGesture(this,t),o=c(r.touches[0],e.frame),a=this._pointerToDate(e,i,o);this.zoom(s,a)}A.preventDefault(t)},h.prototype._onTouch=function(){F.start=this.start,F.end=this.end,F.pinching=!1,F.center=null},h.prototype._onPinch=function(t,e,i){if(F.pinching=!0,t.gesture.touches.length>1){F.center||(F.center=c(t.gesture.center,e.frame));var n=1/t.gesture.scale,s=this._pointerToDate(e,i,F.center),r=c(t.gesture.center,e.frame),o=(this._pointerToDate(e,i,r),parseInt(s+(F.start-s)*n)),a=parseInt(s+(F.end-s)*n);this.setRange(o,a)}},h.prototype._pointerToDate=function(t,e,i){var n;if("horizontal"==e){var s=t.width;return n=this.conversion(s),i.x/n.scale+n.offset}var r=t.height;return n=this.conversion(r),i.y/n.scale+n.offset},h.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,n=e+(this.end-e)*t;this.setRange(i,n)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,n=this.start-i,s=this.end-i;this.setRange(n,s)},u.prototype.add=function(t){if(void 0==t.id)throw new Error("Component has no field id");if(!(t instanceof l||t instanceof u))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},u.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},u.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},u.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},u.prototype.repaint=function H(){function H(i,n){n in e||(i.depends&&i.depends.forEach(function(t){H(t,t.id)}),i.parent&&H(i.parent,i.parent.id),t=i.repaint()||t,e[n]=!0)}var t=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var e={};A.forEach(this.components,H),t&&this.reflow()},u.prototype.reflow=function z(){function z(i,n){n in e||(i.depends&&i.depends.forEach(function(t){z(t,t.id)}),i.parent&&z(i.parent,i.parent.id),t=i.reflow()||t,e[n]=!0)}var t=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var e={};A.forEach(this.components,z),t&&this.repaint()},l.prototype.setOptions=function(t){t&&(A.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},l.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},l.prototype.getContainer=function(){return null},l.prototype.getFrame=function(){return this.frame},l.prototype.repaint=function(){return!1},l.prototype.reflow=function(){return!1},l.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},l.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},l.prototype.requestRepaint=function(){if(!this.controller)throw new Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},l.prototype.requestReflow=function(){if(!this.controller)throw new Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new l,p.prototype.setOptions=l.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="panel";var r=n.className;r&&("function"==typeof r?A.addClassName(s,String(r())):A.addClassName(s,String(r))),this.frame=s,t+=1}if(!s.parentNode){if(!this.parent)throw new Error("Cannot repaint panel: no parent attached");var o=this.parent.getContainer();if(!o)throw new Error("Cannot repaint panel: parent has no container element");o.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=A.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype=new p,f.prototype.setOptions=l.prototype.setOptions,f.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.frame;if(s||(s=document.createElement("div"),this.frame=s,t+=1),!s.parentNode){if(!this.container)throw new Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}s.className="vis timeline rootpanel "+n.orientation;var r=n.className;return r&&A.addClassName(s,A.option.asString(r)),t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},f.prototype.reflow=function(){var t=0,e=A.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},f.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};A.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},f.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},f.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},f.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;A.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var s=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=s,t.hammer||(t.hammer=L(n,{prevent_default:!0})),t.hammer.on(i,s)}}})}},m.prototype=new l,m.prototype.setOptions=l.prototype.setOptions,m.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},m.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},m.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},m.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.getOption("orientation"),r=this.props,o=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis",!a.parentNode){if(!this.parent)throw new Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw new Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var c=a.nextSibling;d.removeChild(a);var u="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,u)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),o.first();for(var l=void 0,p=0;o.hasNext()&&1e3>p;){p++;var f=o.getCurrent(),m=this.toScreen(f),g=o.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,o.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==l&&(l=m),this._repaintMajorText(m,o.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),o.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=o.getLabelMajor(v),w=y.length*(r.majorCharWidth||10)+10;(void 0==l||l>w)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),c?d.insertBefore(a,c):d.appendChild(a)}return t>0},m.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},m.prototype._repaintEnd=function(){A.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},m.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},m.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},m.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},m.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},m.prototype._repaintLine=function(){{var t=this.dom.line,e=this.frame;this.options}this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&t.parentElement&&(e.removeChild(t.line),delete this.dom.line)},m.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},m.prototype.reflow=function(){var t=0,e=A.updateProperty,i=this.frame,n=this.range;if(!n)throw new Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var s=this.props,r=this.getOption("showMinorLabels"),o=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(s.minorCharHeight=a.clientHeight,s.minorCharWidth=a.clientWidth),h&&(s.majorCharHeight=h.clientHeight,s.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=s.parentHeight&&(s.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":s.minorLabelHeight=r?s.minorCharHeight:0,s.majorLabelHeight=o?s.majorCharHeight:0,s.minorLabelTop=0,s.majorLabelTop=s.minorLabelTop+s.minorLabelHeight,s.minorLineTop=-this.top,s.minorLineHeight=Math.max(this.top+s.majorLabelHeight,0),s.minorLineWidth=1,s.majorLineTop=-this.top,s.majorLineHeight=Math.max(this.top+s.minorLabelHeight+s.majorLabelHeight,0),s.majorLineWidth=1,s.lineTop=0;break;case"top":s.minorLabelHeight=r?s.minorCharHeight:0,s.majorLabelHeight=o?s.majorCharHeight:0,s.majorLabelTop=0,s.minorLabelTop=s.majorLabelTop+s.majorLabelHeight,s.minorLineTop=s.minorLabelTop,s.minorLineHeight=Math.max(d-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(d-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw new Error('Unkown orientation "'+this.getOption("orientation")+'"')}var c=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",c),this._updateConversion();var u=A.convert(n.start,"Number"),l=A.convert(n.end,"Number"),p=this.toTime(5*(s.minorCharWidth||10)).valueOf()-this.toTime(0).valueOf();this.step=new TimeStep(new Date(u),new Date(l),p),t+=e(s.range,"start",u),t+=e(s.range,"end",l),t+=e(s.range,"minimumStep",p.valueOf())}return t>0},m.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},g.prototype=new l,g.prototype.setOptions=l.prototype.setOptions,g.prototype.getContainer=function(){return this.frame},g.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCurrentTime"))return t&&(i.removeChild(t),delete this.frame),void 0;t||(t=document.createElement("div"),t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t),this.frame=t),e.conversion||e._updateConversion();var n=new Date,s=e.toScreen(n);t.style.left=s+"px",t.title="Current time: "+n,void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer);var r=this,o=1/e.conversion.scale/2;return 30>o&&(o=30),this.currentTimeTimer=setTimeout(function(){r.repaint()},o),!1},v.prototype=new l,v.prototype.setOptions=l.prototype.setOptions,v.prototype.getContainer=function(){return this.frame},v.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCustomTime"))return t&&(i.removeChild(t),delete this.frame),void 0;if(!t){t=document.createElement("div"),t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t);var n=document.createElement("div");n.style.position="relative",n.style.top="0px",n.style.left="-10px",n.style.height="100%",n.style.width="20px",t.appendChild(n),this.frame=t,this.subscribe(this,"movetime")}e.conversion||e._updateConversion();var s=e.toScreen(this.customTime);return t.style.left=s+"px",t.title="Time: "+this.customTime,!1},v.prototype._setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},v.prototype._getCustomTime=function(){return new Date(this.customTime.valueOf())},v.prototype.subscribe=function(t,e){var i=this,n={component:t,event:e,callback:function(t){i._onMouseDown(t,n)},params:{}};t.on("mousedown",n.callback),i.listeners.push(n)},v.prototype.on=function(t,e){var i=this.frame;if(!i)throw new Error("Cannot add event listener: no parent attached");Y.addListener(this,t,e),A.addEventListener(i,t,e)},v.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=A.getPageX(t),i.moved=!1,i.customTime=this.customTime;var s=this;i.onMouseMove||(i.onMouseMove=function(t){s._onMouseMove(t,e)},A.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){s._onMouseUp(t,e)},A.addEventListener(document,"mouseup",i.onMouseUp)),A.stopPropagation(t),A.preventDefault(t)}},v.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=this.parent,s=A.getPageX(t);void 0===i.mouseX&&(i.mouseX=s);var r=s-i.mouseX;Math.abs(r)>=1&&(i.moved=!0);var o=n.toScreen(i.customTime),a=o+r,h=n.toTime(a);this._setCustomTime(h),Y.trigger(this,"timechange",{customTime:this.customTime}),A.preventDefault(t)},v.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;i.onMouseMove&&(A.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(A.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&Y.trigger(this,"timechanged",{customTime:this.customTime})},y.prototype=new p,y.types={box:_,range:E,rangeoverflow:T,point:b},y.prototype.setOptions=l.prototype.setOptions,y.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},y.prototype.repaint=function(){var t=0,e=A.updateProperty,i=A.option.asSize,n=this.options,s=this.getOption("orientation"),r=this.defaultOptions,o=this.frame;if(!o){o=document.createElement("div"),o.className="itemset";var a=n.className;a&&A.addClassName(o,A.option.asString(a));var h=document.createElement("div");h.className="background",o.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",o.appendChild(d),this.dom.foreground=d;var c=document.createElement("div");c.className="itemset-axis",this.dom.axis=c,this.frame=o,t+=1}if(!this.parent)throw new Error("Cannot repaint itemset: no parent attached");var u=this.parent.getContainer();if(!u)throw new Error("Cannot repaint itemset: parent has no container element");o.parentNode||(u.appendChild(o),t+=1),this.dom.axis.parentNode||(u.appendChild(this.dom.axis),t+=1),t+=e(o.style,"left",i(n.left,"0px")),t+=e(o.style,"top",i(n.top,"0px")),t+=e(o.style,"width",i(n.width,"100%")),t+=e(o.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==s?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var l=this,p=this.queue,f=this.itemsData,m=this.items,g={};return Object.keys(p).forEach(function(e){var i=p[e],s=m[e];switch(i){case"add":case"update":var o=f&&f.get(e,g);if(o){var a=o.type||o.start&&o.end&&"range"||n.type||"box",h=y.types[a];if(s&&(h&&s instanceof h?(s.data=o,t++):(t+=s.hide(),s=null)),!s){if(!h)throw new TypeError('Unknown item type "'+a+'"');s=new h(l,o,n,r),t++}s.repaint(),m[e]=s}delete p[e];break;case"remove":s&&(t+=s.hide()),delete m[e],delete p[e];break;default:console.log('Error: unknown action "'+i+'"')}}),A.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},y.prototype.getForeground=function(){return this.dom.foreground},y.prototype.getBackground=function(){return this.dom.background},y.prototype.getAxis=function(){return this.dom.axis},y.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,s=A.updateProperty,r=A.option.asNumber,o=A.option.asSize,a=this.frame;if(a){this._updateConversion(),A.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=r(e.maxHeight),c=null!=o(e.height);if(c)h=a.offsetHeight;else{var u=this.stack.ordered;if(u.length){var l=u[0].top,p=u[0].top+u[0].height;A.forEach(u,function(t){l=Math.min(l,t.top),p=Math.max(p,t.top+t.height)}),h=p-l+i+n}else h=i+n}null!=d&&(h=Math.min(h,d)),t+=s(this,"height",h),t+=s(this,"top",a.offsetTop),t+=s(this,"left",a.offsetLeft),t+=s(this,"width",a.offsetWidth)}else t+=1;return t>0},y.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},y.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(t){if(!(t instanceof r||t instanceof o))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(n&&(A.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;A.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},y.prototype.getItems=function(){return this.itemsData},y.prototype._onUpdate=function(t){this._toQueue("update",t)},y.prototype._onAdd=function(t){this._toQueue("add",t)},y.prototype._onRemove=function(t){this._toQueue("remove",t)},y.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},y.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},y.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},y.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},w.prototype.select=function(){this.selected=!0},w.prototype.unselect=function(){this.selected=!1},w.prototype.show=function(){return!1},w.prototype.hide=function(){return!1},w.prototype.repaint=function(){return!1},w.prototype.reflow=function(){return!1},w.prototype.getWidth=function(){return this.width},_.prototype=new w(null,null),_.prototype.select=function(){this.selected=!0},_.prototype.unselect=function(){this.selected=!1},_.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");if(!e.box.parentNode){var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");i.appendChild(e.box),t=!0}if(!e.line.parentNode){var n=this.parent.getBackground();if(!n)throw new Error("Cannot repaint time axis: parent has no background container element");n.appendChild(e.line),t=!0}if(!e.dot.parentNode){var s=this.parent.getAxis();if(!n)throw new Error("Cannot repaint time axis: parent has no axis container element");s.appendChild(e.dot),t=!0}if(this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var r=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=r&&(this.className=r,e.box.className="item box"+r,e.line.className="item line"+r,e.dot.className="item dot"+r,t=!0)}return t},_.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint() -},_.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},_.prototype.reflow=function(){var t,e,i,n,s,r,o,a,h,d,c,u,l=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(c=this.data,u=this.parent&&this.parent.range,c&&u){var p=u.end-u.start;this.visible=c.start>u.start-p&&c.start0},_.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot")},_.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,s=t.line,r=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",s.style.left=e.line.left+"px","top"==i?(s.style.top="0px",s.style.height=this.top+"px"):(s.style.top=this.top+this.height+"px",s.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),r.style.left=e.dot.left+"px",r.style.top=e.dot.top+"px"}},b.prototype=new w(null,null),b.prototype.select=function(){this.selected=!0},b.prototype.unselect=function(){this.selected=!1},b.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},b.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},b.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},b.prototype.reflow=function(){var t,e,i,n,s,r,o,a,h,d,c=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var u=d.end-d.start;this.visible=h.start>d.start-u&&h.start0},b.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot))},b.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},E.prototype=new w(null,null),E.prototype.select=function(){this.selected=!0},E.prototype.unselect=function(){this.selected=!1},E.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},E.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},E.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},E.prototype.reflow=function(){var t,e,i,n,s,r,o,a,h,d,c,u,l,p,f,m,g=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw new Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,r=this.parent,o=r.toScreen(this.data.start),a=r.toScreen(this.data.end),c=A.updateProperty,u=t.box,l=r.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,g+=c(e.content,"width",t.content.offsetWidth),g+=c(this,"height",u.offsetHeight),-l>o&&(o=-l),a>2*l&&(a=2*l),p=0>o?Math.min(-o,a-o-e.content.width-2*s):0,g+=c(e.content,"left",p),"top"==f?(m=n,g+=c(this,"top",m)):(m=r.height-this.height-n,g+=c(this,"top",m)),g+=c(this,"left",o),g+=c(this,"width",Math.max(a-o,1))):g+=1),g>0},E.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content))},E.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},T.prototype=new E(null,null),T.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item rangeoverflow"+n,t=!0)}return t},T.prototype.getWidth=function(){return void 0!==this.props.content&&this.width0},x.prototype=new p,x.prototype.setOptions=l.prototype.setOptions,x.prototype.setRange=function(){},x.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},x.prototype.getItems=function(){return this.itemsData},x.prototype.setRange=function(t){this.range=t},x.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(A.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof r?this.groupsData=t:(this.groupsData=new r({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;A.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},x.prototype.getGroups=function(){return this.groupsData},x.prototype.repaint=function(){var t,e,i,n,s=0,r=A.updateProperty,o=A.option.asSize,a=A.option.asElement,h=this.options,d=this.dom.frame,c=this.dom.labels,u=this.dom.labelSet;if(!this.parent)throw new Error("Cannot repaint groupset: no parent attached");var l=this.parent.getContainer();if(!l)throw new Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var p=h.className;p&&A.addClassName(d,A.option.asString(p)),s+=1}d.parentNode||(l.appendChild(d),s+=1);var f=a(h.labelContainer);if(!f)throw new Error('Cannot repaint groupset: option "labelContainer" not defined');c||(c=document.createElement("div"),c.className="labels",this.dom.labels=c),u||(u=document.createElement("div"),u.className="label-set",c.appendChild(u),this.dom.labelSet=u),c.parentNode&&c.parentNode==f||(c.parentNode&&c.parentNode.removeChild(c.parentNode),f.appendChild(c)),s+=r(d.style,"height",o(h.height,this.height+"px")),s+=r(d.style,"top",o(h.top,"0px")),s+=r(d.style,"left",o(h.left,"0px")),s+=r(d.style,"width",o(h.width,"100%")),s+=r(u.style,"top",o(h.top,"0px")),s+=r(u.style,"height",o(h.height,this.height+"px"));var m=this,g=this.queue,v=this.groups,y=this.groupsData,w=Object.keys(g);if(w.length){w.forEach(function(t){var e=g[t],i=v[t];switch(e){case"add":case"update":if(!i){var n=Object.create(m.options);A.extend(n,{height:null,maxHeight:null}),i=new S(m,t,n),i.setItems(m.itemsData),v[t]=i,m.controller.add(i)}i.data=y.get(t),delete g[t];break;case"remove":i&&(i.setItems(),delete v[t],m.controller.remove(i)),delete g[t];break;default:console.log('Error: unknown action "'+e+'"')}});var _=this.groupsData.getIds({order:this.options.groupOrder});for(t=0;t<_.length;t++)!function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})}(v[_[t]],v[_[t-1]]);for(;u.firstChild;)u.removeChild(u.firstChild);for(t=0;t<_.length;t++)e=_[t],n=this._createLabel(e),u.appendChild(n);s++}for(e in v)v.hasOwnProperty(e)&&(i=v[e],n=i.label,n&&(n.style.top=i.top+"px",n.style.height=i.height+"px"));return s>0},x.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var n=document.createElement("div");n.className="inner",i.appendChild(n);var s=e.data&&e.data.content;s instanceof Element?n.appendChild(s):void 0!=s&&(n.innerHTML=s);var r=e.data&&e.data.className;return r&&A.addClassName(i,r),e.label=i,i},x.prototype.getContainer=function(){return this.dom.frame},x.prototype.getLabelsWidth=function(){return this.props.labels.width},x.prototype.reflow=function(){var t,e,i=0,n=this.options,s=A.updateProperty,r=A.option.asNumber,o=A.option.asSize,a=this.dom.frame;if(a){var h,d=r(n.maxHeight),c=null!=o(n.height);if(c)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var u=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var l=e.props&&e.props.label&&e.props.label.width||0;u=Math.max(u,l)}return i+=s(this.props.labels,"width",u),i>0},x.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},x.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},x.prototype._onUpdate=function(t){this._toQueue(t,"update")},x.prototype._onAdd=function(t){this._toQueue(t,"add")},x.prototype._onRemove=function(t){this._toQueue(t,"remove")},x.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},D.prototype.setOptions=function(t){A.extend(this.options,t),this.range.setRange(),this.controller.reflow(),this.controller.repaint()},D.prototype.setCustomTime=function(t){this.customtime._setCustomTime(t)},D.prototype.getCustomTime=function(){return new Date(this.customtime.customTime.valueOf())},D.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof r&&(e=t):e=null,t instanceof r||(e=new r({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),s=n.min,o=n.max;if(null!=s&&null!=o){var a=o.valueOf()-s.valueOf();0>=a&&(a=864e5),s=new Date(s.valueOf()-.05*a),o=new Date(o.valueOf()+.05*a)}void 0!=this.options.start&&(s=A.convert(this.options.start,"Date")),void 0!=this.options.end&&(o=A.convert(this.options.end,"Date")),(null!=s||null!=o)&&this.range.setRange(s,o)}},D.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?x:y;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);A.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!A.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},D.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var s=t.max("start");s&&(i=s.start.valueOf());var r=t.max("end");r&&(i=null==i?r.end.valueOf():Math.max(i,r.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},function(t){function e(t){return D=t,l()}function i(){M=0,C=D.charAt(0)}function n(){M++,C=D.charAt(M)}function s(){return D.charAt(M+1)}function r(t){return L.test(t)}function o(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var n=e.split("."),s=t;n.length;){var r=n.shift();n.length?(s[r]||(s[r]={}),s=s[r]):s[r]=i}}function h(t,e){for(var i,n,s=null,r=[t],a=t;a.parent;)r.push(a.parent),a=a.parent;if(a.nodes)for(i=0,n=a.nodes.length;n>i;i++)if(e.id===a.nodes[i].id){s=a.nodes[i];break}for(s||(s={id:e.id},t.node&&(s.attr=o(s.attr,t.node))),i=r.length-1;i>=0;i--){var h=r[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(s)&&h.nodes.push(s)}e.attr&&(s.attr=o(s.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=o({},t.edge);e.attr=o(i,e.attr)}}function c(t,e,i,n,s){var r={from:e,to:i,type:n};return t.edge&&(r.attr=o({},t.edge)),r.attr=o(r.attr||{},s),r}function u(){for(N=S.NULL,O="";" "==C||" "==C||"\n"==C||"\r"==C;)n();do{var t=!1;if("#"==C){for(var e=M-1;" "==D.charAt(e)||" "==D.charAt(e);)e--;if("\n"==D.charAt(e)||""==D.charAt(e)){for(;""!=C&&"\n"!=C;)n();t=!0}}if("/"==C&&"/"==s()){for(;""!=C&&"\n"!=C;)n();t=!0}if("/"==C&&"*"==s()){for(;""!=C;){if("*"==C&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)n()}while(t);if(""==C)return N=S.DELIMITER,void 0;var i=C+s();if(x[i])return N=S.DELIMITER,O=i,n(),n(),void 0;if(x[C])return N=S.DELIMITER,O=C,n(),void 0;if(r(C)||"-"==C){for(O+=C,n();r(C);)O+=C,n();return"false"==O?O=!1:"true"==O?O=!0:isNaN(Number(O))||(O=Number(O)),N=S.IDENTIFIER,void 0}if('"'==C){for(n();""!=C&&('"'!=C||'"'==C&&'"'==s());)O+=C,'"'==C&&n(),n();if('"'!=C)throw _('End of string " expected');return n(),N=S.IDENTIFIER,void 0}for(N=S.UNKNOWN;""!=C;)O+=C,n();throw new SyntaxError('Syntax error in part "'+b(O,30)+'"')}function l(){var t={};if(i(),u(),"strict"==O&&(t.strict=!0,u()),("graph"==O||"digraph"==O)&&(t.type=O,u()),N==S.IDENTIFIER&&(t.id=O,u()),"{"!=O)throw _("Angle bracket { expected");if(u(),p(t),"}"!=O)throw _("Angle bracket } expected");if(u(),""!==O)throw _("End of file expected");return u(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==O&&"}"!=O;)f(t),";"==O&&u()}function f(t){var e=m(t);if(e)return y(t,e),void 0;var i=g(t);if(!i){if(N!=S.IDENTIFIER)throw _("Identifier expected");var n=O;if(u(),"="==O){if(u(),N!=S.IDENTIFIER)throw _("Identifier expected");t[n]=O,u()}else v(t,n)}}function m(t){var e=null;if("subgraph"==O&&(e={},e.type="subgraph",u(),N==S.IDENTIFIER&&(e.id=O,u())),"{"==O){if(u(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=O)throw _("Angle bracket } expected");u(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function g(t){return"node"==O?(u(),t.node=w(),"node"):"edge"==O?(u(),t.edge=w(),"edge"):"graph"==O?(u(),t.graph=w(),"graph"):null}function v(t,e){var i={id:e},n=w();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==O||"--"==O;){var i,n=O;u();var s=m(t);if(s)i=s;else{if(N!=S.IDENTIFIER)throw _("Identifier or subgraph expected");i=O,h(t,{id:i}),u()}var r=w(),o=c(t,e,i,n,r);d(t,o),e=i}}function w(){for(var t=null;"["==O;){for(u(),t={};""!==O&&"]"!=O;){if(N!=S.IDENTIFIER)throw _("Attribute name expected");var e=O;if(u(),"="!=O)throw _("Equal sign = expected");if(u(),N!=S.IDENTIFIER)throw _("Attribute value expected");var i=O;a(t,e,i),u(),","==O&&u()}if("]"!=O)throw _("Bracket ] expected");u()}return t}function _(t){return new SyntaxError(t+', got "'+b(O,30)+'" (char '+M+")")}function b(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function E(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function T(t){function i(t){var e={from:t.from,to:t.to};return o(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var n=e(t),s={nodes:[],edges:[],options:{}};return n.nodes&&n.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};o(e,t.attr),e.image&&(e.shape="image"),s.nodes.push(e)}),n.edges&&n.edges.forEach(function(t){var e,n;e=t.from instanceof Object?t.from.nodes:{id:t.from},n=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);s.edges.push(e)}),E(e,n,function(e,n){var r=c(s,e.id,n.id,t.type,t.attr),o=i(r);s.edges.push(o)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);s.edges.push(e)})}),n.attr&&(s.options=n.attr),s}var S={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},x={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},D="",M=0,C="",O="",N=S.NULL,L=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=T}("undefined"!=typeof A?A:n),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var n=2*i,s=n/2,r=Math.sqrt(3)/6*n,o=Math.sqrt(n*n-s*s);this.moveTo(t,e-(o-r)),this.lineTo(t+s,e+r),this.lineTo(t-s,e+r),this.lineTo(t,e-(o-r)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var n=2*i,s=n/2,r=Math.sqrt(3)/6*n,o=Math.sqrt(n*n-s*s);this.moveTo(t,e+(o-r)),this.lineTo(t+s,e-r),this.lineTo(t-s,e-r),this.lineTo(t,e+(o-r)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var n=0;10>n;n++){var s=n%2===0?1.3*i:.5*i;this.lineTo(t+s*Math.sin(2*n*Math.PI/10),e-s*Math.cos(2*n*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,n,s){var r=Math.PI/180;0>i-2*s&&(s=i/2),0>n-2*s&&(s=n/2),this.beginPath(),this.moveTo(t+s,e),this.lineTo(t+i-s,e),this.arc(t+i-s,e+s,s,270*r,360*r,!1),this.lineTo(t+i,e+n-s),this.arc(t+i-s,e+n-s,s,0,90*r,!1),this.lineTo(t+s,e+n),this.arc(t+s,e+n-s,s,90*r,180*r,!1),this.lineTo(t,e+s),this.arc(t+s,e+s,s,180*r,270*r,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,n){var s=.5522848,r=i/2*s,o=n/2*s,a=t+i,h=e+n,d=t+i/2,c=e+n/2;this.beginPath(),this.moveTo(t,c),this.bezierCurveTo(t,c-o,d-r,e,d,e),this.bezierCurveTo(d+r,e,a,c-o,a,c),this.bezierCurveTo(a,c+o,d+r,h,d,h),this.bezierCurveTo(d-r,h,t,c+o,t,c)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,r=i,o=n*s,a=.5522848,h=r/2*a,d=o/2*a,c=t+r,u=e+o,l=t+r/2,p=e+o/2,f=e+(n-o/2),m=e+n;this.beginPath(),this.moveTo(c,p),this.bezierCurveTo(c,p+d,l+h,u,l,u),this.bezierCurveTo(l-h,u,t,p+d,t,p),this.bezierCurveTo(t,p-d,l-h,e,l,e),this.bezierCurveTo(l+h,e,c,p-d,c,p),this.lineTo(c,f),this.bezierCurveTo(c,f+d,l+h,m,l,m),this.bezierCurveTo(l-h,m,t,f+d,t,f),this.lineTo(t,p)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,n){var s=t-n*Math.cos(i),r=e-n*Math.sin(i),o=t-.9*n*Math.cos(i),a=e-.9*n*Math.sin(i),h=s+n/3*Math.cos(i+.5*Math.PI),d=r+n/3*Math.sin(i+.5*Math.PI),c=s+n/3*Math.cos(i-.5*Math.PI),u=r+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(o,a),this.lineTo(c,u),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==l&&(l=.001);var r=s.length;this.moveTo(t,e);for(var o=i-t,a=n-e,h=a/o,d=Math.sqrt(o*o+a*a),c=0,u=!0;d>=.1;){var l=s[c++%r];l>d&&(l=d);var p=Math.sqrt(l*l/(1+h*h));0>o&&(p=-p),t+=p,e+=h*p,this[u?"lineTo":"moveTo"](t,e),d-=l,u=!u}}),M.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),this._updateMass()},M.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},M.prototype._updateMass=function(){this.mass=50+20*this.edges.length},M.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.label&&(this.label=t.label),void 0!=t.title&&(this.title=t.title),void 0!=t.group&&(this.group=t.group),void 0!=t.x&&(this.x=t.x),void 0!=t.y&&(this.y=t.y),void 0!=t.value&&(this.value=t.value),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var n in i)i.hasOwnProperty(n)&&(this[n]=i[n])}if(void 0!=t.shape&&(this.shape=t.shape),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.color&&(this.color=M.parseColor(t.color)),void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},M.parseColor=function(t){var e;return A.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,A.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},M.prototype.select=function(){this.selected=!0,this._reset()},M.prototype.unselect=function(){this.selected=!1,this._reset()},M.prototype._reset=function(){this.width=void 0,this.height=void 0},M.prototype.getTitle=function(){return this.title},M.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var n=this.width/2,s=this.height/2,r=Math.sin(e)*n,o=Math.cos(e)*s;return n*s/Math.sqrt(r*r+o*o);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},M.prototype._setForce=function(t,e){this.fx=t,this.fy=e},M.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},M.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i/t,this.x+=this.vx/t}if(!this.yFixed){var n=-this.damping*this.vy,s=(this.fy+n)/this.mass;this.vy+=s/t,this.y+=this.vy/t}},M.prototype.isFixed=function(){return this.xFixed&&this.yFixed},M.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},M.prototype.isSelected=function(){return this.selected},M.prototype.getValue=function(){return this.value},M.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},M.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},M.prototype.draw=function(){throw"Draw method not initialized for node"},M.prototype.resize=function(){throw"Resize method not initialized for node"},M.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},M.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},M.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._label(t,this.label,this.x,e,void 0,"top")},M.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},M.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=i.width+2*e;this.width=n,this.height=n}},M.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=Math.max(i.width,i.height)+2*e;this.radius=n/2,this.width=n,this.height=n}},M.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.widthc;c++)t.fillText(o[c],i,d),d+=h}},M.prototype.getTextSize=function(t){if(void 0!=this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,n=0,s=0,r=e.length;r>s;s++)n=Math.max(n,t.measureText(e[s]).width);return{width:n,height:i}}return{width:0,height:0}},C.prototype.setProperties=function(t,e){if(t)switch(void 0!=t.from&&(this.fromId=t.from),void 0!=t.to&&(this.toId=t.to),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.label&&(this.label=t.label),this.label&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),t.dash&&(void 0!=t.dash.length&&(this.dash.length=t.dash.length),void 0!=t.dash.gap&&(this.dash.gap=t.dash.gap),void 0!=t.dash.altLength&&(this.dash.altLength=t.dash.altLength)),void 0!=t.color&&(this.color=t.color),this.connect(),this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-center":this.draw=this._drawArrowCenter;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}},C.prototype.connect=function(){this.disconnect(),this.from=this.graph.nodes[this.fromId]||null,this.to=this.graph.nodes[this.toId]||null,this.connected=this.from&&this.to,this.connected?(this.from.attachEdge(this),this.to.attachEdge(this)):(this.from&&this.from.detachEdge(this),this.to&&this.to.detachEdge(this))},C.prototype.disconnect=function(){this.from&&(this.from.detachEdge(this),this.from=null),this.to&&(this.to.detachEdge(this),this.to=null),this.connected=!1},C.prototype.getTitle=function(){return this.title},C.prototype.getValue=function(){return this.value},C.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},C.prototype.draw=function(){throw"Method draw not initialized in edge"},C.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,n=this.from.y,s=this.to.x,r=this.to.y,o=t.left,a=t.top,h=C._dist(i,n,s,r,o,a);return e>h},C.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,n,s=this.length/4,r=this.from;r.width||r.resize(t),r.width>r.height?(i=r.x+r.width/2,n=r.y-s):(i=r.x+s,n=r.y-r.height/2),this._circle(t,i,n,s),e=this._pointOnCircle(i,n,s,.5),this._label(t,this.label,e.x,e.y)}},C.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},C.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},C.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},C.prototype._label=function(t,e,i,n){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var s=t.measureText(e).width,r=this.fontSize,o=i-s/2,a=n-r/2;t.fillRect(o,a,s,r),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,o,a)}},C.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!=this.dash.length&&void 0!=this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},C.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},C.prototype._pointOnCircle=function(t,e,i,n){var s=2*(n-3/8)*Math.PI;return{x:t+i*Math.cos(s),y:e-i*Math.sin(s)}},C.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),n=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var s,r,o=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(s=a.x+a.width/2,r=a.y-o):(s=a.x+o,r=a.y-a.height/2),this._circle(t,s,r,o);var i=.2*Math.PI,n=10+5*this.width;e=this._pointOnCircle(s,r,o,.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(s,r,o,.5),this._label(t,this.label,e.x,e.y))}},C.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var n=this.to.x-this.from.x,s=this.to.y-this.from.y,r=Math.sqrt(n*n+s*s),o=this.from.distanceToBorder(t,e+Math.PI),a=(r-o)/r,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,c=this.to.distanceToBorder(t,e),u=(r-c)/r,l=(1-u)*this.from.x+u*this.to.x,p=(1-u)*this.from.y+u*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(l,p),t.stroke(),i=10+5*this.width,t.arrow(l,p,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var m,g,v,y=this.from,w=this.length/4;y.width||y.resize(t),y.width>y.height?(m=y.x+y.width/2,g=y.y-w,v={x:m,y:y.y,angle:.9*Math.PI}):(m=y.x+w,g=y.y-y.height/2,v={x:y.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,w,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(m,g,w,.5),this._label(t,this.label,f.x,f.y))}},C._dist=function(t,e,i,n,s,r){var o=i-t,a=n-e,h=o*o+a*a,d=((s-t)*o+(r-e)*a)/h;d>1?d=1:0>d&&(d=0);var c=t+d*o,u=e+d*a,l=c-s,p=u-r;return Math.sqrt(l*l+p*p)},O.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},O.prototype.setText=function(t){this.frame.innerHTML=t},O.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,s=this.frame.parentNode.clientWidth,r=this.y-e;r+e+this.padding>n&&(r=n-e-this.padding),rs&&(o=s-i-this.padding),o0?s[s.length-1]:null},N.prototype._getPointer=function(t){return{x:t.pageX-R.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-R.util.getAbsoluteTop(this.frame.canvas)}},N.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale()},N.prototype._onDragStart=function(){var t=this.drag;t.selection=[],t.translation=this._getTranslation(),t.nodeId=this._getNodeAt(t.pointer);var e=this.nodes[t.nodeId];if(e){e.isSelected()||this._selectNodes([t.nodeId]);var i=this;this.selection.forEach(function(e){var n=i.nodes[e];if(n){var s={id:e,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}})}},N.prototype._onDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.touches[0]),i=this,n=this.drag,s=n.selection;if(s&&s.length){var r=e.x-n.pointer.x,o=e.y-n.pointer.y;s.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+r)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+o))}),this.moving||(this.moving=!0,this.start())}else{var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},N.prototype._onDragEnd=function(){var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},N.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];n?(this._selectNodes([i]),this.moving||this._redraw()):(this._unselectNodes(),this._redraw())},N.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];if(n){if(n.isSelected())this._unselectNodes([i]);else{var s=!0;this._selectNodes([i],s)}this.moving||this._redraw()}},N.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},N.prototype._zoom=function(t,e){var i=this._getScale();.01>t&&(t=.01),t>10&&(t=10);var n=this._getTranslation(),s=t/i,r=(1-s)*e.x+n.x*s,o=(1-s)*e.y+n.y*s;return this._setScale(t),this._setTranslation(r,o),this._redraw(),t},N.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mouswheelScale"in this.pinch||(this.pinch.mouswheelScale=1);var i=this.pinch.mouswheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=A.fakeGesture(this,t),r=this._getPointer(s.center);i=this._zoom(i,r),this.pinch.mouswheelScale=i}t.preventDefault()},N.prototype._onMouseMoveTitle=function(t){var e=A.fakeGesture(this,t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var n=this,s=function(){n._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(s,300))},N.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},n=this.popupNode;if(void 0==this.popupNode){var s=this.nodes;for(e in s)if(s.hasOwnProperty(e)){var r=s[e];if(void 0!=r.getTitle()&&r.isOverlappingWith(i)){this.popupNode=r;break}}}if(void 0==this.popupNode){var o=this.edges;for(e in o)if(o.hasOwnProperty(e)){var a=o[e];if(a.connected&&void 0!=a.getTitle()&&a.isOverlappingWith(i)){this.popupNode=a;break}}}if(this.popupNode){if(this.popupNode!=n){var h=this;h.popup||(h.popup=new O(h.frame)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},N.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},N.prototype._unselectNodes=function(t,e){var i,n,s,r=!1;if(t)for(i=0,n=t.length;n>i;i++){s=t[i],this.nodes[s].unselect();for(var o=0;oi;i++)s=this.selection[i],this.nodes[s].unselect(),r=!0;this.selection=[]}return!r||1!=e&&void 0!=e||this._trigger("select"),r},N.prototype._selectNodes=function(t,e){var i,n,s=!1,r=!0;if(t.length!=this.selection.length)r=!1;else for(i=0,n=Math.min(t.length,this.selection.length);n>i;i++)if(t[i]!=this.selection[i]){r=!1;break}if(r)return s;if(void 0==e||0==e){var o=!1;s=this._unselectNodes(void 0,o)}for(i=0,n=t.length;n>i;i++){var a=t[i],h=-1!=this.selection.indexOf(a);h||(this.nodes[a].select(),this.selection.push(a),s=!0)}return s&&this._trigger("select"),s},N.prototype._getNodesOverlappingWith=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&e[n].isOverlappingWith(t)&&i.push(n);return i},N.prototype.getSelection=function(){return this.selection.concat([])},N.prototype.setSelection=function(t){var e,i,n;if(!t||void 0==t.length)throw"Selection must be an array with ids";for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],this.nodes[n].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');s.select(),this.selection.push(n)}this.redraw()},N.prototype._updateSelection=function(){for(var t=0;ti;i++)for(var s=t[i],r=s.edges,o=0,a=r.length;a>o;o++){var h=r[o],d=null;h.from==s?d=h.to:h.to==s&&(d=h.from);var c,u;if(d)for(c=0,u=t.length;u>c;c++)if(t[c]==d){d=null;break}if(d)for(c=0,u=e.length;u>c;c++)if(e[c]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],n=this.nodes;for(var s in n)if(n.hasOwnProperty(s)){for(var r=[n[s]],o=0;t>o;o++)r=r.concat(e(r));i.push(r)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},N.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight},N.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof r||t instanceof o)this.nodesData=t;else if(t instanceof Array)this.nodesData=new r,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new r}if(e&&A.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;A.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},N.prototype._addNodes=function(t){for(var e,i=0,n=t.length;n>i;i++){e=t[i];var s=this.nodesData.get(e),r=new M(s,this.images,this.groups,this.constants);if(this.nodes[e]=r,!r.isFixed()){var o=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);r.x=o*Math.cos(h),r.y=o*Math.sin(h),this.moving=!0}}this._reconnectEdges(),this._updateValueRange(this.nodes)},N.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,n=0,s=t.length;s>n;n++){var r=t[n],o=e[r],a=i.get(r);o?o.setProperties(a,this.constants):(o=new M(properties,this.images,this.groups,this.constants),e[r]=o,o.isFixed()||(this.moving=!0))}this._reconnectEdges(),this._updateValueRange(e)},N.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,n=t.length;n>i;i++){var s=t[i];delete e[s]}this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},N.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof r||t instanceof o)this.edgesData=t;else if(t instanceof Array)this.edgesData=new r,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new r}if(e&&A.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;A.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},N.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var r=t[n],o=e[r];o&&o.disconnect();var a=i.get(r);e[r]=new C(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},N.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var r=t[n],o=i.get(r),a=e[r];a?(a.disconnect(),a.setProperties(o,this.constants),a.connect()):(a=new C(o,this,this.constants),this.edges[r]=a)}this.moving=!0,this._updateValueRange(e)},N.prototype._removeEdges=function(t){for(var e=this.edges,i=0,n=t.length;n>i;i++){var s=t[i],r=e[s];r&&(r.disconnect(),delete e[s])}this.moving=!0,this._updateValueRange(e)},N.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var n=i[t];n.from=null,n.to=null,n.connect()}},N.prototype._updateValueRange=function(t){var e,i=void 0,n=void 0;for(e in t)if(t.hasOwnProperty(e)){var s=t[e].getValue();void 0!==s&&(i=void 0===i?s:Math.min(s,i),n=void 0===n?s:Math.max(s,n))}if(void 0!==i&&void 0!==n)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,n)},N.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},N.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),t.restore()},N.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},N.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},N.prototype._setScale=function(t){this.scale=t},N.prototype._getScale=function(){return this.scale},N.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},N.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},N.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},N.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},N.prototype._drawNodes=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&(e[n].isSelected()?i.push(n):e[n].draw(t));for(var s=0,r=i.length;r>s;s++)e[i[s]].draw(t)},N.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.connected&&e[i].draw(t)}},N.prototype._doStabilize=function(){for(var t=(new Date,0),e=this.constants.minVelocity,i=!1;!i&&t0&&e==s.EVENT_END?e=s.EVENT_MOVE:c||(e=s.EVENT_END),c||null===r?r=h:h=r,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(c=s.PointerEvent.updatePointer(e,h))),c||(r=null,o=!1,a=!1,s.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=s.HAS_POINTEREVENTS?s.PointerEvent.getEvents():s.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],s.EVENT_TYPES[s.EVENT_START]=t[0],s.EVENT_TYPES[s.EVENT_MOVE]=t[1],s.EVENT_TYPES[s.EVENT_END]=t[2]},getTouchList:function(t){return s.HAS_POINTEREVENTS?s.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var n=this.getTouchList(i,e),r=s.POINTER_TOUCH;return(i.type.match(/mouse/)||s.PointerEvent.matchType(s.POINTER_MOUSE,i))&&(r=s.POINTER_MOUSE),{center:s.utils.getCenter(n),timeStamp:(new Date).getTime(),target:i.target,touches:n,eventType:e,pointerType:r,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return s.detection.stopDetect()}}}},s.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==s.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[s.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==s.POINTER_MOUSE,i[s.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==s.POINTER_TOUCH,i[s.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==s.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},s.utils={extend:function(t,e,n){for(var s in e)t[s]!==i&&n||(t[s]=e[s]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],n=0,s=t.length;s>n;n++)e.push(t[n].pageX),i.push(t[n].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,n=e.pageX-t.pageX;return 180*Math.atan2(i,n)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),n=Math.abs(t.pageY-e.pageY);return i>=n?t.pageX-e.pageX>0?s.DIRECTION_LEFT:s.DIRECTION_RIGHT:t.pageY-e.pageY>0?s.DIRECTION_UP:s.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,n=e.pageY-t.pageY;return Math.sqrt(i*i+n*n)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==s.DIRECTION_UP||t==s.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,n=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var s=0;si;i++){var r=this.gestures[i];if(!this.stopped&&e[r.name]!==!1&&r.handler.call(r,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==s.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=s.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent; -if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,n=t.touches.length;n>i;i++)e.touches.push(s.utils.extend({},t.touches[i]))}var r=t.timeStamp-e.timeStamp,o=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=s.utils.getVelocity(r,o,a);return s.utils.extend(t,{deltaTime:r,deltaX:o,deltaY:a,velocityX:h.x,velocityY:h.y,distance:s.utils.getDistance(e.center,t.center),angle:s.utils.getAngle(e.center,t.center),direction:s.utils.getDirection(e.center,t.center),scale:s.utils.getScale(e.touches,t.touches),rotation:s.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),s.utils.extend(s.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},s.gestures=s.gestures||{},s.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case s.EVENT_START:clearTimeout(this.timer),s.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==s.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case s.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case s.EVENT_END:clearTimeout(this.timer)}}},s.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==s.EVENT_END){var i=s.detection.previous,n=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},s.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,void 0;if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==s.POINTER_MOUSE?(t.stopDetect(),void 0):(e.options.prevent_default&&t.preventDefault(),t.eventType==s.EVENT_START&&e.trigger(this.name,t),void 0)}},s.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==s.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=s:(t.Hammer=s,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return s}))}(this)},{}],3:[function(e,i){(function(n){function s(t,e){return function(i){return u(t.call(this,i),e)}}function r(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function o(){}function a(t){T(t),d(this,t)}function h(t){var e=v(t),i=e.year||0,n=e.month||0,s=e.week||0,r=e.day||0,o=e.hour||0,a=e.minute||0,h=e.second||0,d=e.millisecond||0;this._milliseconds=+d+1e3*h+6e4*a+36e5*o,this._days=+r+7*s,this._months=+n+12*i,this._data={},this._bubble()}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function c(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e,i){for(var n=Math.abs(t)+"",s=t>=0;n.lengthn;n++)(i&&t[n]!==e[n]||!i&&w(t[n])!==w(e[n]))&&o++;return o+r}function g(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=Ge[t]||Be[e]||e}return t}function v(t){var e,i,n={};for(i in t)t.hasOwnProperty(i)&&(e=g(i),e&&(n[e]=t[i]));return n}function y(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}re[t]=function(s,r){var o,a,h=re.fn._lang[t],d=[];if("number"==typeof s&&(r=s,s=n),a=function(t){var e=re().utc().set(i,t);return h.call(re.fn._lang,e,s||"")},null!=r)return a(r);for(o=0;e>o;o++)d.push(a(o));return d}}function w(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function _(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function b(t){return E(t)?366:365}function E(t){return t%4===0&&t%100!==0||t%400===0}function T(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[ue]<0||t._a[ue]>11?ue:t._a[le]<1||t._a[le]>_(t._a[ce],t._a[ue])?le:t._a[pe]<0||t._a[pe]>23?pe:t._a[fe]<0||t._a[fe]>59?fe:t._a[me]<0||t._a[me]>59?me:t._a[ge]<0||t._a[ge]>999?ge:-1,t._pf._overflowDayOfYear&&(ce>e||e>le)&&(e=le),t._pf.overflow=e)}function S(t){t._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function x(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function D(t){return t?t.toLowerCase().replace("_","-"):t}function M(t,e){return e._isUTC?re(t).zone(e._offset||0):re(t).local()}function C(t,e){return e.abbr=t,ve[t]||(ve[t]=new o),ve[t].set(e),ve[t]}function O(t){delete ve[t]}function N(t){var i,n,s,r,o=0,a=function(t){if(!ve[t]&&ye)try{e("./lang/"+t)}catch(i){}return ve[t]};if(!t)return re.fn._lang;if(!p(t)){if(n=a(t))return n;t=[t]}for(;o0;){if(n=a(r.slice(0,i).join("-")))return n;if(s&&s.length>=i&&m(r,s,!0)>=i-1)break;i--}o++}return re.fn._lang}function L(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function I(t){var e,i,n=t.match(Ee);for(e=0,i=n.length;i>e;e++)n[e]=Ke[n[e]]?Ke[n[e]]:L(n[e]);return function(s){var r="";for(e=0;i>e;e++)r+=n[e]instanceof Function?n[e].call(s,t):n[e];return r}}function k(t,e){return t.isValid()?(e=A(e,t.lang()),qe[e]||(qe[e]=I(e)),qe[e](t)):t.lang().invalidDate()}function A(t,e){function i(t){return e.longDateFormat(t)||t}var n=5;for(Te.lastIndex=0;n>=0&&Te.test(t);)t=t.replace(Te,i),Te.lastIndex=0,n-=1;return t}function P(t,e){var i,n=e._strict;switch(t){case"DDDD":return Pe;case"YYYY":case"GGGG":case"gggg":return n?Ye:De;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return n?Fe:Me;case"S":if(n)return ke;case"SS":if(n)return Ae;case"SSS":case"DDD":return n?Pe:xe;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Oe;case"a":case"A":return N(e._l)._meridiemParse;case"X":return Ie;case"Z":case"ZZ":return Ne;case"T":return Le;case"SSSS":return Ce;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return n?Ae:Se;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return n?ke:Se;default:return i=new RegExp(j(W(t.replace("\\","")),"i"))}}function Y(t){t=t||"";var e=t.match(Ne)||[],i=e[e.length-1]||[],n=(i+"").match(We)||["-",0,0],s=+(60*n[1])+w(n[2]);return"+"===n[0]?-s:s}function F(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[ue]=w(e)-1);break;case"MMM":case"MMMM":n=N(i._l).monthsParse(e),null!=n?s[ue]=n:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(s[le]=w(e));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=w(e));break;case"YY":s[ce]=w(e)+(w(e)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":s[ce]=w(e);break;case"a":case"A":i._isPm=N(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[pe]=w(e);break;case"m":case"mm":s[fe]=w(e);break;case"s":case"ss":s[me]=w(e);break;case"S":case"SS":case"SSS":case"SSSS":s[ge]=w(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=Y(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function R(t){var e,i,n,s,r,o,a,h,d,c,u=[];if(!t._d){for(n=z(t),t._w&&null==t._a[le]&&null==t._a[ue]&&(r=function(e){var i=parseInt(e,10);return e?e.length<3?i>68?1900+i:2e3+i:i:null==t._a[ce]?re().weekYear():t._a[ce]},o=t._w,null!=o.GG||null!=o.W||null!=o.E?a=J(r(o.GG),o.W||1,o.E,4,1):(h=N(t._l),d=null!=o.d?Z(o.d,h):null!=o.e?parseInt(o.e,10)+h._week.dow:0,c=parseInt(o.w,10)||1,null!=o.d&&db(s)&&(t._pf._overflowDayOfYear=!0),i=X(s,0,t._dayOfYear),t._a[ue]=i.getUTCMonth(),t._a[le]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=u[e]=n[e];for(;7>e;e++)t._a[e]=u[e]=null==t._a[e]?2===e?1:0:t._a[e];u[pe]+=w((t._tzm||0)/60),u[fe]+=w((t._tzm||0)%60),t._d=(t._useUTC?X:q).apply(null,u)}}function H(t){var e;t._d||(e=v(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],R(t))}function z(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function U(t){t._a=[],t._pf.empty=!0;var e,i,n,s,r,o=N(t._l),a=""+t._i,h=a.length,d=0;for(n=A(t._f,o).match(Ee)||[],e=0;e0&&t._pf.unusedInput.push(r),a=a.slice(a.indexOf(i)+i.length),d+=i.length),Ke[s]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(s),F(s,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(s);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[pe]<12&&(t._a[pe]+=12),t._isPm===!1&&12===t._a[pe]&&(t._a[pe]=0),R(t),T(t)}function W(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,s){return e||i||n||s})}function j(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function V(t){var e,i,n,s,r;if(0===t._f.length)return t._pf.invalidFormat=!0,t._d=new Date(0/0),void 0;for(s=0;sr)&&(n=r,i=e));d(t,i||e)}function G(t){var e,i=t._i,n=Re.exec(i);if(n){for(t._pf.iso=!0,e=4;e>0;e--)if(n[e]){t._f=ze[e-1]+(n[6]||" ");break}for(e=0;4>e;e++)if(Ue[e][1].exec(i)){t._f+=Ue[e][0];break}i.match(Ne)&&(t._f+="Z"),U(t)}else t._d=new Date(i)}function B(t){var e=t._i,i=we.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?G(t):p(e)?(t._a=e.slice(0),R(t)):f(e)?t._d=new Date(+e):"object"==typeof e?H(t):t._d=new Date(e)}function q(t,e,i,n,s,r,o){var a=new Date(t,e,i,n,s,r,o);return 1970>t&&a.setFullYear(t),a}function X(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function Z(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function K(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function Q(t,e,i){var n=de(Math.abs(t)/1e3),s=de(n/60),r=de(s/60),o=de(r/24),a=de(o/365),h=45>n&&["s",n]||1===s&&["m"]||45>s&&["mm",s]||1===r&&["h"]||22>r&&["hh",r]||1===o&&["d"]||25>=o&&["dd",o]||45>=o&&["M"]||345>o&&["MM",de(o/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,K.apply({},h)}function $(t,e,i){var n,s=i-e,r=i-t.day();return r>s&&(r-=7),s-7>r&&(r+=7),n=re(t).add("d",r),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function J(t,e,i,n,s){var r,o,a=new Date(u(t,6,!0)+"-01-01").getUTCDay();return i=null!=i?i:s,r=s-a+(a>n?7:0),o=7*(e-1)+(i-s)+r+1,{year:o>0?t:t-1,dayOfYear:o>0?o:b(t-1)+o}}function te(t){var e=t._i,i=t._f;return"undefined"==typeof t._pf&&S(t),null===e?re.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=N().preparse(e)),re.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?p(i)?V(t):U(t):B(t),new a(t))}function ee(t,e){re.fn[t]=re.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),re.updateOffset(this),this):this._d["get"+i+e]()}}function ie(t){re.duration.fn[t]=function(){return this._data[t]}}function ne(t,e){re.duration.fn["as"+t]=function(){return+this/e}}function se(t){var e=!1,i=re;"undefined"==typeof ender&&(t?(he.moment=function(){return!e&&console&&console.warn&&(e=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),i.apply(null,arguments)},d(he.moment,i)):he.moment=re)}for(var re,oe,ae="2.5.0",he=this,de=Math.round,ce=0,ue=1,le=2,pe=3,fe=4,me=5,ge=6,ve={},ye="undefined"!=typeof i&&i.exports&&"undefined"!=typeof e,we=/^\/?Date\((\-?\d+)/i,_e=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,be=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Ee=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,Te=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Se=/\d\d?/,xe=/\d{1,3}/,De=/\d{1,4}/,Me=/[+\-]?\d{1,6}/,Ce=/\d+/,Oe=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Ne=/Z|[\+\-]\d\d:?\d\d/gi,Le=/T/i,Ie=/[\+\-]?\d+(\.\d{1,3})?/,ke=/\d/,Ae=/\d\d/,Pe=/\d{3}/,Ye=/\d{4}/,Fe=/[+\-]?\d{6}/,Re=/^\s*\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)?)?$/,He="YYYY-MM-DDTHH:mm:ssZ",ze=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Ue=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],We=/([\+\-]|\d\d)/gi,je="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Ve={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Ge={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Be={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},qe={},Xe="DDD w W M D d".split(" "),Ze="M D H h m s w W".split(" "),Ke={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},YYYYYY:function(){var t=this.year(),e=t>=0?"+":"-";return e+u(Math.abs(t),6)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},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 w(this.milliseconds()/100)},SS:function(){return u(w(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},SSSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(w(t/60),2)+":"+u(w(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(w(t/60),2)+u(w(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},Qe=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Xe.length;)oe=Xe.pop(),Ke[oe+"o"]=r(Ke[oe],oe);for(;Ze.length;)oe=Ze.pop(),Ke[oe+oe]=s(Ke[oe],2);for(Ke.DDDD=s(Ke.DDD,3),d(o.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=re.utc([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,n;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=re([2e3,1]).day(e),n="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(n.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var s=this._relativeTime[i];return"function"==typeof s?s(t,e,i,n):s.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return $(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),re=function(t,e,i,s){return"boolean"==typeof i&&(s=i,i=n),te({_i:t,_f:e,_l:i,_strict:s,_isUTC:!1})},re.utc=function(t,e,i,s){var r;return"boolean"==typeof i&&(s=i,i=n),r=te({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e,_strict:s}).utc()},re.unix=function(t){return re(1e3*t)},re.duration=function(t,e){var i,n,s,r=t,o=null;return re.isDuration(t)?r={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(r={},e?r[e]=t:r.milliseconds=t):(o=_e.exec(t))?(i="-"===o[1]?-1:1,r={y:0,d:w(o[le])*i,h:w(o[pe])*i,m:w(o[fe])*i,s:w(o[me])*i,ms:w(o[ge])*i}):(o=be.exec(t))&&(i="-"===o[1]?-1:1,s=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},r={y:s(o[2]),M:s(o[3]),d:s(o[4]),h:s(o[5]),m:s(o[6]),s:s(o[7]),w:s(o[8])}),n=new h(r),re.isDuration(t)&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},re.version=ae,re.defaultFormat=He,re.updateOffset=function(){},re.lang=function(t,e){var i;return t?(e?C(D(t),e):null===e?(O(t),t="en"):ve[t]||N(t),i=re.duration.fn._lang=re.fn._lang=N(t),i._abbr):re.fn._lang._abbr},re.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),N(t)},re.isMoment=function(t){return t instanceof a},re.isDuration=function(t){return t instanceof h},oe=Qe.length-1;oe>=0;--oe)y(Qe[oe]);for(re.normalizeUnits=function(t){return g(t)},re.invalid=function(t){var e=re.utc(0/0);return null!=t?d(e._pf,t):e._pf.userInvalidated=!0,e},re.parseZone=function(t){return re(t).parseZone()},d(re.fn=a.prototype,{clone:function(){return re(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=re(this).utc();return 00:!1},parsingFlags:function(){return d({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=k(this,t||re.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),l(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),l(this,i,-1),this},diff:function(t,e,i){var n,s,r=M(t,this),o=6e4*(this.zone()-r.zone());return e=g(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+r.daysInMonth()),s=12*(this.year()-r.year())+(this.month()-r.month()),s+=(this-re(this).startOf("month")-(r-re(r).startOf("month")))/n,s-=6e4*(this.zone()-re(this).startOf("month").zone()-(r.zone()-re(r).startOf("month").zone()))/n,"year"===e&&(s/=12)):(n=this-r,s="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?(n-o)/864e5:"week"===e?(n-o)/6048e5:n),i?s:c(s)},from:function(t,e){return re.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(re(),t)},calendar:function(){var t=M(re(),this).startOf("day"),e=this.diff(t,"days",!0),i=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(this.lang().calendar(i,this))},isLeapYear:function(){return E(this.year())},isDST:function(){return this.zone()+re(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+re(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+M(t,this).startOf(e)},min:function(t){return t=re.apply(null,arguments),this>t?this:t},max:function(t){return t=re.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=Y(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&l(this,re.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?re(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return _(this.year(),this.month())},dayOfYear:function(t){var e=de((re(this).startOf("day")-re(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(t){var e=$(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=$(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=$(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=g(t),this[t]()},set:function(t,e){return t=g(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===n?this._lang:(this._lang=N(t),this)}}),oe=0;oei;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,s;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=new Array(r),s=0;r>s;){var a,h;s in o&&(a=o[s],h=t.call(i,a,s,o),n[s]=h),s++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],s=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(s,r,o,e)&&n.push(r)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(s){if("object"!=typeof s&&"function"!=typeof s||null===s)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in s)t.call(s,r)&&o.push(r);if(e)for(var a=0;n>a;a++)t.call(s,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s});var z={};z.isNumber=function(t){return t instanceof Number||"number"==typeof t},z.isString=function(t){return t instanceof String||"string"==typeof t},z.isDate=function(t){if(t instanceof Date)return!0;if(z.isString(t)){var e=P.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},z.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},z.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},z.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var s in n)n.hasOwnProperty(s)&&void 0!==n[s]&&(t[s]=n[s])}return t},z.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(z.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(L.isMoment(t))return new Date(t.valueOf());if(z.isString(t))return i=P.exec(t),i?new Date(Number(i[1])):L(t).toDate();throw new Error("Cannot convert object of type "+z.getType(t)+" to type Date");case"Moment":if(z.isNumber(t))return L(t);if(t instanceof Date)return L(t.valueOf());if(L.isMoment(t))return L(t);if(z.isString(t))return i=P.exec(t),L(i?Number(i[1]):t);throw new Error("Cannot convert object of type "+z.getType(t)+" to type Date");case"ISODate":if(z.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(L.isMoment(t))return t.toDate().toISOString();if(z.isString(t))return i=P.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+z.getType(t)+" to type ISODate");case"ASPDate":if(z.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(z.isString(t)){i=P.exec(t);var n;return n=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+n+")/"}throw new Error("Cannot convert object of type "+z.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+z.getType(t)+' to type "'+e+'"')}};var P=/^\/?Date\((\-?\d+)/i;z.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},z.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetLeft,n-=s.scrollLeft,s=s.offsetParent;return n},z.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetTop,n-=s.scrollTop,s=s.offsetParent;return n},z.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)},z.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0)},z.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},z.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},z.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},z.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},z.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},z.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},z.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},z.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},z.fakeGesture=function(t,e){var i=null;return N.event.collectEventData(this,i,e)},z.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},z.option={},z.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},z.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},z.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},z.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),z.isString(t)?t:z.isNumber(t)?t+"px":e||null},z.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null};var F={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var s=e[i];if(s&&s.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];s||(s={object:t,events:{}},this.listeners.push(s));var o=s.events[e];o||(o=[],s.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete s.events[e]);var r=0,a=s.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];if(o)for(var r=0,a=o.length;a>r;r++)o[r](i)}}};s.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:new RegExp(t.replace("*","\\w+")),s={id:z.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(s),s.id},s.prototype.off=function(t){for(var e=0;eo;o++)i=s._addItem(t[o]),n.push(i);else if(z.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var c={},l=0,u=a.length;u>l;l++){var p=a[l];c[p]=t.getValue(h,l)}i=s._addItem(c),n.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=s._addItem(t),n.push(i)}return n.length&&this._trigger("add",{items:n},e),n},o.prototype.update=function(t,e){var i=[],n=[],s=this,o=s.fieldId,r=function(t){var e=t[o];s.data[e]?(e=s._updateItem(t),n.push(e)):(e=s._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(z.isDataTable(t))for(var d=this._getColumnNames(t),c=0,l=t.getNumberOfRows();l>c;c++){for(var u={},p=0,f=d.length;f>p;p++){var g=d[p];u[g]=t.getValue(c,p)}r(u)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e),i.concat(n)},o.prototype.get=function(){var t,e,i,n,s=this,o=this.showInternalIds,r=z.getType(arguments[0]);"String"==r||"Number"==r?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==r?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var a;if(i&&i.type){if(a="DataTable"==i.type?"DataTable":"Array",n&&a!=z.getType(n))throw new Error('Type of parameter "data" ('+z.getType(n)+") does not correspond with specified options.type ("+i.type+")");if("DataTable"==a&&!z.isDataTable(n))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else a=n?"DataTable"==z.getType(n)?"DataTable":"Array":"Array";void 0!=i&&void 0!=i.showInternalIds&&(this.showInternalIds=i.showInternalIds);var h,d,c,l,u=i&&i.convert||this.options.convert,p=i&&i.filter,f=[];if(void 0!=t)h=s._getItem(t,u),p&&!p(h)&&(h=null);else if(void 0!=e)for(c=0,l=e.length;l>c;c++)h=s._getItem(e[c],u),(!p||p(h))&&f.push(h);else for(d in this.data)this.data.hasOwnProperty(d)&&(h=s._getItem(d,u),(!p||p(h))&&f.push(h));if(this.showInternalIds=o,i&&i.order&&void 0==t&&this._sort(f,i.order),i&&i.fields){var g=i.fields;if(void 0!=t)h=this._filterFields(h,g);else for(c=0,l=f.length;l>c;c++)f[c]=this._filterFields(f[c],g)}if("DataTable"==a){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,h);else for(c=0,l=f.length;l>c;c++)s._appendRow(n,m,f[c]);return n}if(void 0!=t)return h;if(n){for(c=0,l=f.length;l>c;c++)n.push(f[c]);return n}return f},o.prototype.getIds=function(t){var e,i,n,s,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,c=[];if(a)if(h){o=[];for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&o.push(s));for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&c.push(s[this.fieldId]));else if(h){o=[];for(n in r)r.hasOwnProperty(n)&&o.push(r[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=r[n],c.push(s[this.fieldId]));return c},o.prototype.forEach=function(t,e){var i,n,s=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in r)r.hasOwnProperty(n)&&(i=this._getItem(n,o),(!s||s(i))&&t(i,n))},o.prototype.map=function(t,e){var i,n=e&&e.filter,s=e&&e.convert||this.options.convert,o=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,s),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},o.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},o.prototype._sort=function(t,e){if(z.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],s=e[i];return n>s?1:s>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,n,s,o=[];if(t instanceof Array)for(i=0,n=t.length;n>i;i++)s=this._remove(t[i]),null!=s&&o.push(s);else s=this._remove(t),null!=s&&o.push(s);return o.length&&this._trigger("remove",{items:o},e),o},o.prototype._remove=function(t){if(z.isNumber(t)||z.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},o.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},o.prototype.max=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||r>n)&&(i=o,n=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||n>r)&&(i=o,n=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.convert[t],s=0;for(var o in e)if(e.hasOwnProperty(o)){for(var r=e[o],a=z.convert(r[t],n),h=!1,d=0;s>d;d++)if(i[d]==a){h=!0;break}h||(i[s]=a,s++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=z.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=z.convert(t[n],s)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,n,s=this.data[t];if(!s)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a&&!this.showInternalIds||(o[i]=z.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a&&!this.showInternalIds||(o[i]=n));return o},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=z.convert(t[n],s)}return e},o.prototype.isInternalId=function(t){return t in this.internalIds},o.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),s=0,o=e.length;o>s;s++){var r=e[s];t.setValue(n,s,i[r])}},r.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var s in this.ids)this.ids.hasOwnProperty(s)&&e.push(s);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)s=e[i],this.ids[s]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},r.prototype.get=function(){var t,e,i,n=this,s=z.getType(arguments[0]);"String"==s||"Number"==s||"Array"==s?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=z.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},r.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},r.prototype._onEvent=function(t,e,i){var n,s,o,r,a=e&&e.items,h=this.data,d=[],c=[],l=[];if(a&&h){switch(t){case"add":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r&&(this.ids[o]=!0,d.push(o));break;case"update":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r?this.ids[o]?c.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],l.push(o));break;case"remove":for(n=0,s=a.length;s>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],l.push(o))}d.length&&this._trigger("add",{items:d},i),c.length&&this._trigger("update",{items:c},i),l.length&&this._trigger("remove",{items:l},i)}},r.prototype.subscribe=o.prototype.subscribe,r.prototype.unsubscribe=o.prototype.unsubscribe,r.prototype._trigger=o.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step); +break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+1e3*this.step*60);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+1e3*this.step*60*60);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,s=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return L(t).format("SSS");case TimeStep.SCALE.SECOND:return L(t).format("s");case TimeStep.SCALE.MINUTE:return L(t).format("HH:mm");case TimeStep.SCALE.HOUR:return L(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return L(t).format("ddd D");case TimeStep.SCALE.DAY:return L(t).format("D");case TimeStep.SCALE.MONTH:return L(t).format("MMM");case TimeStep.SCALE.YEAR:return L(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return L(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return L(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return L(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return L(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return L(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){z.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw new Error("Cannot stack items: parent does not contain items");var e=[],i=0;z.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw new Error("Option order must be a function");e.sort(n),this.ordered=e},a.prototype._stack=function(){var t,e,i,n=this.ordered,s=this.options,o=s.orientation||this.defaultOptions.orientation,r="top"==o;for(i=s.margin&&void 0!==s.margin.item?s.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,n,s){for(var o=this.collision,r=t[e],a=n;a>=i;a--){var h=t[a];if(o(r,h,s)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){z.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},h.prototype.subscribe=function(t,e,i){function n(e){s._onMouseWheel(e,t,i)}var s=this;if("move"==e)t.on("dragstart",function(e){s._onDragStart(e,t)}),t.on("drag",function(e){s._onDrag(e,t,i)}),t.on("dragend",function(e){s._onDragEnd(e,t)});else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". Choose "move" or "zoom".');t.on("mousewheel",n),t.on("DOMMouseScroll",n),t.on("touch",function(){s._onTouch()}),t.on("pinch",function(e){s._onPinch(e,t,i)})}},h.prototype.on=function(t,e){var i=["rangechange","rangechanged"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},h.prototype.off=function(t,e){F.removeListener(this,t,e)},h.prototype._trigger=function(t){F.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,n=null!=t?z.convert(t,"Date").valueOf():this.start,s=null!=e?z.convert(e,"Date").valueOf():this.end,o=null!=this.options.max?z.convert(this.options.max,"Date").valueOf():null,r=null!=this.options.min?z.convert(this.options.min,"Date").valueOf():null;if(isNaN(n)||null===n)throw new Error('Invalid start "'+t+'"');if(isNaN(s)||null===s)throw new Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!==r&&r>n&&(i=r-n,n+=i,s+=i,null!=o&&s>o&&(s=o)),null!==o&&s>o&&(i=s-o,n-=i,s-=i,null!=r&&r>n&&(n=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>s-n&&(this.end-this.start===a?(n=this.start,s=this.end):(i=a-(s-n),n-=i/2,s+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),s-n>h&&(this.end-this.start===h?(n=this.start,s=this.end):(i=s-n-h,n+=i/2,s-=i/2))}var d=this.start!=n||this.end!=s;return this.start=n,this.end=s,d},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&e-t!=0?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var Y={};h.prototype._onDragStart=function(t,e){if(!Y.pinching){Y.start=this.start,Y.end=this.end;var i=e.frame;i&&(i.style.cursor="move")}},h.prototype._onDrag=function(t,e,i){if(d(i),!Y.pinching){var n="horizontal"==i?t.gesture.deltaX:t.gesture.deltaY,s=Y.end-Y.start,o="horizontal"==i?e.width:e.height,r=-n/o*s;this._applyRange(Y.start+r,Y.end+r),this._trigger("rangechange")}},h.prototype._onDragEnd=function(t,e){Y.pinching||(e.frame&&(e.frame.style.cursor="auto"),this._trigger("rangechanged"))},h.prototype._onMouseWheel=function(t,e,i){d(i);var n=0;if(t.wheelDelta?n=t.wheelDelta/120:t.detail&&(n=-t.detail/3),n){var s;s=0>n?1-n/5:1/(1+n/5);var o=z.fakeGesture(this,t),r=c(o.touches[0],e.frame),a=this._pointerToDate(e,i,r);this.zoom(s,a)}z.preventDefault(t)},h.prototype._onTouch=function(){Y.start=this.start,Y.end=this.end,Y.pinching=!1,Y.center=null},h.prototype._onPinch=function(t,e,i){if(Y.pinching=!0,t.gesture.touches.length>1){Y.center||(Y.center=c(t.gesture.center,e.frame));var n=1/t.gesture.scale,s=this._pointerToDate(e,i,Y.center),o=c(t.gesture.center,e.frame),r=(this._pointerToDate(e,i,o),parseInt(s+(Y.start-s)*n)),a=parseInt(s+(Y.end-s)*n);this.setRange(r,a)}},h.prototype._pointerToDate=function(t,e,i){var n;if("horizontal"==e){var s=t.width;return n=this.conversion(s),i.x/n.scale+n.offset}var o=t.height;return n=this.conversion(o),i.y/n.scale+n.offset},h.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,n=e+(this.end-e)*t;this.setRange(i,n)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,n=this.start-i,s=this.end-i;this.setRange(n,s)},l.prototype.add=function(t){if(void 0==t.id)throw new Error("Component has no field id");if(!(t instanceof u||t instanceof l))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},l.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},l.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},l.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},l.prototype.repaint=function V(){function V(i,n){n in e||(i.depends&&i.depends.forEach(function(t){V(t,t.id)}),i.parent&&V(i.parent,i.parent.id),t=i.repaint()||t,e[n]=!0)}var t=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var e={};z.forEach(this.components,V),t&&this.reflow()},l.prototype.reflow=function G(){function G(i,n){n in e||(i.depends&&i.depends.forEach(function(t){G(t,t.id)}),i.parent&&G(i.parent,i.parent.id),t=i.reflow()||t,e[n]=!0)}var t=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var e={};z.forEach(this.components,G),t&&this.repaint()},u.prototype.setOptions=function(t){t&&(z.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},u.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},u.prototype.getContainer=function(){return null},u.prototype.getFrame=function(){return this.frame},u.prototype.repaint=function(){return!1},u.prototype.reflow=function(){return!1},u.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},u.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},u.prototype.requestRepaint=function(){if(!this.controller)throw new Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},u.prototype.requestReflow=function(){if(!this.controller)throw new Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new u,p.prototype.setOptions=u.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="panel";var o=n.className;o&&("function"==typeof o?z.addClassName(s,String(o())):z.addClassName(s,String(o))),this.frame=s,t+=1}if(!s.parentNode){if(!this.parent)throw new Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw new Error("Cannot repaint panel: parent has no container element");r.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=z.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype=new p,f.prototype.setOptions=u.prototype.setOptions,f.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.frame;if(s||(s=document.createElement("div"),this.frame=s,t+=1),!s.parentNode){if(!this.container)throw new Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}s.className="vis timeline rootpanel "+n.orientation;var o=n.className;return o&&z.addClassName(s,z.option.asString(o)),t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},f.prototype.reflow=function(){var t=0,e=z.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},f.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?void(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow()):void t._unwatch()};z.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},f.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},f.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},f.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;z.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var s=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=s,t.hammer||(t.hammer=N(n,{prevent_default:!0})),t.hammer.on(i,s)}}})}},g.prototype=new u,g.prototype.setOptions=u.prototype.setOptions,g.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},g.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},g.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},g.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis",!a.parentNode){if(!this.parent)throw new Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw new Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var c=a.nextSibling;d.removeChild(a);var l="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,l)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var u=void 0,p=0;r.hasNext()&&1e3>p;){p++;var f=r.getCurrent(),g=this.toScreen(f),m=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(g,r.getLabelMinor()),m&&this.getOption("showMajorLabels")?(g>0&&(void 0==u&&(u=g),this._repaintMajorText(g,r.getLabelMajor())),this._repaintMajorLine(g)):this._repaintMinorLine(g),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),_=y.length*(o.majorCharWidth||10)+10;(void 0==u||u>_)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),c?d.insertBefore(a,c):d.appendChild(a)}return t>0},g.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},g.prototype._repaintEnd=function(){z.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},g.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},g.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},g.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},g.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},g.prototype._repaintLine=function(){{var t=this.dom.line,e=this.frame;this.options}this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&t.parentElement&&(e.removeChild(t.line),delete this.dom.line)},g.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},g.prototype.reflow=function(){var t=0,e=z.updateProperty,i=this.frame,n=this.range;if(!n)throw new Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var s=this.props,o=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(s.minorCharHeight=a.clientHeight,s.minorCharWidth=a.clientWidth),h&&(s.majorCharHeight=h.clientHeight,s.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=s.parentHeight&&(s.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.minorLabelTop=0,s.majorLabelTop=s.minorLabelTop+s.minorLabelHeight,s.minorLineTop=-this.top,s.minorLineHeight=Math.max(this.top+s.majorLabelHeight,0),s.minorLineWidth=1,s.majorLineTop=-this.top,s.majorLineHeight=Math.max(this.top+s.minorLabelHeight+s.majorLabelHeight,0),s.majorLineWidth=1,s.lineTop=0;break;case"top":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.majorLabelTop=0,s.minorLabelTop=s.majorLabelTop+s.majorLabelHeight,s.minorLineTop=s.minorLabelTop,s.minorLineHeight=Math.max(d-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(d-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw new Error('Unkown orientation "'+this.getOption("orientation")+'"')}var c=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",c),this._updateConversion();var l=z.convert(n.start,"Number"),u=z.convert(n.end,"Number"),p=this.toTime(5*(s.minorCharWidth||10)).valueOf()-this.toTime(0).valueOf();this.step=new TimeStep(new Date(l),new Date(u),p),t+=e(s.range,"start",l),t+=e(s.range,"end",u),t+=e(s.range,"minimumStep",p.valueOf())}return t>0},g.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},m.prototype=new u,m.prototype.setOptions=u.prototype.setOptions,m.prototype.getContainer=function(){return this.frame},m.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCurrentTime"))return void(t&&(i.removeChild(t),delete this.frame));t||(t=document.createElement("div"),t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t),this.frame=t),e.conversion||e._updateConversion();var n=new Date,s=e.toScreen(n);t.style.left=s+"px",t.title="Current time: "+n,void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer);var o=this,r=1/e.conversion.scale/2;return 30>r&&(r=30),this.currentTimeTimer=setTimeout(function(){o.repaint()},r),!1},v.prototype=new u,v.prototype.setOptions=u.prototype.setOptions,v.prototype.getContainer=function(){return this.frame},v.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCustomTime"))return void(t&&(i.removeChild(t),delete this.frame));if(!t){t=document.createElement("div"),t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t);var n=document.createElement("div");n.style.position="relative",n.style.top="0px",n.style.left="-10px",n.style.height="100%",n.style.width="20px",t.appendChild(n),this.frame=t,this.subscribe(this,"movetime")}e.conversion||e._updateConversion();var s=e.toScreen(this.customTime);return t.style.left=s+"px",t.title="Time: "+this.customTime,!1},v.prototype._setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},v.prototype._getCustomTime=function(){return new Date(this.customTime.valueOf())},v.prototype.subscribe=function(t,e){var i=this,n={component:t,event:e,callback:function(t){i._onMouseDown(t,n)},params:{}};t.on("mousedown",n.callback),i.listeners.push(n)},v.prototype.on=function(t,e){var i=this.frame;if(!i)throw new Error("Cannot add event listener: no parent attached");F.addListener(this,t,e),z.addEventListener(i,t,e)},v.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=z.getPageX(t),i.moved=!1,i.customTime=this.customTime;var s=this;i.onMouseMove||(i.onMouseMove=function(t){s._onMouseMove(t,e)},z.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){s._onMouseUp(t,e)},z.addEventListener(document,"mouseup",i.onMouseUp)),z.stopPropagation(t),z.preventDefault(t)}},v.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=this.parent,s=z.getPageX(t);void 0===i.mouseX&&(i.mouseX=s);var o=s-i.mouseX;Math.abs(o)>=1&&(i.moved=!0);var r=n.toScreen(i.customTime),a=r+o,h=n.toTime(a);this._setCustomTime(h),F.trigger(this,"timechange",{customTime:this.customTime}),z.preventDefault(t)},v.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;i.onMouseMove&&(z.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(z.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&F.trigger(this,"timechanged",{customTime:this.customTime})},y.prototype=new p,y.types={box:w,range:S,rangeoverflow:E,point:b},y.prototype.setOptions=u.prototype.setOptions,y.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},y.prototype.setSelection=function(t){var e,i,n,s,o;if(t){if(!Array.isArray(t))throw new TypeError("Array expected");for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],s=this.items[n],s&&s.unselect();for(this.selection=[],e=0,i=t.length;i>e;e++)n=t[e],s=this.items[n],s&&(this.selection.push(n),s.select());o=this.selection.concat([]),F.trigger(this,"select",{ids:o}),this.controller&&this.requestRepaint()}},y.prototype.getSelection=function(){return this.selection.concat([])},y.prototype._deselect=function(t){for(var e=this.selection,i=0,n=e.length;n>i;i++)if(e[i]==t){e.splice(i,1);break}},y.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=n.className;a&&z.addClassName(r,z.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var c=document.createElement("div");c.className="itemset-axis",this.dom.axis=c,this.frame=r,t+=1}if(!this.parent)throw new Error("Cannot repaint itemset: no parent attached");var l=this.parent.getContainer();if(!l)throw new Error("Cannot repaint itemset: parent has no container element");r.parentNode||(l.appendChild(r),t+=1),this.dom.axis.parentNode||(l.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==s?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var u=this,p=this.queue,f=this.itemsData,g=this.items,m={};for(var v in p)if(p.hasOwnProperty(v)){var _=p[v],w=g[v],b=_.action;switch(b){case"add":case"update":var S=f&&f.get(v,m);if(S){var E=S.type||S.start&&S.end&&"range"||n.type||"box",T=y.types[E];if(w&&(T&&w instanceof T?(w.data=S,t++):(t+=w.hide(),w=null)),!w){if(!T)throw new TypeError('Unknown item type "'+E+'"');w=new T(u,S,n,o),w.id=_.id,t++}w.repaint(),g[v]=w}delete p[v];break;case"remove":w&&(w.selected&&u._deselect(v),t+=w.hide()),delete g[v],delete p[v];break;default:console.log('Error: unknown action "'+b+'"')}}return z.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},y.prototype.getForeground=function(){return this.dom.foreground},y.prototype.getBackground=function(){return this.dom.background},y.prototype.getAxis=function(){return this.dom.axis},y.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,s=z.updateProperty,o=z.option.asNumber,r=z.option.asSize,a=this.frame;if(a){this._updateConversion(),z.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),c=null!=r(e.height);if(c)h=a.offsetHeight;else{var l=this.stack.ordered;if(l.length){var u=l[0].top,p=l[0].top+l[0].height;z.forEach(l,function(t){u=Math.min(u,t.top),p=Math.max(p,t.top+t.height)}),h=p-u+i+n}else h=i+n}null!=d&&(h=Math.min(h,d)),t+=s(this,"height",h),t+=s(this,"top",a.offsetTop),t+=s(this,"left",a.offsetLeft),t+=s(this,"width",a.offsetWidth)}else t+=1;return t>0},y.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t +},y.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(t){if(!(t instanceof o||t instanceof r))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(n&&(z.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;z.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},y.prototype.getItems=function(){return this.itemsData},y.prototype._onUpdate=function(t){this._toQueue("update",t)},y.prototype._onAdd=function(t){this._toQueue("add",t)},y.prototype._onRemove=function(t){this._toQueue("remove",t)},y.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]={id:e,action:t}}),this.controller&&this.requestRepaint()},y.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},y.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},y.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},_.prototype.select=function(){this.selected=!0,this.visible&&this.repaint()},_.prototype.unselect=function(){this.selected=!1,this.visible&&this.repaint()},_.prototype.show=function(){return!1},_.prototype.hide=function(){return!1},_.prototype.repaint=function(){return!1},_.prototype.reflow=function(){return!1},_.prototype.getWidth=function(){return this.width},w.prototype=new _(null,null),w.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");if(!e.box.parentNode){var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");i.appendChild(e.box),t=!0}if(!e.line.parentNode){var n=this.parent.getBackground();if(!n)throw new Error("Cannot repaint time axis: parent has no background container element");n.appendChild(e.line),t=!0}if(!e.dot.parentNode){var s=this.parent.getAxis();if(!n)throw new Error("Cannot repaint time axis: parent has no axis container element");s.appendChild(e.dot),t=!0}if(this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},w.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},w.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},w.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,c,l,u=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(c=this.data,l=this.parent&&this.parent.range,c&&l){var p=l.end-l.start;this.visible=c.start>l.start-p&&c.start0},w.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot",t.box["timeline-item"]=this)},w.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,s=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",s.style.left=e.line.left+"px","top"==i?(s.style.top="0px",s.style.height=this.top+"px"):(s.style.top=this.top+this.height+"px",s.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},b.prototype=new _(null,null),b.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},b.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},b.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},b.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,c=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var l=d.end-d.start;this.visible=h.start>d.start-l&&h.start0},b.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot),t.point["timeline-item"]=this)},b.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},S.prototype=new _(null,null),S.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},S.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},S.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},S.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,c,l,u,p,f,g,m=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw new Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start),a=o.toScreen(this.data.end),c=z.updateProperty,l=t.box,u=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,m+=c(e.content,"width",t.content.offsetWidth),m+=c(this,"height",l.offsetHeight),-u>r&&(r=-u),a>2*u&&(a=2*u),p=0>r?Math.min(-r,a-r-e.content.width-2*s):0,m+=c(e.content,"left",p),"top"==f?(g=n,m+=c(this,"top",g)):(g=o.height-this.height-n,m+=c(this,"top",g)),m+=c(this,"left",r),m+=c(this,"width",Math.max(a-r,1))):m+=1),m>0},S.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content),t.box["timeline-item"]=this)},S.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},E.prototype=new S(null,null),E.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item rangeoverflow"+n,t=!0)}return t},E.prototype.getWidth=function(){return void 0!==this.props.content&&this.width0},x.prototype=new p,x.prototype.setOptions=u.prototype.setOptions,x.prototype.setRange=function(){},x.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},x.prototype.getItems=function(){return this.itemsData},x.prototype.setRange=function(t){this.range=t},x.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(z.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;z.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},x.prototype.getGroups=function(){return this.groupsData},x.prototype.setSelection=function(t){var e=[],i=this.groups;for(var n in i)if(i.hasOwnProperty(n)){var s=i[n];s.setSelection(t)}return e},x.prototype.getSelection=function(){var t=[],e=this.groups;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];t=t.concat(n.getSelection())}return t},x.prototype.repaint=function(){var t,e,i,n,s=0,o=z.updateProperty,r=z.option.asSize,a=z.option.asElement,h=this.options,d=this.dom.frame,c=this.dom.labels,l=this.dom.labelSet;if(!this.parent)throw new Error("Cannot repaint groupset: no parent attached");var u=this.parent.getContainer();if(!u)throw new Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var p=h.className;p&&z.addClassName(d,z.option.asString(p)),s+=1}d.parentNode||(u.appendChild(d),s+=1);var f=a(h.labelContainer);if(!f)throw new Error('Cannot repaint groupset: option "labelContainer" not defined');c||(c=document.createElement("div"),c.className="labels",this.dom.labels=c),l||(l=document.createElement("div"),l.className="label-set",c.appendChild(l),this.dom.labelSet=l),c.parentNode&&c.parentNode==f||(c.parentNode&&c.parentNode.removeChild(c.parentNode),f.appendChild(c)),s+=o(d.style,"height",r(h.height,this.height+"px")),s+=o(d.style,"top",r(h.top,"0px")),s+=o(d.style,"left",r(h.left,"0px")),s+=o(d.style,"width",r(h.width,"100%")),s+=o(l.style,"top",r(h.top,"0px")),s+=o(l.style,"height",r(h.height,this.height+"px"));var g=this,m=this.queue,v=this.groups,y=this.groupsData,_=Object.keys(m);if(_.length){_.forEach(function(t){var e=m[t],i=v[t];switch(e){case"add":case"update":if(!i){var n=Object.create(g.options);z.extend(n,{height:null,maxHeight:null}),i=new T(g,t,n),i.setItems(g.itemsData),v[t]=i,g.controller.add(i)}i.data=y.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete v[t],g.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupOrder});for(t=0;t0},x.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var n=document.createElement("div");n.className="inner",i.appendChild(n);var s=e.data&&e.data.content;s instanceof Element?n.appendChild(s):void 0!=s&&(n.innerHTML=s);var o=e.data&&e.data.className;return o&&z.addClassName(i,o),e.label=i,i},x.prototype.getContainer=function(){return this.dom.frame},x.prototype.getLabelsWidth=function(){return this.props.labels.width},x.prototype.reflow=function(){var t,e,i=0,n=this.options,s=z.updateProperty,o=z.option.asNumber,r=z.option.asSize,a=this.dom.frame;if(a){var h,d=o(n.maxHeight),c=null!=r(n.height);if(c)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var l=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var u=e.props&&e.props.label&&e.props.label.width||0;l=Math.max(l,u)}return i+=s(this.props.labels,"width",l),i>0},x.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},x.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},x.prototype._onUpdate=function(t){this._toQueue(t,"update")},x.prototype._onAdd=function(t){this._toQueue(t,"add")},x.prototype._onRemove=function(t){this._toQueue(t,"remove")},x.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},C.prototype.setOptions=function(t){z.extend(this.options,t),this.range.setRange(t.start,t.end),this.controller.reflow(),this.controller.repaint()},C.prototype.setCustomTime=function(t){this.customtime._setCustomTime(t)},C.prototype.getCustomTime=function(){return new Date(this.customtime.customTime.valueOf())},C.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),s=n.min,r=n.max;if(null!=s&&null!=r){var a=r.valueOf()-s.valueOf();0>=a&&(a=864e5),s=new Date(s.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(s=z.convert(this.options.start,"Date")),void 0!=this.options.end&&(r=z.convert(this.options.end,"Date")),(null!=s||null!=r)&&this.range.setRange(s,r)}},C.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?x:y;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);z.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!z.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},C.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var s=t.max("start");s&&(i=s.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},C.prototype.setSelection=function(t){this.content&&this.content.setSelection(t)},C.prototype.getSelection=function(){return this.content?this.content.getSelection():[]},C.prototype.on=function(t,e){var i=["rangechange","rangechanged","select"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},C.prototype.off=function(t,e){F.removeListener(this,t,e)},C.prototype._trigger=function(t,e){F.trigger(this,t,e||{})},C.prototype._onSelectItem=function(t){var e=this._itemFromTarget(t),i=e?[e.id]:[];this.setSelection(i),this._trigger("select",{items:this.getSelection()}),t.stopPropagation()},C.prototype._onMultiSelectItem=function(t){var e,i=this._itemFromTarget(t);if(i){e=this.getSelection();var n=e.indexOf(i.id);-1==n?e.push(i.id):e.splice(n,1),this.setSelection(e),this._trigger("select",{items:this.getSelection()}),t.stopPropagation()}},C.prototype._itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},function(t){function e(t){return C=t,u()}function i(){M=0,D=C.charAt(0)}function n(){M++,D=C.charAt(M)}function s(){return C.charAt(M+1)}function o(t){return N.test(t)}function r(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var n=e.split("."),s=t;n.length;){var o=n.shift();n.length?(s[o]||(s[o]={}),s=s[o]):s[o]=i}}function h(t,e){for(var i,n,s=null,o=[t],a=t;a.parent;)o.push(a.parent),a=a.parent;if(a.nodes)for(i=0,n=a.nodes.length;n>i;i++)if(e.id===a.nodes[i].id){s=a.nodes[i];break}for(s||(s={id:e.id},t.node&&(s.attr=r(s.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(s)&&h.nodes.push(s)}e.attr&&(s.attr=r(s.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function c(t,e,i,n,s){var o={from:e,to:i,type:n};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},s),o}function l(){for(O=T.NULL,I="";" "==D||" "==D||"\n"==D||"\r"==D;)n();do{var t=!1;if("#"==D){for(var e=M-1;" "==C.charAt(e)||" "==C.charAt(e);)e--;if("\n"==C.charAt(e)||""==C.charAt(e)){for(;""!=D&&"\n"!=D;)n();t=!0}}if("/"==D&&"/"==s()){for(;""!=D&&"\n"!=D;)n();t=!0}if("/"==D&&"*"==s()){for(;""!=D;){if("*"==D&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==D||" "==D||"\n"==D||"\r"==D;)n()}while(t);if(""==D)return void(O=T.DELIMITER);var i=D+s();if(x[i])return O=T.DELIMITER,I=i,n(),void n();if(x[D])return O=T.DELIMITER,I=D,void n();if(o(D)||"-"==D){for(I+=D,n();o(D);)I+=D,n();return"false"==I?I=!1:"true"==I?I=!0:isNaN(Number(I))||(I=Number(I)),void(O=T.IDENTIFIER)}if('"'==D){for(n();""!=D&&('"'!=D||'"'==D&&'"'==s());)I+=D,'"'==D&&n(),n();if('"'!=D)throw w('End of string " expected');return n(),void(O=T.IDENTIFIER)}for(O=T.UNKNOWN;""!=D;)I+=D,n();throw new SyntaxError('Syntax error in part "'+b(I,30)+'"')}function u(){var t={};if(i(),l(),"strict"==I&&(t.strict=!0,l()),("graph"==I||"digraph"==I)&&(t.type=I,l()),O==T.IDENTIFIER&&(t.id=I,l()),"{"!=I)throw w("Angle bracket { expected");if(l(),p(t),"}"!=I)throw w("Angle bracket } expected");if(l(),""!==I)throw w("End of file expected");return l(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==I&&"}"!=I;)f(t),";"==I&&l()}function f(t){var e=g(t);if(e)return void y(t,e);var i=m(t);if(!i){if(O!=T.IDENTIFIER)throw w("Identifier expected");var n=I;if(l(),"="==I){if(l(),O!=T.IDENTIFIER)throw w("Identifier expected");t[n]=I,l()}else v(t,n)}}function g(t){var e=null;if("subgraph"==I&&(e={},e.type="subgraph",l(),O==T.IDENTIFIER&&(e.id=I,l())),"{"==I){if(l(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=I)throw w("Angle bracket } expected");l(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function m(t){return"node"==I?(l(),t.node=_(),"node"):"edge"==I?(l(),t.edge=_(),"edge"):"graph"==I?(l(),t.graph=_(),"graph"):null}function v(t,e){var i={id:e},n=_();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==I||"--"==I;){var i,n=I;l();var s=g(t);if(s)i=s;else{if(O!=T.IDENTIFIER)throw w("Identifier or subgraph expected");i=I,h(t,{id:i}),l()}var o=_(),r=c(t,e,i,n,o);d(t,r),e=i}}function _(){for(var t=null;"["==I;){for(l(),t={};""!==I&&"]"!=I;){if(O!=T.IDENTIFIER)throw w("Attribute name expected");var e=I;if(l(),"="!=I)throw w("Equal sign = expected");if(l(),O!=T.IDENTIFIER)throw w("Attribute value expected");var i=I;a(t,e,i),l(),","==I&&l()}if("]"!=I)throw w("Bracket ] expected");l()}return t}function w(t){return new SyntaxError(t+', got "'+b(I,30)+'" (char '+M+")")}function b(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function S(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function E(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var n=e(t),s={nodes:[],edges:[],options:{}};return n.nodes&&n.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};r(e,t.attr),e.image&&(e.shape="image"),s.nodes.push(e)}),n.edges&&n.edges.forEach(function(t){var e,n;e=t.from instanceof Object?t.from.nodes:{id:t.from},n=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);s.edges.push(e)}),S(e,n,function(e,n){var o=c(s,e.id,n.id,t.type,t.attr),r=i(o);s.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);s.edges.push(e)})}),n.attr&&(s.options=n.attr),s}var T={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},x={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},C="",M=0,D="",I="",O=T.NULL,N=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=E}("undefined"!=typeof z?z:n),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e-(r-o)),this.lineTo(t+s,e+o),this.lineTo(t-s,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e+(r-o)),this.lineTo(t+s,e-o),this.lineTo(t-s,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var n=0;10>n;n++){var s=n%2===0?1.3*i:.5*i;this.lineTo(t+s*Math.sin(2*n*Math.PI/10),e-s*Math.cos(2*n*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,n,s){var o=Math.PI/180;0>i-2*s&&(s=i/2),0>n-2*s&&(s=n/2),this.beginPath(),this.moveTo(t+s,e),this.lineTo(t+i-s,e),this.arc(t+i-s,e+s,s,270*o,360*o,!1),this.lineTo(t+i,e+n-s),this.arc(t+i-s,e+n-s,s,0,90*o,!1),this.lineTo(t+s,e+n),this.arc(t+s,e+n-s,s,90*o,180*o,!1),this.lineTo(t,e+s),this.arc(t+s,e+s,s,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,n){var s=.5522848,o=i/2*s,r=n/2*s,a=t+i,h=e+n,d=t+i/2,c=e+n/2;this.beginPath(),this.moveTo(t,c),this.bezierCurveTo(t,c-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,c-r,a,c),this.bezierCurveTo(a,c+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,c+r,t,c)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,o=i,r=n*s,a=.5522848,h=o/2*a,d=r/2*a,c=t+o,l=e+r,u=t+o/2,p=e+r/2,f=e+(n-r/2),g=e+n;this.beginPath(),this.moveTo(c,p),this.bezierCurveTo(c,p+d,u+h,l,u,l),this.bezierCurveTo(u-h,l,t,p+d,t,p),this.bezierCurveTo(t,p-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,c,p-d,c,p),this.lineTo(c,f),this.bezierCurveTo(c,f+d,u+h,g,u,g),this.bezierCurveTo(u-h,g,t,f+d,t,f),this.lineTo(t,p)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,n){var s=t-n*Math.cos(i),o=e-n*Math.sin(i),r=t-.9*n*Math.cos(i),a=e-.9*n*Math.sin(i),h=s+n/3*Math.cos(i+.5*Math.PI),d=o+n/3*Math.sin(i+.5*Math.PI),c=s+n/3*Math.cos(i-.5*Math.PI),l=o+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(c,l),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==u&&(u=.001);var o=s.length;this.moveTo(t,e);for(var r=i-t,a=n-e,h=a/r,d=Math.sqrt(r*r+a*a),c=0,l=!0;d>=.1;){var u=s[c++%o];u>d&&(u=d);var p=Math.sqrt(u*u/(1+h*h));0>r&&(p=-p),t+=p,e+=h*p,this[l?"lineTo":"moveTo"](t,e),d-=u,l=!l}}),M.prototype.resetCluster=function(){this.formationScale=void 0,this.clusterSize=1,this.containedNodes={},this.containedEdges={},this.clusterSessions=[]},M.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),-1==this.dynamicEdges.indexOf(t)&&this.dynamicEdges.push(t),this.dynamicEdgesLength=this.dynamicEdges.length,this._updateMass()},M.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&(this.edges.splice(e,1),this.dynamicEdges.splice(e,1)),this.dynamicEdgesLength=this.dynamicEdges.length,this._updateMass()},M.prototype._updateMass=function(){this.mass=1+.6*this.edges.length},M.prototype.setProperties=function(t,e){if(t){if(this.originalLabel=void 0,void 0!==t.id&&(this.id=t.id),void 0!==t.label&&(this.label=t.label,this.originalLabel=t.label),void 0!==t.title&&(this.title=t.title),void 0!==t.group&&(this.group=t.group),void 0!==t.x&&(this.x=t.x),void 0!==t.y&&(this.y=t.y),void 0!==t.value&&(this.value=t.value),void 0!==t.horizontalAlignLeft&&(this.horizontalAlignLeft=t.horizontalAlignLeft),void 0!==t.verticalAlignTop&&(this.verticalAlignTop=t.verticalAlignTop),void 0!==t.triggerFunction&&(this.triggerFunction=t.triggerFunction),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var n in i)i.hasOwnProperty(n)&&(this[n]=i[n])}if(void 0!==t.shape&&(this.shape=t.shape),void 0!==t.image&&(this.image=t.image),void 0!==t.radius&&(this.radius=t.radius),void 0!==t.color&&(this.color=M.parseColor(t.color)),void 0!==t.fontColor&&(this.fontColor=t.fontColor),void 0!==t.fontSize&&(this.fontSize=t.fontSize),void 0!==t.fontFace&&(this.fontFace=t.fontFace),void 0!==this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!==t.x,this.yFixed=this.yFixed||void 0!==t.y,this.radiusFixed=this.radiusFixed||void 0!==t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},M.parseColor=function(t){var e;return z.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,z.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},M.prototype.select=function(){this.selected=!0,this._reset()},M.prototype.unselect=function(){this.selected=!1,this._reset()},M.prototype.clearSizeCache=function(){this._reset()},M.prototype._reset=function(){this.width=void 0,this.height=void 0},M.prototype.getTitle=function(){return this.title},M.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var n=this.width/2,s=this.height/2,o=Math.sin(e)*n,r=Math.cos(e)*s; +return n*s/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},M.prototype._setForce=function(t,e){this.fx=t,this.fy=e},M.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},M.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i*t,this.x+=this.vx*t}if(!this.yFixed){var n=-this.damping*this.vy,s=(this.fy+n)/this.mass;this.vy+=s*t,this.y+=this.vy*t}},M.prototype.isFixed=function(){return this.xFixed&&this.yFixed},M.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t?!0:(this.vx=0,this.vy=0,!1)},M.prototype.isSelected=function(){return this.selected},M.prototype.getValue=function(){return this.value},M.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},M.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}this.baseRadiusValue=this.radius},M.prototype.draw=function(){throw"Draw method not initialized for node"},M.prototype.resize=function(){throw"Resize method not initialized for node"},M.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},M.prototype._resizeImage=function(){if(!this.width||!this.height){var t,e;if(this.value){this.radius=this.baseRadiusValue;var i=this.imageObj.height/this.imageObj.width;void 0!==i?(t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height):(t=0,e=0)}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e,this.width>0&&this.height>0&&(this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor)}},M.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;if(0!=this.imageObj.width){if(this.clusterSize>1){var i=this.clusterSize>1?10:0;i*=this.graphScaleInv,i=Math.min(.2*this.width,i),t.globalAlpha=.5,t.drawImage(this.imageObj,this.left-i,this.top-i,this.width+2*i,this.height+2*i)}t.globalAlpha=1,t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2}else e=this.y;this._label(t,this.label,this.x,e,void 0,"top")},M.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=.5*(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=.5*(this.clusterSize-1)*this.clusterSizeHeightFactor}},M.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.roundRect(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth,this.radius),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=i.width+2*e;this.width=n,this.height=n,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.database(this.x-this.width/2-2*t.lineWidth,this.y-.5*this.height-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=Math.max(i.width,i.height)+2*e;this.radius=n/2,this.width=n,this.height=n,this.radius+=.5*(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.circle(this.x,this.y,this.radius+2*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.width1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.ellipse(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.ellipse(this.left,this.top,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._drawDot=function(t){this._drawShape(t,"circle")},M.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},M.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},M.prototype._drawSquare=function(t){this._drawShape(t,"square")},M.prototype._drawStar=function(t){this._drawShape(t,"star")},M.prototype._resizeShape=function(){if(!this.width){this.radius=this.baseRadiusValue;var t=2*this.radius;this.width=t,this.height=t,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=.5*(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var i=2.5,n=2,s=2;switch(e){case"dot":s=2;break;case"square":s=2;break;case"triangle":s=3;break;case"triangleDown":s=3;break;case"star":s=4}t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?n:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t[e](this.x,this.y,this.radius+s*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?n:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.label&&this._label(t,this.label,this.x,this.y+this.height/2,void 0,"top")},M.prototype._resizeText=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._label(t,this.label,this.x,this.y)},M.prototype._label=function(t,e,i,n,s,o){if(e){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=s||"center",t.textBaseline=o||"middle";for(var r=e.split("\n"),a=r.length,h=this.fontSize+4,d=n+(1-a)/2*h,c=0;a>c;c++)t.fillText(r[c],i,d),d+=h}},M.prototype.getTextSize=function(t){if(void 0!==this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,n=0,s=0,o=e.length;o>s;s++)n=Math.max(n,t.measureText(e[s]).width);return{width:n,height:i}}return{width:0,height:0}},M.prototype.inArea=function(){return void 0!==this.width?this.x+this.width*this.graphScaleInv>=this.canvasTopLeft.x&&this.x-this.width*this.graphScaleInv=this.canvasTopLeft.y&&this.y-this.height*this.graphScaleInv=this.canvasTopLeft.x&&this.x=this.canvasTopLeft.y&&this.yh},D.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,n,s=this.length/4,o=this.from;o.width||o.resize(t),o.width>o.height?(i=o.x+o.width/2,n=o.y-s):(i=o.x+s,n=o.y-o.height/2),this._circle(t,i,n,s),e=this._pointOnCircle(i,n,s,.5),this._label(t,this.label,e.x,e.y)}},D.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax)*this.graphScaleInv:this.width*this.graphScaleInv},D.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},D.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},D.prototype._label=function(t,e,i,n){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var s=t.measureText(e).width,o=this.fontSize,r=i-s/2,a=n-o/2;t.fillRect(r,a,s,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},D.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!==this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!==this.dash.length&&void 0!==this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},D.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},D.prototype._pointOnCircle=function(t,e,i,n){var s=2*(n-3/8)*Math.PI;return{x:t+i*Math.cos(s),y:e-i*Math.sin(s)}},D.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),n=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var s,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(s=a.x+a.width/2,o=a.y-r):(s=a.x+r,o=a.y-a.height/2),this._circle(t,s,o,r);var i=.2*Math.PI,n=10+5*this.width;e=this._pointOnCircle(s,o,r,.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(s,o,r,.5),this._label(t,this.label,e.x,e.y))}},D.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var n=this.to.x-this.from.x,s=this.to.y-this.from.y,o=Math.sqrt(n*n+s*s),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,c=this.to.distanceToBorder(t,e),l=(o-c)/o,u=(1-l)*this.from.x+l*this.to.x,p=(1-l)*this.from.y+l*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(u,p),t.stroke(),i=10+5*this.width,t.arrow(u,p,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var g,m,v,y=this.from,_=this.length/4;y.width||y.resize(t),y.width>y.height?(g=y.x+y.width/2,m=y.y-_,v={x:g,y:y.y,angle:.9*Math.PI}):(g=y.x+_,m=y.y-y.height/2,v={x:y.x,y:m,angle:.6*Math.PI}),t.beginPath(),t.arc(g,m,_,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(g,m,_,.5),this._label(t,this.label,f.x,f.y))}},D._dist=function(t,e,i,n,s,o){var r=i-t,a=n-e,h=r*r+a*a,d=((s-t)*r+(o-e)*a)/h;d>1?d=1:0>d&&(d=0);var c=t+d*r,l=e+d*a,u=c-s,p=l-o;return Math.sqrt(u*u+p*p)},D.prototype.setScale=function(t){this.graphScaleInv=1/t},I.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},I.prototype.setText=function(t){this.frame.innerHTML=t},I.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,s=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>n&&(o=n-e-this.padding),os&&(r=s-i-this.padding),r1)return this.activeSector[this.activeSector.length-2];throw new TypeError("there are not enough sectors in the this.activeSector array.")},_setActiveSector:function(t){this.activeSector.push(t)},_forgetLastSector:function(){this.activeSector.pop()},_createNewSector:function(t){this.sectors.active[t]={nodes:{},edges:{},nodeIndices:[],formationScale:this.scale,drawingNode:void 0},this.sectors.active[t].drawingNode=new M({id:t,color:{background:"#eaefef",border:"495c5e"}},{},{},this.constants),this.sectors.active[t].drawingNode.clusterSize=2},_deleteActiveSector:function(t){delete this.sectors.active[t]},_deleteFrozenSector:function(t){delete this.sectors.frozen[t]},_freezeSector:function(t){this.sectors.frozen[t]=this.sectors.active[t],this._deleteActiveSector(t)},_activateSector:function(t){this.sectors.active[t]=this.sectors.frozen[t],this._deleteFrozenSector(t)},_mergeThisWithFrozen:function(t){for(var e in this.nodes)this.nodes.hasOwnProperty(e)&&(this.sectors.frozen[t].nodes[e]=this.nodes[e]);for(var i in this.edges)this.edges.hasOwnProperty(i)&&(this.sectors.frozen[t].edges[i]=this.edges[i]);for(var n=0;n1?this[t](n[0],n[1]):this[t](e)}this._loadLatestSector()},_doInAllFrozenSectors:function(t,e){if(void 0===e)for(var i in this.sectors.frozen)this.sectors.frozen.hasOwnProperty(i)&&(this._switchToFrozenSector(i),this[t]());else for(var i in this.sectors.frozen)if(this.sectors.frozen.hasOwnProperty(i)){this._switchToFrozenSector(i);var n=Array.prototype.splice.call(arguments,1);n.length>1?this[t](n[0],n[1]):this[t](e)}this._loadLatestSector()},_doInNavigationSector:function(t,e){if(this._switchToNavigationSector(),void 0===e)this[t]();else{var i=Array.prototype.splice.call(arguments,1);i.length>1?this[t](i[0],i[1]):this[t](e)}this._loadLatestSector()},_doInAllSectors:function(t,e){var i=Array.prototype.splice.call(arguments,1);void 0===e?(this._doInAllActiveSectors(t),this._doInAllFrozenSectors(t)):i.length>1?(this._doInAllActiveSectors(t,i[0],i[1]),this._doInAllFrozenSectors(t,i[0],i[1])):(this._doInAllActiveSectors(t,e),this._doInAllFrozenSectors(t,e))},_clearNodeIndexList:function(){var t=this._sector();this.sectors.active[t].nodeIndices=[],this.nodeIndices=this.sectors.active[t].nodeIndices},_drawSectorNodes:function(t,e){var i,n=1e9,s=-1e9,o=1e9,r=-1e9;for(var a in this.sectors[e])if(this.sectors[e].hasOwnProperty(a)&&void 0!==this.sectors[e][a].drawingNode){this._switchToSector(a,e),n=1e9,s=-1e9,o=1e9,r=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),o>i.x-.5*i.width&&(o=i.x-.5*i.width),ri.y-.5*i.height&&(n=i.y-.5*i.height),st&&n>s;)s%3==0?this.forceAggregateHubs():this.increaseClusterLevel(),i=this.nodeIndices.length,s+=1;s>1&&1==e&&this.repositionNodes()},openCluster:function(t){var e=this.moving;if(t.clusterSize>this.constants.clustering.sectorThreshold&&this._nodeInActiveArea(t)&&("default"!=this._sector()||1!=this.nodeIndices.length)){this._addSector(t);for(var i=0;this.nodeIndices.lengthi;)this.decreaseClusterLevel(),i+=1}else this._expandClusterNode(t,!1,!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels();this.moving!=e&&this.start()},updateClustersDefault:function(){1==this.constants.clustering.enabled&&this.updateClusters(0,!1,!1)},increaseClusterLevel:function(){this.updateClusters(-1,!1,!0)},decreaseClusterLevel:function(){this.updateClusters(1,!1,!0)},updateClusters:function(t,e,i){var n=this.moving,s=this.nodeIndices.length;this.previousScale>this.scale&&0==t&&this._collapseSector(),this.previousScale>this.scale||-1==t?this._formClusters(i):(this.previousScalethis.scale||-1==t)&&(this._aggregateHubs(i),this._updateNodeIndexList()),(this.previousScale>this.scale||-1==t)&&(this.handleChains(),this._updateNodeIndexList()),this.previousScale=this.scale,this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.lengththis.constants.clustering.chainThreshold&&this._reduceAmountOfChains(1-this.constants.clustering.chainThreshold/t)},_aggregateHubs:function(t){this._getHubSize(),this._formClustersByHub(t,!1)},forceAggregateHubs:function(){var t=this.moving,e=this.nodeIndices.length;this._aggregateHubs(!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.length!=e&&(this.clusterSession+=1),this.moving!=t&&this.start()},_openClustersBySize:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];1==e.inView()&&(e.width*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientWidth||e.height*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientHeight)&&this.openCluster(e)}},_openClusters:function(t,e){for(var i=0;i1&&(t.clusterSizei)){var r=o.from,a=o.to;o.to.mass>o.from.mass&&(r=o.to,a=o.from),1==a.dynamicEdgesLength?this._addToCluster(r,a,!1):1==r.dynamicEdgesLength&&this._addToCluster(a,r,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],n=i.toId==e.id?this.nodes[i.fromId]:this.nodes[i.toId];e.id!=n.id&&(n.mass>e.mass?this._addToCluster(n,e,!0):this._addToCluster(e,n,!0))}}},_formClustersByHub:function(t,e){for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&this._formClusterFromHub(this.nodes[i],t,e)},_formClusterFromHub:function(t,e,i,n){if(void 0===n&&(n=0),t.dynamicEdgesLength>=this.hubThreshold&&0==i||t.dynamicEdgesLength==this.hubThreshold&&1==i){for(var s,o,r,a=this.constants.clustering.clusterEdgeThreshold/this.scale,h=!1,d=[],c=t.dynamicEdges.length,l=0;c>l;l++)d.push(t.dynamicEdges[l].id);if(0==e)for(h=!1,l=0;c>l;l++){var u=this.edges[d[l]];if(void 0!==u&&u.connected&&u.toId!=u.fromId&&(s=u.to.x-u.from.x,o=u.to.y-u.from.y,r=Math.sqrt(s*s+o*o),a>r)){h=!0;break}}if(!e&&h||e)for(l=0;c>l;l++)if(u=this.edges[d[l]],void 0!==u){var p=this.nodes[u.fromId==t.id?u.toId:u.fromId];p.dynamicEdges.length<=this.hubThreshold+n&&p.id!=t.id&&this._addToCluster(t,p,e)}}},_addToCluster:function(t,e,i){t.containedNodes[e.id]=e;for(var n=0;n1)for(var n=0;n1&&(e.label="[".concat(String(e.clusterSize),"]"))}for(t in this.nodes)this.nodes.hasOwnProperty(t)&&(e=this.nodes[t],1==e.clusterSize&&(e.label=void 0!==e.originalLabel?e.originalLabel:String(e.id)))},_nodeInActiveArea:function(t){return Math.abs(t.x-this.areaCenter.x)<=this.constants.clustering.activeAreaBoxSize/this.scale&&Math.abs(t.y-this.areaCenter.y)<=this.constants.clustering.activeAreaBoxSize/this.scale},repositionNodes:function(){for(var t=0;tn&&(n=o.dynamicEdgesLength),t+=o.dynamicEdgesLength,e+=Math.pow(o.dynamicEdgesLength,2),i+=1}t/=i,e/=i;var r=e-Math.pow(t,2),a=Math.sqrt(r);this.hubThreshold=Math.floor(t+2*a),this.hubThreshold>n&&(this.hubThreshold=n)},_reduceAmountOfChains:function(t){this.hubThreshold=2;var e=Math.floor(this.nodeIndices.length*t);for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&e>0&&(this._formClusterFromHub(this.nodes[i],!0,!0,1),e-=1)},_getChainFraction:function(){var t=0,e=0;for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&(2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&(t+=1),e+=1);return t/e}},W={_getNodesOverlappingWith:function(t,e){var i=this.nodes;for(var n in i)i.hasOwnProperty(n)&&i[n].isOverlappingWith(t)&&e.push(n)},_getAllNodesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getNodesOverlappingWith",t,e),e},_getAllNavigationNodesOverlappingWith:function(t){var e=[];return this._doInNavigationSector("_getNodesOverlappingWith",t,e),e},_pointerToPositionObject:function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y);return{left:e,top:i,right:e,bottom:i}},_pointerToScreenPositionObject:function(t){var e=t.x,i=t.y;return{left:e,top:i,right:e,bottom:i}},_getNavigationNodeAt:function(t){var e=this._pointerToScreenPositionObject(t),i=this._getAllNavigationNodesOverlappingWith(e);return i.length>0?this.sectors.navigation.nodes[i[i.length-1]]:null},_getNodeAt:function(t){var e=this._pointerToPositionObject(t);return overlappingNodes=this._getAllNodesOverlappingWith(e),overlappingNodes.length>0?this.nodes[overlappingNodes[overlappingNodes.length-1]]:null},_getEdgeAt:function(){return null},_addToSelection:function(t){this.selection.push(t.id),this.selectionObj[t.id]=t},_removeFromSelection:function(t){for(var e=0;ee;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');this._selectNode(s,!0,!0)}this.redraw()},_updateSelection:function(){for(var t=0;tt.x-t.width&&(n=t.x-t.width),st.y-t.height&&(e=t.y-t.height),i=this.constants.clustering.initialMaxNodes)var n=38.8467/(e-14.50184)+.0116;else var n=42.54117319/(e+39.31966387)+.1944405;else{var s=1.1*(Math.abs(i.minX)+Math.abs(i.maxX)),o=1.1*(Math.abs(i.minY)+Math.abs(i.maxY)),r=this.frame.canvas.clientWidth/s,a=this.frame.canvas.clientHeight/o;n=a>=r?r:a}n>1&&(n=1),this.pinch.mousewheelScale=n,this._setScale(n),this._centerGraph(i),this.start()},O.prototype._updateNodeIndexList=function(){this._clearNodeIndexList();for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodeIndices.push(t)},O.prototype.setData=function(t,e){if(void 0===e&&(e=!1),t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var i=U.util.DOTToGraph(t.dot);return void this.setData(i)}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this._putDataInSector(),e||(this.stabilize&&this._doStabilize(),this.moving=!0,this.start())},O.prototype.setOptions=function(t){if(t){if(void 0!==t.width&&(this.width=t.width),void 0!==t.height&&(this.height=t.height),void 0!==t.stabilize&&(this.stabilize=t.stabilize),void 0!==t.selectable&&(this.selectable=t.selectable),t.clustering){this.constants.clustering.enabled=!0;for(var e in t.clustering)t.clustering.hasOwnProperty(e)&&(this.constants.clustering[e]=t.clustering[e])}else void 0!==t.clustering&&(this.constants.clustering.enabled=!1);if(t.navigation){this.constants.navigation.enabled=!0;for(var e in t.navigation)t.navigation.hasOwnProperty(e)&&(this.constants.navigation[e]=t.navigation[e])}else void 0!==t.navigation&&(this.constants.navigation.enabled=!1);if(t.keyboard){this.constants.keyboard.enabled=!0;for(var e in t.keyboard)t.keyboard.hasOwnProperty(e)&&(this.constants.keyboard[e]=t.keyboard[e])}else void 0!==t.keyboard&&(this.constants.keyboard.enabled=!1);if(t.edges){for(e in t.edges)t.edges.hasOwnProperty(e)&&(this.constants.edges[e]=t.edges[e]);void 0!==t.edges.length&&t.nodes&&void 0===t.nodes.distance&&(this.constants.edges.length=t.edges.length,this.constants.nodes.distance=1.25*t.edges.length),t.edges.fontColor||(this.constants.edges.fontColor=t.edges.color),t.edges.dash&&(void 0!==t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!==t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!==t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=M.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var n=t.groups[i];this.groups.add(i,n)}}this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1),this._loadNavigationControls(),this._createKeyBinds(),this._redraw()},O.prototype.on=function(t,e){var i=["select"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},O.prototype.off=function(t,e){F.removeListener(this,t,e)},O.prototype._trigger=function(t,e){F.trigger(this,t,e)},O.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=N(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("doubletap",e._onDoubleTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("release",e._onRelease.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("DOMMouseScroll",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},O.prototype._createKeyBinds=function(){var t=this;this.mousetrap=A,this.mousetrap.reset(),1==this.constants.keyboard.enabled&&(this.mousetrap.bind("up",this._moveUp.bind(t),"keydown"),this.mousetrap.bind("up",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("down",this._moveDown.bind(t),"keydown"),this.mousetrap.bind("down",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("left",this._moveLeft.bind(t),"keydown"),this.mousetrap.bind("left",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("right",this._moveRight.bind(t),"keydown"),this.mousetrap.bind("right",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("=",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("=",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("-",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("-",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("[",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("[",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("]",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("]",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pageup",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("pageup",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pagedown",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("pagedown",this._stopZoom.bind(t),"keyup"))},O.prototype._getPointer=function(t){return{x:t.pageX-U.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-U.util.getAbsoluteTop(this.frame.canvas)}},O.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale(),this._handleTouch(this.drag.pointer)},O.prototype._onDragStart=function(){var t=this.drag,e=this._getNodeAt(t.pointer);if(t.dragging=!0,t.selection=[],t.translation=this._getTranslation(),t.nodeId=null,null!=e){t.nodeId=e.id,e.isSelected()||this._selectNode(e,!1);var i=this;this.selection.forEach(function(e){var n=i.nodes[e];if(n){var s={id:e,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}})}},O.prototype._onDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.touches[0]),i=this,n=this.drag,s=n.selection;if(s&&s.length){var o=e.x-n.pointer.x,r=e.y-n.pointer.y;s.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+r))}),this.moving||(this.moving=!0,this.start())}else{var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},O.prototype._onDragEnd=function(){this.drag.dragging=!1;var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},O.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]);this._handleTap(e)},O.prototype._onDoubleTap=function(t){var e=this._getPointer(t.gesture.touches[0]);this._handleDoubleTap(e)},O.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]);this._handleOnHold(e)},O.prototype._onRelease=function(){this._handleOnRelease()},O.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},O.prototype._zoom=function(t,e){var i=this._getScale();1e-5>t&&(t=1e-5),t>10&&(t=10);var n=this._getTranslation(),s=t/i,o=(1-s)*e.x+n.x*s,r=(1-s)*e.y+n.y*s;return this.areaCenter={x:this._canvasToX(e.x),y:this._canvasToY(e.y)},this.pinch.mousewheelScale=t,this._setScale(t),this._setTranslation(o,r),this.updateClustersDefault(),this._redraw(),t},O.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mousewheelScale"in this.pinch||(this.pinch.mousewheelScale=1);var i=this.pinch.mousewheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=z.fakeGesture(this,t),o=this._getPointer(s.center);i=this._zoom(i,o)}t.preventDefault()},O.prototype._onMouseMoveTitle=function(t){var e=z.fakeGesture(this,t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var n=this,s=function(){n._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.drag.dragging||(this.popupTimer=setTimeout(s,300))},O.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},n=this.popupNode;if(void 0==this.popupNode){var s=this.nodes;for(e in s)if(s.hasOwnProperty(e)){var o=s[e];if(void 0!==o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0===this.popupNode){var r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!==a.getTitle()&&a.isOverlappingWith(i)){this.popupNode=a;break}}}if(this.popupNode){if(this.popupNode!=n){var h=this;h.popup||(h.popup=new I(h.frame)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},O.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},O.prototype._getConnectionCount=function(t){function e(t){for(var e=[],i=0,n=t.length;n>i;i++)for(var s=t[i],o=s.edges,r=0,a=o.length;a>r;r++){var h=o[r],d=null;h.from==s?d=h.to:h.to==s&&(d=h.from);var c,l;if(d)for(c=0,l=t.length;l>c;c++)if(t[c]==d){d=null;break}if(d)for(c=0,l=e.length;l>c;c++)if(e[c]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],n=this.nodes;for(var s in n)if(n.hasOwnProperty(s)){for(var o=[n[s]],r=0;t>r;r++)o=o.concat(e(o));i.push(o)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},O.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight,1==this.constants.navigation.enabled&&this._relocateNavigation()},O.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof o||t instanceof r)this.nodesData=t;else if(t instanceof Array)this.nodesData=new o,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new o}if(e&&z.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;z.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},O.prototype._addNodes=function(t){for(var e,i=0,n=t.length;n>i;i++){e=t[i];var s=this.nodesData.get(e),o=new M(s,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()){var r=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);o.x=r*Math.cos(h),o.y=r*Math.sin(h),this.moving=!0}}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(this.nodes)},O.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new M(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(e)},O.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,n=t.length;n>i;i++){var s=t[i];delete e[s]}this._updateNodeIndexList(),this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},O.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof o||t instanceof r)this.edgesData=t;else if(t instanceof Array)this.edgesData=new o,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new o}if(e&&z.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;z.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},O.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o];r&&r.disconnect();var a=i.get(o,{showInternalIds:!0});e[o]=new D(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},O.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new D(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},O.prototype._removeEdges=function(t){for(var e=this.edges,i=0,n=t.length;n>i;i++){var s=t[i],o=e[s];o&&(o.disconnect(),delete e[s])}this.moving=!0,this._updateValueRange(e)},O.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var n=i[t];n.from=null,n.to=null,n.connect()}},O.prototype._updateValueRange=function(t){var e,i=void 0,n=void 0;for(e in t)if(t.hasOwnProperty(e)){var s=t[e].getValue();void 0!==s&&(i=void 0===i?s:Math.min(s,i),n=void 0===n?s:Math.max(s,n))}if(void 0!==i&&void 0!==n)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,n)},O.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},O.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this.canvasTopLeft={x:this._canvasToX(0),y:this._canvasToY(0)},this.canvasBottomRight={x:this._canvasToX(this.frame.canvas.clientWidth),y:this._canvasToY(this.frame.canvas.clientHeight)},this._doInAllSectors("_drawAllSectorNodes",t),this._doInAllSectors("_drawEdges",t),this._doInAllSectors("_drawNodes",t),t.restore(),1==this.constants.navigation.enabled&&this._doInNavigationSector("_drawNodes",t,!0)},O.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},O.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},O.prototype._setScale=function(t){this.scale=t},O.prototype._getScale=function(){return this.scale},O.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},O.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},O.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},O.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},O.prototype._drawNodes=function(t,e){void 0===e&&(e=!1);var i=this.nodes,n=[];for(var s in i)i.hasOwnProperty(s)&&(i[s].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight),i[s].isSelected()?n.push(s):(i[s].inArea()||e)&&i[s].draw(t));for(var o=0,r=n.length;r>o;o++)(i[n[o]].inArea()||e)&&i[n[o]].draw(t)},O.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.setScale(this.scale),n.connected&&e[i].draw(t)}},O.prototype._doStabilize=function(){for(var t=0,e=this.constants.minVelocity,i=!1;!i&&tthis.constants.clustering.clusterThreshold&&1==this.constants.clustering.enabled&&this.clusterToFit(this.constants.clustering.reduceToNodes,!1),this._calculateForces())},O.prototype._calculateForces=function(){var t,e,i,n,s,o,r,a,h,d,c,l,u,p,f,g,m,v,y=this.nodes,_=this.edges,w=.08*this.forceFactor;for(g=0;gn&&(i=Math.atan2(e,t),r=.5*b>n?1:1/(1+Math.exp((n/b-1)*S)),r*=0==v?1:1+v*this.constants.clustering.forceAmplification,r*=this.forceFactor,s=Math.cos(i)*r,o=Math.sin(i)*r,l._addForce(-s,-o),u._addForce(s,o));for(f in _)_.hasOwnProperty(f)&&(p=_[f],p.connected&&this.nodes.hasOwnProperty(p.toId)&&this.nodes.hasOwnProperty(p.fromId)&&(v=p.to.clusterSize+p.from.clusterSize-2,t=p.to.x-p.from.x,e=p.to.y-p.from.y,d=p.length,d+=v*this.constants.clustering.edgeGrowth,h=Math.sqrt(t*t+e*e),i=Math.atan2(e,t),a=p.stiffness*(d-h)*this.forceFactor,s=Math.cos(i)*a,o=Math.sin(i)*a,p.from._addForce(-s,-o),p.to._addForce(s,o)))},O.prototype._isMoving=function(t){var e=t/this.scale,i=this.nodes;for(var n in i)if(i.hasOwnProperty(n)&&i[n].isMoving(e))return!0;return!1},O.prototype._discreteStepNodes=function(){var t=.01,e=this.nodes;for(var i in e)e.hasOwnProperty(i)&&e[i].discreteStep(t);var n=this.constants.minVelocity;this.moving=this._isMoving(n)},O.prototype.start=function(){if(!this.freezeSimulation)if(this.moving&&(this._doInAllActiveSectors("_initializeForceCalculation"),this._doInAllActiveSectors("_discreteStepNodes"),this._findCenter(this._getRange())),this.moving||0!=this.xIncrement||0!=this.yIncrement||0!=this.zoomIncrement){if(!this.timer){var t=this;this.timer=window.setTimeout(function(){if(t.timer=void 0,0!=t.xIncrement||0!=t.yIncrement){var e=t._getTranslation();t._setTranslation(e.x+t.xIncrement,e.y+t.yIncrement)}if(0!=t.zoomIncrement){var i={x:t.frame.canvas.clientWidth/2,y:t.frame.canvas.clientHeight/2};t._zoom(t.scale*(1+t.zoomIncrement),i)}t.start(),t._redraw()},this.renderTimestep)}}else this._redraw()},O.prototype.singleStep=function(){if(this.moving){this._initializeForceCalculation(),this._discreteStepNodes();var t=this.constants.minVelocity;this.moving=this._isMoving(t),this._redraw()}},O.prototype.toggleFreeze=function(){0==this.freezeSimulation?this.freezeSimulation=!0:(this.freezeSimulation=!1,this.start())},O.prototype._loadClusterSystem=function(){this.clusterSession=0,this.hubThreshold=5;for(var t in H)H.hasOwnProperty(t)&&(O.prototype[t]=H[t])},O.prototype._loadSectorSystem=function(){this.sectors={},this.activeSector=["default"],this.sectors.active={},this.sectors.active["default"]={nodes:{},edges:{},nodeIndices:[],formationScale:1,drawingNode:void 0},this.sectors.frozen={},this.sectors.navigation={nodes:{},edges:{},nodeIndices:[],formationScale:1,drawingNode:void 0},this.nodeIndices=this.sectors.active["default"].nodeIndices;for(var t in R)R.hasOwnProperty(t)&&(O.prototype[t]=R[t])},O.prototype._loadSelectionSystem=function(){this.selection=[],this.selectionObj={};for(var t in W)W.hasOwnProperty(t)&&(O.prototype[t]=W[t])},O.prototype._loadNavigationControls=function(){for(var t in j)j.hasOwnProperty(t)&&(O.prototype[t]=j[t]);1==this.constants.navigation.enabled&&this._loadNavigationElements()},O.prototype._relocateNavigation=function(){},O.prototype._unHighlightAll=function(){};var U={util:z,events:F,Controller:l,DataSet:o,DataView:r,Range:h,Stack:a,TimeStep:TimeStep,EventBus:s,components:{items:{Item:_,ItemBox:w,ItemPoint:b,ItemRange:S},Component:u,Panel:p,RootPanel:f,ItemSet:y,TimeAxis:g},graph:{Node:M,Edge:D,Popup:I,Groups:Groups,Images:Images},Timeline:C,Graph:O};"undefined"!=typeof n&&(n=U),"undefined"!=typeof i&&"undefined"!=typeof i.exports&&(i.exports=U),"function"==typeof t&&t(function(){return U}),"undefined"!=typeof window&&(window.vis=U)},{hammerjs:2,moment:3,mousetrap:4}],2:[function(t,e){!function(t,i){"use strict";function n(){if(!s.READY){s.event.determineEventTypes();for(var t in s.gestures)s.gestures.hasOwnProperty(t)&&s.detection.register(s.gestures[t]);s.event.onTouch(s.DOCUMENT,s.EVENT_MOVE,s.detection.detect),s.event.onTouch(s.DOCUMENT,s.EVENT_END,s.detection.detect),s.READY=!0}}var s=function(t,e){return new s.Instance(t,e||{})};s.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},s.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,s.HAS_TOUCHEVENTS="ontouchstart"in t,s.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,s.NO_MOUSEEVENTS=s.HAS_TOUCHEVENTS&&navigator.userAgent.match(s.MOBILE_REGEX),s.EVENT_TYPES={},s.DIRECTION_DOWN="down",s.DIRECTION_LEFT="left",s.DIRECTION_UP="up",s.DIRECTION_RIGHT="right",s.POINTER_MOUSE="mouse",s.POINTER_TOUCH="touch",s.POINTER_PEN="pen",s.EVENT_START="start",s.EVENT_MOVE="move",s.EVENT_END="end",s.DOCUMENT=document,s.plugins={},s.READY=!1,s.Instance=function(t,e){var i=this; +return n(),this.element=t,this.enabled=!0,this.options=s.utils.extend(s.utils.extend({},s.defaults),e||{}),this.options.stop_browser_behavior&&s.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),s.event.onTouch(t,s.EVENT_START,function(t){i.enabled&&s.detection.startDetect(i,t)}),this},s.Instance.prototype={on:function(t,e){for(var i=t.split(" "),n=0;n0&&e==s.EVENT_END?e=s.EVENT_MOVE:c||(e=s.EVENT_END),c||null===o?o=h:h=o,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(c=s.PointerEvent.updatePointer(e,h))),c||(o=null,r=!1,a=!1,s.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=s.HAS_POINTEREVENTS?s.PointerEvent.getEvents():s.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],s.EVENT_TYPES[s.EVENT_START]=t[0],s.EVENT_TYPES[s.EVENT_MOVE]=t[1],s.EVENT_TYPES[s.EVENT_END]=t[2]},getTouchList:function(t){return s.HAS_POINTEREVENTS?s.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var n=this.getTouchList(i,e),o=s.POINTER_TOUCH;return(i.type.match(/mouse/)||s.PointerEvent.matchType(s.POINTER_MOUSE,i))&&(o=s.POINTER_MOUSE),{center:s.utils.getCenter(n),timeStamp:(new Date).getTime(),target:i.target,touches:n,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return s.detection.stopDetect()}}}},s.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==s.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[s.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==s.POINTER_MOUSE,i[s.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==s.POINTER_TOUCH,i[s.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==s.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},s.utils={extend:function(t,e,n){for(var s in e)t[s]!==i&&n||(t[s]=e[s]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],n=0,s=t.length;s>n;n++)e.push(t[n].pageX),i.push(t[n].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,n=e.pageX-t.pageX;return 180*Math.atan2(i,n)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),n=Math.abs(t.pageY-e.pageY);return i>=n?t.pageX-e.pageX>0?s.DIRECTION_LEFT:s.DIRECTION_RIGHT:t.pageY-e.pageY>0?s.DIRECTION_UP:s.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,n=e.pageY-t.pageY;return Math.sqrt(i*i+n*n)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==s.DIRECTION_UP||t==s.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,n=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var s=0;si;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==s.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=s.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,n=t.touches.length;n>i;i++)e.touches.push(s.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=s.utils.getVelocity(o,r,a);return s.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:s.utils.getDistance(e.center,t.center),angle:s.utils.getAngle(e.center,t.center),direction:s.utils.getDirection(e.center,t.center),scale:s.utils.getScale(e.touches,t.touches),rotation:s.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),s.utils.extend(s.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},s.gestures=s.gestures||{},s.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case s.EVENT_START:clearTimeout(this.timer),s.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==s.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case s.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case s.EVENT_END:clearTimeout(this.timer)}}},s.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==s.EVENT_END){var i=s.detection.previous,n=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},s.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),void(this.triggered=!1);if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==s.POINTER_MOUSE?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==s.EVENT_START&&e.trigger(this.name,t)))}},s.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==s.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=s:(t.Hammer=s,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return s}))}(this)},{}],3:[function(e,i){(function(n){function s(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function o(t,e){return function(i){return p(t.call(this,i),e)}}function r(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function a(){}function h(t){x(t),c(this,t)}function d(t){var e=_(t),i=e.year||0,n=e.month||0,s=e.week||0,o=e.day||0,r=e.hour||0,a=e.minute||0,h=e.second||0,d=e.millisecond||0;this._milliseconds=+d+1e3*h+6e4*a+36e5*r,this._days=+o+7*s,this._months=+n+12*i,this._data={},this._bubble()}function c(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function l(t){var e,i={};for(e in t)t.hasOwnProperty(e)&&_e.hasOwnProperty(e)&&(i[e]=t[e]);return i}function u(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e,i){for(var n=""+Math.abs(t),s=t>=0;n.lengthn;n++)(i&&t[n]!==e[n]||!i&&b(t[n])!==b(e[n]))&&r++;return r+o}function y(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=Xe[t]||Ze[e]||e}return t}function _(t){var e,i,n={};for(i in t)t.hasOwnProperty(i)&&(e=y(i),e&&(n[e]=t[i]));return n}function w(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}re[t]=function(s,o){var r,a,h=re.fn._lang[t],d=[];if("number"==typeof s&&(o=s,s=n),a=function(t){var e=re().utc().set(i,t);return h.call(re.fn._lang,e,s||"")},null!=o)return a(o);for(r=0;e>r;r++)d.push(a(r));return d}}function b(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function S(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function E(t){return T(t)?366:365}function T(t){return t%4===0&&t%100!==0||t%400===0}function x(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[ue]<0||t._a[ue]>11?ue:t._a[pe]<1||t._a[pe]>S(t._a[le],t._a[ue])?pe:t._a[fe]<0||t._a[fe]>23?fe:t._a[ge]<0||t._a[ge]>59?ge:t._a[me]<0||t._a[me]>59?me:t._a[ve]<0||t._a[ve]>999?ve:-1,t._pf._overflowDayOfYear&&(le>e||e>pe)&&(e=pe),t._pf.overflow=e)}function C(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function M(t){return t?t.toLowerCase().replace("_","-"):t}function D(t,e){return e._isUTC?re(t).zone(e._offset||0):re(t).local()}function I(t,e){return e.abbr=t,ye[t]||(ye[t]=new a),ye[t].set(e),ye[t]}function O(t){delete ye[t]}function N(t){var i,n,s,o,r=0,a=function(t){if(!ye[t]&&we)try{e("./lang/"+t)}catch(i){}return ye[t]};if(!t)return re.fn._lang;if(!g(t)){if(n=a(t))return n;t=[t]}for(;r0;){if(n=a(o.slice(0,i).join("-")))return n;if(s&&s.length>=i&&v(o,s,!0)>=i-1)break;i--}r++}return re.fn._lang}function L(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function A(t){var e,i,n=t.match(Te);for(e=0,i=n.length;i>e;e++)n[e]=Je[n[e]]?Je[n[e]]:L(n[e]);return function(s){var o="";for(e=0;i>e;e++)o+=n[e]instanceof Function?n[e].call(s,t):n[e];return o}}function k(t,e){return t.isValid()?(e=z(e,t.lang()),Ke[e]||(Ke[e]=A(e)),Ke[e](t)):t.lang().invalidDate()}function z(t,e){function i(t){return e.longDateFormat(t)||t}var n=5;for(xe.lastIndex=0;n>=0&&xe.test(t);)t=t.replace(xe,i),xe.lastIndex=0,n-=1;return t}function P(t,e){var i,n=e._strict;switch(t){case"DDDD":return Fe;case"YYYY":case"GGGG":case"gggg":return n?Ye:De;case"Y":case"G":case"g":return He;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return n?Re:Ie;case"S":if(n)return ze;case"SS":if(n)return Pe;case"SSS":if(n)return Fe;case"DDD":return Me;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ne;case"a":case"A":return N(e._l)._meridiemParse;case"X":return ke;case"Z":case"ZZ":return Le;case"T":return Ae;case"SSSS":return Oe;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return n?Pe:Ce;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Ce;default:return i=new RegExp(V(U(t.replace("\\","")),"i"))}}function F(t){t=t||"";var e=t.match(Le)||[],i=e[e.length-1]||[],n=(i+"").match(Ge)||["-",0,0],s=+(60*n[1])+b(n[2]);return"+"===n[0]?-s:s}function Y(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[ue]=b(e)-1);break;case"MMM":case"MMMM":n=N(i._l).monthsParse(e),null!=n?s[ue]=n:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(s[pe]=b(e));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=b(e));break;case"YY":s[le]=b(e)+(b(e)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":s[le]=b(e);break;case"a":case"A":i._isPm=N(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[fe]=b(e);break;case"m":case"mm":s[ge]=b(e);break;case"s":case"ss":s[me]=b(e);break;case"S":case"SS":case"SSS":case"SSSS":s[ve]=b(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=F(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function R(t){var e,i,n,s,o,r,a,h,d,c,l=[];if(!t._d){for(n=W(t),t._w&&null==t._a[pe]&&null==t._a[ue]&&(o=function(e){var i=parseInt(e,10);return e?e.length<3?i>68?1900+i:2e3+i:i:null==t._a[le]?re().weekYear():t._a[le]},r=t._w,null!=r.GG||null!=r.W||null!=r.E?a=te(o(r.GG),r.W||1,r.E,4,1):(h=N(t._l),d=null!=r.d?K(r.d,h):null!=r.e?parseInt(r.e,10)+h._week.dow:0,c=parseInt(r.w,10)||1,null!=r.d&&dE(s)&&(t._pf._overflowDayOfYear=!0),i=Z(s,0,t._dayOfYear),t._a[ue]=i.getUTCMonth(),t._a[pe]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=l[e]=n[e];for(;7>e;e++)t._a[e]=l[e]=null==t._a[e]?2===e?1:0:t._a[e];l[fe]+=b((t._tzm||0)/60),l[ge]+=b((t._tzm||0)%60),t._d=(t._useUTC?Z:X).apply(null,l)}}function H(t){var e;t._d||(e=_(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],R(t))}function W(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function j(t){t._a=[],t._pf.empty=!0;var e,i,n,s,o,r=N(t._l),a=""+t._i,h=a.length,d=0;for(n=z(t._f,r).match(Te)||[],e=0;e0&&t._pf.unusedInput.push(o),a=a.slice(a.indexOf(i)+i.length),d+=i.length),Je[s]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(s),Y(s,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(s);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[fe]<12&&(t._a[fe]+=12),t._isPm===!1&&12===t._a[fe]&&(t._a[fe]=0),R(t),x(t)}function U(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,s){return e||i||n||s})}function V(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function G(t){var e,i,n,o,r;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(o=0;or)&&(n=r,i=e));c(t,i||e)}function B(t){var e,i,n=t._i,s=We.exec(n);if(s){for(t._pf.iso=!0,e=0,i=Ue.length;i>e;e++)if(Ue[e][1].exec(n)){t._f=Ue[e][0]+(s[6]||" ");break}for(e=0,i=Ve.length;i>e;e++)if(Ve[e][1].exec(n)){t._f+=Ve[e][0];break}n.match(Le)&&(t._f+="Z"),j(t)}else t._d=new Date(n)}function q(t){var e=t._i,i=be.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?B(t):g(e)?(t._a=e.slice(0),R(t)):m(e)?t._d=new Date(+e):"object"==typeof e?H(t):t._d=new Date(e)}function X(t,e,i,n,s,o,r){var a=new Date(t,e,i,n,s,o,r);return 1970>t&&a.setFullYear(t),a}function Z(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function K(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function $(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function Q(t,e,i){var n=ce(Math.abs(t)/1e3),s=ce(n/60),o=ce(s/60),r=ce(o/24),a=ce(r/365),h=45>n&&["s",n]||1===s&&["m"]||45>s&&["mm",s]||1===o&&["h"]||22>o&&["hh",o]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",ce(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,$.apply({},h)}function J(t,e,i){var n,s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),n=re(t).add("d",o),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function te(t,e,i,n,s){var o,r,a=Z(t,0,1).getUTCDay();return i=null!=i?i:s,o=s-a+(a>n?7:0)-(s>a?7:0),r=7*(e-1)+(i-s)+o+1,{year:r>0?t:t-1,dayOfYear:r>0?r:E(t-1)+r}}function ee(t){var e=t._i,i=t._f;return null===e?re.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=N().preparse(e)),re.isMoment(e)?(t=l(e),t._d=new Date(+e._d)):i?g(i)?G(t):j(t):q(t),new h(t))}function ie(t,e){re.fn[t]=re.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),re.updateOffset(this),this):this._d["get"+i+e]()}}function ne(t){re.duration.fn[t]=function(){return this._data[t]}}function se(t,e){re.duration.fn["as"+t]=function(){return+this/e}}function oe(t){var e=!1,i=re;"undefined"==typeof ender&&(t?(de.moment=function(){return!e&&console&&console.warn&&(e=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),i.apply(null,arguments)},c(de.moment,i)):de.moment=re)}for(var re,ae,he="2.5.1",de=this,ce=Math.round,le=0,ue=1,pe=2,fe=3,ge=4,me=5,ve=6,ye={},_e={_isAMomentObject:null,_i:null,_f:null,_l:null,_strict:null,_isUTC:null,_offset:null,_pf:null,_lang:null},we="undefined"!=typeof i&&i.exports&&"undefined"!=typeof e,be=/^\/?Date\((\-?\d+)/i,Se=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Ee=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Te=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,xe=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Ce=/\d\d?/,Me=/\d{1,3}/,De=/\d{1,4}/,Ie=/[+\-]?\d{1,6}/,Oe=/\d+/,Ne=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Le=/Z|[\+\-]\d\d:?\d\d/gi,Ae=/T/i,ke=/[\+\-]?\d+(\.\d{1,3})?/,ze=/\d/,Pe=/\d\d/,Fe=/\d{3}/,Ye=/\d{4}/,Re=/[+-]?\d{6}/,He=/[+-]?\d+/,We=/^\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)?)?$/,je="YYYY-MM-DDTHH:mm:ssZ",Ue=[["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}/]],Ve=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Ge=/([\+\-]|\d\d)/gi,Be="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),qe={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Xe={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Ze={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Ke={},$e="DDD w W M D d".split(" "),Qe="M D H h m s w W".split(" "),Je={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},YYYYYY:function(){var t=this.year(),e=t>=0?"+":"-";return e+p(Math.abs(t),6)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return p(this.weekYear(),4)},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return p(this.isoWeekYear(),4)},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},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 b(this.milliseconds()/100)},SS:function(){return p(b(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},SSSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(b(t/60),2)+":"+p(b(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(b(t/60),2)+p(b(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ti=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];$e.length;)ae=$e.pop(),Je[ae+"o"]=r(Je[ae],ae);for(;Qe.length;)ae=Qe.pop(),Je[ae+ae]=o(Je[ae],2);for(Je.DDDD=o(Je.DDD,3),c(a.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=re.utc([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,n;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=re([2e3,1]).day(e),n="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(n.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var s=this._relativeTime[i];return"function"==typeof s?s(t,e,i,n):s.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return J(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),re=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=n),r={},r._isAMomentObject=!0,r._i=t,r._f=e,r._l=i,r._strict=o,r._isUTC=!1,r._pf=s(),ee(r)},re.utc=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=n),r={},r._isAMomentObject=!0,r._useUTC=!0,r._isUTC=!0,r._l=i,r._i=t,r._f=e,r._strict=o,r._pf=s(),ee(r).utc()},re.unix=function(t){return re(1e3*t)},re.duration=function(t,e){var i,n,s,o=t,r=null;return re.isDuration(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(r=Se.exec(t))?(i="-"===r[1]?-1:1,o={y:0,d:b(r[pe])*i,h:b(r[fe])*i,m:b(r[ge])*i,s:b(r[me])*i,ms:b(r[ve])*i}):(r=Ee.exec(t))&&(i="-"===r[1]?-1:1,s=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},o={y:s(r[2]),M:s(r[3]),d:s(r[4]),h:s(r[5]),m:s(r[6]),s:s(r[7]),w:s(r[8])}),n=new d(o),re.isDuration(t)&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},re.version=he,re.defaultFormat=je,re.updateOffset=function(){},re.lang=function(t,e){var i;return t?(e?I(M(t),e):null===e?(O(t),t="en"):ye[t]||N(t),i=re.duration.fn._lang=re.fn._lang=N(t),i._abbr):re.fn._lang._abbr},re.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),N(t)},re.isMoment=function(t){return t instanceof h||null!=t&&t.hasOwnProperty("_isAMomentObject")},re.isDuration=function(t){return t instanceof d},ae=ti.length-1;ae>=0;--ae)w(ti[ae]);for(re.normalizeUnits=function(t){return y(t)},re.invalid=function(t){var e=re.utc(0/0);return null!=t?c(e._pf,t):e._pf.userInvalidated=!0,e},re.parseZone=function(t){return re(t).parseZone()},c(re.fn=h.prototype,{clone:function(){return re(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=re(this).utc();return 00:!1},parsingFlags:function(){return c({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=k(this,t||re.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),f(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),f(this,i,-1),this},diff:function(t,e,i){var n,s,o=D(t,this),r=6e4*(this.zone()-o.zone());return e=y(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),s=12*(this.year()-o.year())+(this.month()-o.month()),s+=(this-re(this).startOf("month")-(o-re(o).startOf("month")))/n,s-=6e4*(this.zone()-re(this).startOf("month").zone()-(o.zone()-re(o).startOf("month").zone()))/n,"year"===e&&(s/=12)):(n=this-o,s="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?(n-r)/864e5:"week"===e?(n-r)/6048e5:n),i?s:u(s)},from:function(t,e){return re.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(re(),t)},calendar:function(){var t=D(re(),this).startOf("day"),e=this.diff(t,"days",!0),i=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(this.lang().calendar(i,this))},isLeapYear:function(){return T(this.year())},isDST:function(){return this.zone()+re(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+re(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+D(t,this).startOf(e)},min:function(t){return t=re.apply(null,arguments),this>t?this:t},max:function(t){return t=re.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=F(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&f(this,re.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?re(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return S(this.year(),this.month())},dayOfYear:function(t){var e=ce((re(this).startOf("day")-re(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(t){var e=J(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=J(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=J(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=y(t),this[t]()},set:function(t,e){return t=y(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===n?this._lang:(this._lang=N(t),this)}}),ae=0;ae-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function o(t,e){return t.sort().join(",")===e.sort().join(",")}function r(t){t=t||{};var e,i=!1;for(e in M)t[e]?i=!0:M[e]=0;i||(I=!1)}function a(t,e,i,n,s){var r,a,h=[];if(!x[t])return[];for("keyup"==i&&u(t)&&(e=[t]),r=0;r95&&112>t||b.hasOwnProperty(t)&&(_[b[t]]=t)}return _}function g(t,e,i){return i||(i=f()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function m(t,e,i,s){M[t]=0,s||(s=g(e[0],[]));var o,a=function(){I=s,++M[t],p()},h=function(t){d(i,t),"keyup"!==s&&(D=n(t)),setTimeout(r,10)};for(o=0;o1)return m(t,d,e,i);for(h="+"===t?["+"]:t.split("+"),o=0;o":".","?":"/","|":"\\"},T={option:"alt",command:"meta","return":"enter",escape:"esc"},x={},C={},M={},D=!1,I=!1,O=1;20>O;++O)b[111+O]="f"+O;for(O=0;9>=O;++O)b[O+96]=O;i(document,"keypress",l),i(document,"keydown",l),i(document,"keyup",l);var N={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),C[t+":"+i]=e,this},unbind:function(t,e){return C[t+":"+e]&&(delete C[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return C[t+":"+e](),this},reset:function(){return x={},C={},this}};e.exports=N},{}]},{},[1])(1)}); \ No newline at end of file diff --git a/docs/graph.html b/docs/graph.html index 2c679bc1..8dfe2594 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -15,25 +15,6 @@

Graph documentation

-

Contents

- -

Overview

Graph is a visualization to display graphs and networks consisting of nodes @@ -51,6 +32,37 @@ vis.js library.

+ +

Contents

+ + +

Example

Here a basic graph example. Note that unlike the @@ -147,7 +159,7 @@ The constructor accepts three parameters: -

Data Format

+

Data format

The data parameter of the Graph constructor is an object which can contain different types of data. @@ -605,7 +617,7 @@ var graph = new vis.Graph(container, data); -

Configuration Options

+

Configuration options

Options can be used to customize the graph. Options are defined as a JSON object. @@ -628,239 +640,353 @@ var options = {

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
edges.colorString"#2B7CE9"The default color of a edge.
edges.dashObjectObject - Object containing default properties for dashed lines. - Available properties: length, gap, - altLength. -
edges.dash.altLengthnumbernoneDefault length of the alternated dash in pixels on a dashed line. - Specifying dash.altLength allows for creating - a dashed line with a dash-dot style, for example when - dash.length=10 and dash.altLength=5. - See also the option dahs.length. - Only applicable when the line style is dash-line.
edges.dash.lengthnumber10Default length of a dash in pixels on a dashed line. - Only applicable when the line style is dash-line.
edges.dash.gapnumber5Default length of a gap in pixels on a dashed line. - Only applicable when the line style is dash-line.
edges.lengthNumber100The default length of a edge.
edges.styleString"line"The default style of a edge. - Choose from line (default), arrow, - arrow-center, dash-line.
edges.widthNumber1The default width of a edge.
groupsObjectnoneIt is possible to specify custom styles for groups. - Each node assigned a group gets the specified style. - See Groups for an overview of the available styles - and an example. -
heightString"400px"The height of the graph in pixels or as a percentage.
nodes.colorString | ObjectObjectDefault color of the nodes. When color is a string, the color is applied - to both background as well as the border of the node.
nodes.color.backgroundString"#97C2FC"Default background color of the nodes
nodes.color.borderString"#2B7CE9"Default border color of the nodes
nodes.color.highlightString | ObjectObjectDefault color of the node when the node is selected. Applied to - both border and background of the node.
nodes.color.highlight.backgroundString"#D2E5FF"Default background color of the node when selected.
nodes.color.highlight.borderString"#2B7CE9"Default border color of the node when selected.
nodes.fontColorString"black"Default font color for the text label in the nodes.
nodes.fontFaceString"sans"Default font face for the text label in the nodes, for example "verdana" or "arial".
nodes.fontSizeNumber14Default font size in pixels for the text label in the nodes.
nodes.groupStringnoneDefault group for the nodes.
nodes.imageStringnoneDefault image url for the nodes. only applicable with shape image.
nodes.widthMinNumber16The minimum width for a scaled image. Only applicable with shape image.
nodes.widthMaxNumber64The maximum width for a scaled image. Only applicable with shape image.
nodes.shapeString"ellipse"The default shape for all nodes. - Choose from - ellipse (default), circle, box, - database, image, label, dot, - star, triangle, triangleDown, and square. - This shape can be overridden by a group shape, or by a shape of an individual node.
nodes.radiusNumber5The default radius for a node. Only applicable with shape dot.
nodes.radiusMinNumber5The minimum radius for a scaled node. Only applicable with shape dot.
nodes.radiusMaxNumber20The maximum radius for a scaled node. Only applicable with shape dot.
selectableBooleantrueIf true, nodes in the graph can be selected by clicking them. - Long press can be used to select multiple nodes.
stabilizeBooleantrueIf true, the graph is stabilized before displaying it. If false, - the nodes move to a stabe position visibly in an animated way.
widthString"400px"The width of the graph in pixels or as a percentage.
NameTypeDefaultDescription
clusteringObjectnone + Clustering configuration. Clustering is turned off by default. See section Clustering for an overview of the available options. +
edgesObjectnone + Configuration options applied to all edges. See section Edges configuration for an overview of the available options. +
groupsObjectnoneIt is possible to specify custom styles for groups. + Each node assigned a group gets the specified style. + See Groups configuration for an overview of the available styles + and an example. +
heightString"400px"The height of the graph in pixels or as a percentage.
keyboardObjectnone + Configuration options for shortcuts keys. Sortcut keys are turned off by default. See section Keyboard navigation for an overview of the available options. +
navigationObjectnone + Configuration options for the navigation controls. See section Navigation controls for an overview of the available options. +
nodesObjectnone + Configuration options applied to all nodes. See section Nodes configuration for an overview of the available options. +
selectableBooleantrueIf true, nodes in the graph can be selected by clicking them. + Long press can be used to select multiple nodes.
stabilizeBooleantrueIf true, the graph is stabilized before displaying it. If false, + the nodes move to a stabe position visibly in an animated way.
widthString"400px"The width of the graph in pixels or as a percentage.

-

Groups

+

Nodes configuration

+

+ Nodes can be configured with different styles and shapes. To configure nodes, provide an object named nodes in the options for the Graph. +

+ +

+ For example to give the nodes a custom color, shape, and size: +

+
+var options = {
+  // ...
+  nodes: {
+    color: {
+      background: 'white',
+      border: 'red',
+      highlight: {
+        background: 'pink',
+        border: 'red'
+      }
+    },
+    shape: 'star',
+    radius: 24
+  }
+};
+
+ +

+ The following options are available for nodes. These options must be created + inside an object nodes in the graphs options object.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
colorString | ObjectObjectDefault color of the nodes. When color is a string, the color is applied + to both background as well as the border of the node.
color.backgroundString"#97C2FC"Default background color of the nodes
color.borderString"#2B7CE9"Default border color of the nodes
color.highlightString | ObjectObjectDefault color of the node when the node is selected. In case of a string, the color is applied to + both border and background of the node.
color.highlight.backgroundString"#D2E5FF"Default background color of the node when selected.
color.highlight.borderString"#2B7CE9"Default border color of the node when selected.
fontColorString"black"Default font color for the text label in the nodes.
fontFaceString"sans"Default font face for the text label in the nodes, for example "verdana" or "arial".
fontSizeNumber14Default font size in pixels for the text label in the nodes.
groupStringnoneDefault group for the nodes.
imageStringnoneDefault image url for the nodes. only applicable to shape image.
widthMinNumber16The minimum width for a scaled image. Only applicable to shape image.
widthMaxNumber64The maximum width for a scaled image. Only applicable to shape image.
shapeString"ellipse"The default shape for all nodes. + Choose from + ellipse (default), circle, box, + database, image, label, dot, + star, triangle, triangleDown, and square. + This shape can be overridden by a group shape, or by a shape of an individual node.
radiusNumber5The default radius for a node. Only applicable to shapes dot, + star, triangle, triangleDown, and square.
radiusMinNumber5The minimum radius for a scaled node. Only applicable to shapes dot, + star, triangle, triangleDown, and square.
radiusMaxNumber20The maximum radius for a scaled node. Only applicable to shapes dot, + star, triangle, triangleDown, and square.
+ + +

Edges configuration

+ +

+ Edges can be configured with different length and styling. To configure edges, provide an object named edges in the options for the Graph. +

+ +

+ For example to set the width of all edges to 2 pixels and give them a red color: +

+
+var options = {
+  // ...
+  edges: {
+    color: 'red',
+    width: 2
+  }
+};
+
+ +

+ The following options are available for edges. These options must be created + inside an object edges in the graphs options object. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
colorString"#2B7CE9"The default color of a edge.
dashObjectObject + Object containing default properties for dashed lines. + Available properties: length, gap, + altLength. +
dash.altLengthnumbernoneDefault length of the alternated dash in pixels on a dashed line. + Specifying dash.altLength allows for creating + a dashed line with a dash-dot style, for example when + dash.length=10 and dash.altLength=5. + See also the option dahs.length. + Only applicable when the line style is dash-line.
dash.lengthnumber10Default length of a dash in pixels on a dashed line. + Only applicable when the line style is dash-line.
dash.gapnumber5Default length of a gap in pixels on a dashed line. + Only applicable when the line style is dash-line.
lengthNumber100The default length of a edge.
styleString"line"The default style of a edge. + Choose from line (default), arrow, + arrow-center, dash-line.
widthNumber1The default width of a edge.
+ +

Groups configuration

It is possible to specify custom styles for groups of nodes. Each node having assigned to this group gets the specified style. @@ -932,9 +1058,10 @@ var nodes = [ color.highlight - String + String | Object "#D2E5FF" - Color of the node when selected. + Default color of the node when the node is selected. In case of a string, the color is applied to + both border and background of the node. color.highlight.background @@ -995,35 +1122,267 @@ var nodes = [ +

Clustering

+

+ The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without + sacrificing performance. When loading a large dataset, the nodes are clustered initially (this may take a small while) to have a + responsive visualization to work with. The clustering is both outside-in and inside-out. Outside-in means that nodes with only one + connection will be contained, or clustered, in the node it is connected to. Inside-out clustering first determines which nodes are hubs. + Hubs are defined as the nodes with the top 3% highest amount of connections (assuming normal distribution). These hubs then "grow", meaning + they contain the nodes they are connected to within themselves. The edges that were connected to the nodes that are absorbed will be reconnected to the cluster. +
+
+ A cluster is just a node that has references to the nodes and edges it contains. It has an internal counter to keep track of its size, which is then used + to calculate the required forces. The contained nodes are removed from the global nodes index, greatly speeding up the system. +
+
+ The clustering has the following user-configurable settings. The default values have been tested with the Graph examples and work well. + The default state for clustering is off. +

-

Methods

+
+// These variables must be defined in an options object named clustering.
+// If a variable is not supplied, the default value is used.
+var options = {
+    clustering: {
+      initialMaxNodes: 100,
+      clusterThreshold:500,
+      reduceToNodes:300,
+      chainThreshold: 0.4,
+      clusterEdgeThreshold: 20,
+      sectorThreshold: 50,
+      screenSizeThreshold: 0.2,
+      fontSizeMultiplier:  4.0,
+      forceAmplification:  0.6,
+      distanceAmplification: 0.2,
+      edgeGrowth: 11,
+      nodeScaling: {width:  10,
+                    height: 10,
+                    radius: 10},
+      activeAreaBoxSize: 100
+    }
+}
+// OR to just load the module with default values:
+var options: {
+    clustering: true
+}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
initialMaxNodesNumber100If the initial amount of nodes is larger than this value, clustering starts until the total number of nodes is less than this value.
clusterThresholdNumber500While zooming in and out, clusters can open up. Once there are more than absoluteMaxNumberOfNodes nodes, + clustering starts until reduceToMaxNumberOfNodes nodes are left. This is done to ensure performance is continuously fluid.
reduceToNodesNumber300While zooming in and out, clusters can open up. Once there are more than absoluteMaxNumberOfNodes nodes, + clustering starts until reduceToMaxNumberOfNodes nodes are left. This is done to ensure performance is continiously fluid.
chainThresholdNumber0.4Because of the clustering methods used, long chains of nodes can be formed. To reduce these chains, this threshold is used. + A chainThreshold of 0.4 means that no more than 40% of all nodes are allowed to be a chain node (two connections). + If there are more, they are clustered together.
clusterEdgeThresholdNumber20This is the absolute edge length threshold in pixels. If the edge is smaller on screen (that means zooming out reduces this length) + the node will be clustered. This is triggered when zooming out.
sectorThresholdInteger50If a cluster larger than sectorThreshold is opened, a seperate instance called a sector, will be created. All the simulation of + nodes outside of this instance will be paused. This is to maintain performance and clarity when examining large clusters. + A sector is collapsed when zooming out far enough. Also, when opening a cluster, if this cluster is smaller than this value, it is fully unpacked.
screenSizeThresholdNumber0.2When zooming in, the clusters become bigger. A screenSizeThreshold of 0.2 means that if the width or height of this cluster + becomes bigger than 20% of the width or height of the canvas, the cluster is opened. If a sector has been created, if the sector is smaller than + 20%, we collapse this sector.
fontSizeMultiplierNumber4.0This parameter denotes the increase in fontSize of the cluster when a single node is added to it.
forceAmplificationNumber0.6This factor is used to calculate the increase of the repulsive force of a cluster. It is calculated by the following + formula: repulsingForce *= 1 + (clusterSize * forceAmplification).
distanceAmplificationNumber0.2This factor is used to calculate the increase in effective range of the repulsive force of the cluster. + A larger cluster has a longer range. It is calculated by the following + formula: minDistance *= 1 + (clusterSize * distanceAmplification).
edgeGrowthNumber11This factor determines the elongation of edges connected to a cluster.
nodeScaling.widthNumber10This factor determines how much the width of a cluster increases in pixels per added node.
nodeScaling.heightNumber10This factor determines how much the height of a cluster increases in pixels per added node.
nodeScaling.radiusNumber10This factor determines how much the radius of a cluster increases in pixels per added node.
activeAreaBoxSizeNumber100Imagine a square with an edge length of activeAreaBoxSize pixels around your cursor. + If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector. + This is regardless of the zoom level.
+ +

- Graph supports the following methods. + Graph has a menu with navigation controls, which is disabled by default. + It can be configured with the following settings.

+
+// simple use of navigation controls
+var options: {
+  navigation: true
+}
+
+// advanced use of navigation controls
+var options: {
+  navigation: {
+    iconPath: '/path/to/navigation/icons/'
+  }
+}
+
+ - - + + + - - - + + + + + +
MethodReturn TypeNameTypeDefault Description
setData(data)noneLoads data. Parameter data is an object containing - nodes, edges, and options. Parameters nodes, edges are an Array. - Options is a name-value map and is optional. - iconPathstring"/img"The path to the icon images can be defined here. If custom icons are used, they have to have the same filename as the ones originally packaged with vis.js.
+ +

Keyboard navigation

+

+ The graph can be navigated using shortcut keys. + It can be configured with the following user-configurable settings. + The default state for the keyboard navigation is off. The predefined keys can be found in the example 20_navigation.html. +

+ +
+// simple use of keyboard controls
+var options: {
+    keyboard: true
+}
+
+// advanced configuration for keyboard controls
+var options: {
+  keyboard: {
+    speed: {
+      x: 10,
+      y: 10,
+      zoom: 0.02
+    }
+  }
+}
+
+ + + + + + + - - - + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
setOptions(options)noneSet options for the graph. The available options are described in - the section Configuration Options. + speed.xNumber10This defines the speed of the camera movement in the x direction when using the keyboard navigation.
speed.yNumber10This defines the speed of the camera movement in the y direction when using the keyboard navigation.
speed.zoomNumber0.02This defines the zoomspeed when using the keyboard navigation.
+ +

Methods

+

+ Graph supports the following methods. +

+ + + + + + + @@ -1034,12 +1393,43 @@ var nodes = [ + + + + + + + + + + + + + + + + + + + + + + + + @@ -1063,7 +1453,7 @@ var nodes = [

Events

- Graph fires events after one or multiple nodes are selected. + Graph fires events after one or multiple nodes are selected or deselected. The event can be catched by creating a listener.

@@ -1072,22 +1462,35 @@ var nodes = [

-function onSelect() {
-  alert('selected nodes: ' + graph.getSelection());
+graph.on('select', function (properties) {
+  alert('selected nodes: ' + properties.nodes);
+});
+
+ +

+ A listener can be removed via the function off: +

+ +
+function onSelect (properties) {
+  alert('selected nodes: ' + properties.nodes);
 }
 
-vis.events.addListener(graph, 'select', onSelect);
+// add event listener
+graph.on('select', onSelect);
+
+// do stuff...
+
+// remove event listener
+graph.off('select', onSelect);
 
+

The following events are available.

MethodReturn TypeDescription
getSelection()
on(event, callback)noneCreate an event listener. The callback function is invoked every time the event is triggered. Avialable events: select. The callback function is invoked as callback(properties), where properties is an object containing event specific properties. See section Events for more information.
off(event, callback)noneRemove an event listener created before via function on(event, callback). See section Events for more information.
redraw() none Redraw the graph. Useful when the layout of the webpage changed.
setData(data,[disableStart])noneLoads data. Parameter data is an object containing + nodes, edges, and options. Parameters nodes, edges are an Array. + Options is a name-value map and is optional. Parameter disableStart is + an optional Boolean and can disable the start of the simulation that would begin at the end + of this function by default. +
setOptions(options)noneSet options for the graph. The available options are described in + the section Configuration Options. +
setSelection(selection) none
- - - - @@ -1096,18 +1499,19 @@ vis.events.addListener(graph, 'select', onSelect); - + -
name Description
selectFired after the user selects or unselects a node by clicking it, - or when selecting a number of nodes by dragging a selection area - around them. Not fired when the method setSelection - is executed. The ids of the selected nodes can be retrieved via the - method getSelection. + Fired after the user selects or deselects a node by clicking it. + Not fired when the method setSelectionis executed. + +
    +
  • nodes: an array with the ids of the selected nodes
  • +
none
-

Data Policy

+

Data policy

All code and data is processed and rendered in the browser. No data is sent to any server. diff --git a/docs/timeline.html b/docs/timeline.html index c2335c60..eaaf2339 100644 --- a/docs/timeline.html +++ b/docs/timeline.html @@ -14,6 +14,16 @@

Timeline documentation

+

Overview

+

+ The Timeline is an interactive visualization chart to visualize data in time. + The data items can take place on a single date, or have a start and end date (a range). + You can freely move and zoom in the timeline by dragging and scrolling in the + Timeline. Items can be created, edited, and deleted in the timeline. + The time scale on the axis is adjusted automatically, and supports scales ranging + from milliseconds to years. +

+

Contents

-

Overview

-

- The Timeline is an interactive visualization chart to visualize data in time. - The data items can take place on a single date, or have a start and end date (a range). - You can freely move and zoom in the timeline by dragging and scrolling in the - Timeline. Items can be created, edited, and deleted in the timeline. - The time scale on the axis is adjusted automatically, and supports scales ranging - from milliseconds to years. -

-

Example

The following code shows how to create a Timeline and provide it with data. @@ -342,7 +343,7 @@ var options = { end - Date + Date | Number | String none The initial end date for the axis of the timeline. If not provided, the latest date present in the items set is taken as @@ -387,7 +388,7 @@ var options = { max - Date + Date | Number | String none Set a maximum Date for the visible range. It will not be possible to move beyond this maximum. @@ -404,7 +405,7 @@ var options = { min - Date + Date | Number | String none Set a minimum Date for the visible range. It will not be possible to move beyond this minimum. @@ -482,7 +483,7 @@ var options = { start - Date + Date | Number | String none The initial start date for the axis of the timeline. If not provided, the earliest date present in the events is taken as start date. @@ -544,12 +545,32 @@ var options = { Retrieve the custom time. Only applicable when the option showCustomTime is true. + setCustomTime(time) none Adjust the custom time bar. Only applicable when the option showCustomTime is true. time is a Date object. + + + getSelection() + ids + Get an array with the ids of the currently selected items. + + + + on(event, callback) + none + Create an event listener. The callback function is invoked every time the event is triggered. Avialable events: rangechange, rangechanged, select. The callback function is invoked as callback(properties), where properties is an object containing event specific properties. See section Events for more information. + + + + off(event, callback) + none + Remove an event listener created before via function on(event, callback). See section Events for more information. + + setGroups(groups) none @@ -560,6 +581,7 @@ var options = { must correspond with the id of the group. + setItems(items) none @@ -572,12 +594,107 @@ var options = { setOptions(options) none - Set or update options. It is possible to change any option - of the timeline at any time. You can for example switch orientation - on the fly. + Set or update options. It is possible to change any option of the timeline at any time. You can for example switch orientation on the fly. + + + + + setSelection([ids]) + none + Select or deselect items. Currently selected items will be unselected. + + + + + + +

Events

+

+ Timeline fires events when changing the visible window by dragging, or when + selecting items. +

+ +

+ Here an example on how to listen for a select event. +

+ +
+timeline.on('select', function (properties) {
+  alert('selected items: ' + properties.nodes);
+});
+
+ +

+ A listener can be removed via the function off: +

+ +
+function onSelect (properties) {
+  alert('selected items: ' + properties.nodes);
+}
+
+// add event listener
+timeline.on('select', onSelect);
+
+// do stuff...
+
+// remove event listener
+timeline.off('select', onSelect);
+
+ + +

+ The following events are available. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nameDescriptionProperties
rangechangeFired repeatedly when the user is dragging the timeline window. + +
    +
  • start (Number): timestamp of the current start of the window.
  • +
  • end (Number): timestamp of the current end of the window.
  • +
+
rangechangedFired once after the user has dragging the timeline window. + +
    +
  • start (Number): timestamp of the current start of the window.
  • +
  • end (Number): timestamp of the current end of the window.
  • +
selectFired after the user selects or deselects items by tapping or holding them. + Not fired when the method setSelectionis executed. + +
    +
  • items: an array with the ids of the selected items
  • +
+
diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html index 24e5926a..f5a05364 100755 --- a/examples/graph/02_random_nodes.html +++ b/examples/graph/02_random_nodes.html @@ -74,6 +74,7 @@ nodes: nodes, edges: edges }; + /* var options = { nodes: { shape: 'circle' @@ -83,12 +84,18 @@ }, stabilize: false }; + */ + var options = { + edges: { + length: 50 + }, + stabilize: false + }; graph = new vis.Graph(container, data, options); // add event listeners - vis.events.addListener(graph, 'select', function(params) { - document.getElementById('selection').innerHTML = - 'Selection: ' + graph.getSelection(); + graph.on('select', function(params) { + document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes; }); } diff --git a/examples/graph/07_selections.html b/examples/graph/07_selections.html index 25faf4c9..98e44ade 100644 --- a/examples/graph/07_selections.html +++ b/examples/graph/07_selections.html @@ -51,11 +51,9 @@ graph = new vis.Graph(container, data, options); // add event listener - function onSelect() { - document.getElementById('info').innerHTML += - 'selection: ' + graph.getSelection().join(', ') + '
'; - } - vis.events.addListener(graph, 'select', onSelect); + graph.on('select', function(params) { + document.getElementById('info').innerHTML += 'selection: ' + params.nodes + '
'; + }); // set initial selection (id's of some nodes) graph.setSelection([3, 4, 5]); diff --git a/examples/graph/17_network_info.html b/examples/graph/17_network_info.html index ec4379e2..0b7e033f 100644 --- a/examples/graph/17_network_info.html +++ b/examples/graph/17_network_info.html @@ -17,7 +17,7 @@ } - + + + + + + +

Clustering - Fully random graph

+
+ This example shows a fully randomly generated set of nodes and connected edges. + By clicking the checkbox you can turn clustering on and off. If you increase the number of nodes to + a value higher than 100, automatic clustering is used before the initial draw (assuming the checkbox is checked). +
+
+ Clustering is done automatically when zooming out. When zooming in over the cluster, the cluster pops open. When the cluster is very big, a special instance + will be created and the cluster contents will only be simulated in there. Double click will also open a cluster. +
+
+ Try values of 500 and 5000 with and without clustering. All thresholds can be changed to suit your dataset. +
+
+
+ + + + + +
+
+ +
+ +

+ + diff --git a/examples/graph/19_scale_free_graph_clustering.html b/examples/graph/19_scale_free_graph_clustering.html new file mode 100644 index 00000000..cced805b --- /dev/null +++ b/examples/graph/19_scale_free_graph_clustering.html @@ -0,0 +1,141 @@ + + + + Graph | Scale free graph clustering + + + + + + + + + +

Clustering - Scale-Free-Graph

+
+ This example shows therandomly generated scale-free-graph set of nodes and connected edges from example 2. + By clicking the checkbox you can turn clustering on and off. If you increase the number of nodes to + a value higher than 100, automatic clustering is used before the initial draw (assuming the checkbox is checked). +
+
+ Clustering is done automatically when zooming out. When zooming in over the cluster, the cluster pops open. When the cluster is very big, a special instance + will be created and the cluster contents will only be simulated in there. Double click will also open a cluster. +
+
+ Try values of 500 and 5000 with and without clustering. All thresholds can be changed to suit your dataset. + Experiment with the clusterEdgeThreshold, which increases the formation of clusters when zoomed out (assuming the checkbox is checked). +
+
+
+ + + + + + + +
+
+ +
+ +

+ + diff --git a/examples/graph/20_navigation.html b/examples/graph/20_navigation.html new file mode 100644 index 00000000..cc9ed615 --- /dev/null +++ b/examples/graph/20_navigation.html @@ -0,0 +1,181 @@ + + + + Graph | Navigation + + + + + + + + + +

Navigation controls and keyboad navigation

+
+ This example is the same as example 2, except for the navigation controls that has been activated. The navigation controls are described below.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Icons:
Keyboard shortcuts:
Up arrow
Down arrow
Left arrow
Right arrow
=
[
Page up
-
]
Page down
None
Description:
Move up
Move down
Move left
Move right
Zoom in
Zoom out
Zoom extends
+
+ Apart from clicking the icons, you can also navigate using the keyboard. The buttons are in table above. + Zoom Extends changes the zoom and position of the camera to encompass all visible nodes. + + +
+
+ +
+ + + +
+
+ +
+ +

+ + diff --git a/examples/graph/index.html b/examples/graph/index.html index 7cb11657..d48f7c87 100644 --- a/examples/graph/index.html +++ b/examples/graph/index.html @@ -29,6 +29,9 @@

15_dot_language_playground.html

16_dynamic_data.html

17_network_info.html

+

18_fully_random_nodes_clustering.html

+

19_scale_free_graph_clustering.html

+

20_navigation.html

graphviz_gallery.html

diff --git a/examples/timeline/02_dataset.html b/examples/timeline/02_dataset.html index e493663d..c15e9c7b 100644 --- a/examples/timeline/02_dataset.html +++ b/examples/timeline/02_dataset.html @@ -39,18 +39,18 @@ } }); items.add([ - {id: 1, content: 'item 1
start', start: now.clone().add('days', 4)}, - {id: 2, content: 'item 2', start: now.clone().add('days', -2)}, - {id: 3, content: 'item 3', start: now.clone().add('days', 2)}, - {id: 4, content: 'item 4', start: now.clone().add('days', 0), end: now.clone().add('days', 3).toDate()}, - {id: 5, content: 'item 5', start: now.clone().add('days', 9), type:'point'}, - {id: 6, content: 'item 6', start: now.clone().add('days', 11)} + {id: 1, content: 'item 1
start', start: '2014-01-23'}, + {id: 2, content: 'item 2', start: '2014-01-18'}, + {id: 3, content: 'item 3', start: '2014-01-21'}, + {id: 4, content: 'item 4', start: '2014-01-19', end: '2014-01-24'}, + {id: 5, content: 'item 5', start: '2014-01-28', type:'point'}, + {id: 6, content: 'item 6', start: '2014-01-26'} ]); var container = document.getElementById('visualization'); var options = { - start: now.clone().add('days', -3), - end: now.clone().add('days', 7), + start: '2014-01-10', + end: '2014-02-10', orientation: 'top', height: '100%', showCurrentTime: true diff --git a/examples/timeline/06_event_listeners.html b/examples/timeline/06_event_listeners.html new file mode 100644 index 00000000..8ea72e1b --- /dev/null +++ b/examples/timeline/06_event_listeners.html @@ -0,0 +1,53 @@ + + + + Timeline | Event listeners + + + + + + + +
+

+
+ + + + \ No newline at end of file diff --git a/examples/timeline/index.html b/examples/timeline/index.html index 91b28ffe..2ff3984b 100644 --- a/examples/timeline/index.html +++ b/examples/timeline/index.html @@ -17,6 +17,7 @@

03_much_data.html

04_html_data.html

05_groups.html

+

06_event_listeners.html

diff --git a/misc/how_to_publish.md b/misc/how_to_publish.md index ee998a51..3b7557da 100644 --- a/misc/how_to_publish.md +++ b/misc/how_to_publish.md @@ -49,13 +49,25 @@ This generates the vis.js library in the folder `./dist`. Verify if it installs the just released version, and verify if it works. - - Install the libarry via bower: + - Install the library via bower: bower install vis Verify if it installs the just released version, and verify if it works. +- Publish the library at cdnjs.org + - clone the cdnjs project + - pull changes: `git pull upstream` + - add the new version of the library under /ajax/libs/vis/ + - add new folder /x.y.z/ with the new library + - update the version number in package.json + - test the library by running `npm test` + - then do a pull request with as title "[author] Update vis.js to x.y.z" + (with correct version). + + + ## Update website - Copy the `dist` folder from the `master` branch to the `github-pages` branch. @@ -72,6 +84,8 @@ This generates the vis.js library in the folder `./dist`. node updateversion.js +- Commit the changes in the `gh-pages` branch. + ## Prepare next version diff --git a/package.json b/package.json index 1998f1c9..73365fad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "0.4.0-SNAPSHOT", + "version": "0.5.0-SNAPSHOT", "description": "A dynamic, browser-based visualization library.", "homepage": "http://visjs.org/", "repository": { @@ -28,8 +28,11 @@ "devDependencies": { "jake": "latest", "jake-utils": "latest", - "browserify": "latest", + "browserify": "3.22", + "wrench": "latest", "moment": "latest", - "hammerjs": "1.0.5" + "hammerjs": "1.0.5", + "mousetrap": "latest", + "node-watch": "latest" } } diff --git a/src/DataSet.js b/src/DataSet.js index 36604a58..13e0f52e 100644 --- a/src/DataSet.js +++ b/src/DataSet.js @@ -42,6 +42,7 @@ function DataSet (options) { this.data = {}; // map with data indexed by id this.fieldId = this.options.fieldId || 'id'; // name of the field containing id this.convert = {}; // field types by field name + this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function if (this.options.convert) { for (var field in this.options.convert) { @@ -275,6 +276,7 @@ DataSet.prototype.update = function (data, senderId) { */ DataSet.prototype.get = function (args) { var me = this; + var globalShowInternalIds = this.showInternalIds; // parse the arguments var id, ids, options, data; @@ -318,6 +320,13 @@ DataSet.prototype.get = function (args) { type = 'Array'; } + // we allow the setting of this value for a single get request. + if (options != undefined) { + if (options.showInternalIds != undefined) { + this.showInternalIds = options.showInternalIds; + } + } + // build options var convert = options && options.convert || this.options.convert; var filter = options && options.filter; @@ -352,6 +361,9 @@ DataSet.prototype.get = function (args) { } } + // restore the global value of showInternalIds + this.showInternalIds = globalShowInternalIds; + // order the results if (options && options.order && id == undefined) { this._sort(items, options.order); @@ -831,7 +843,7 @@ DataSet.prototype._getItem = function (id, convert) { if (raw.hasOwnProperty(field)) { value = raw[field]; // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { + if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) { converted[field] = util.convert(value, convert[field]); } } @@ -843,13 +855,12 @@ DataSet.prototype._getItem = function (id, convert) { if (raw.hasOwnProperty(field)) { value = raw[field]; // output all fields, except internal ids - if ((field != fieldId) || !(value in internalIds)) { + if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) { converted[field] = value; } } } } - return converted; }; @@ -883,6 +894,17 @@ DataSet.prototype._updateItem = function (item) { return id; }; +/** + * check if an id is an internal or external id + * @param id + * @returns {boolean} + * @private + */ +DataSet.prototype.isInternalId = function(id) { + return (id in this.internalIds); +}; + + /** * Get an array with the column names of a Google DataTable * @param {DataTable} dataTable diff --git a/src/graph/ClusterMixin.js b/src/graph/ClusterMixin.js new file mode 100644 index 00000000..74436275 --- /dev/null +++ b/src/graph/ClusterMixin.js @@ -0,0 +1,1019 @@ +/** + * Creation of the ClusterMixin var. + * + * This contains all the functions the Graph object can use to employ clustering + * + * Alex de Mulder + * 21-01-2013 + */ +var ClusterMixin = { + +/** + * This is only called in the constructor of the graph object + * */ + startWithClustering : function() { + // cluster if the data set is big + this.clusterToFit(this.constants.clustering.initialMaxNodes, true); + + // updates the lables after clustering + this.updateLabels(); + + // this is called here because if clusterin is disabled, the start and stabilize are called in + // the setData function. + if (this.stabilize) { + this._doStabilize(); + } + this.start(); + }, + + /** + * This function clusters until the initialMaxNodes has been reached + * + * @param {Number} maxNumberOfNodes + * @param {Boolean} reposition + */ + clusterToFit : function(maxNumberOfNodes, reposition) { + var numberOfNodes = this.nodeIndices.length; + + var maxLevels = 50; + var level = 0; + + // we first cluster the hubs, then we pull in the outliers, repeat + while (numberOfNodes > maxNumberOfNodes && level < maxLevels) { + if (level % 3 == 0) { + this.forceAggregateHubs(); + } + else { + this.increaseClusterLevel(); + } + numberOfNodes = this.nodeIndices.length; + level += 1; + } + + // after the clustering we reposition the nodes to reduce the initial chaos + if (level > 1 && reposition == true) { + this.repositionNodes(); + } + }, + + /** + * This function can be called to open up a specific cluster. It is only called by + * It will unpack the cluster back one level. + * + * @param node | Node object: cluster to open. + */ + openCluster : function(node) { + var isMovingBeforeClustering = this.moving; + if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && + !(this._sector() == "default" && this.nodeIndices.length == 1)) { + this._addSector(node); + var level = 0; + while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { + this.decreaseClusterLevel(); + level += 1; + } + } + else { + this._expandClusterNode(node,false,true); + + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this.updateLabels(); + } + + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + }, + + /** + * This calls the updateClustes with default arguments + */ + updateClustersDefault : function() { + if (this.constants.clustering.enabled == true) { + this.updateClusters(0,false,false); + } + }, + + /** + * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will + * be clustered with their connected node. This can be repeated as many times as needed. + * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. + */ + increaseClusterLevel : function() { + this.updateClusters(-1,false,true); + }, + + + + /** + * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will + * be unpacked if they are a cluster. This can be repeated as many times as needed. + * This can be called externally (by a key-bind for instance) to look into clusters without zooming. + */ + decreaseClusterLevel : function() { + this.updateClusters(1,false,true); + }, + + + /** + * This is the main clustering function. It clusters and declusters on zoom or forced + * This function clusters on zoom, it can be called with a predefined zoom direction + * If out, check if we can form clusters, if in, check if we can open clusters. + * This function is only called from _zoom() + * + * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn + * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters + * @param {Boolean} force | enabled or disable forcing + * + */ + updateClusters : function(zoomDirection,recursive,force) { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; + + // on zoom out collapse the sector if the scale is at the level the sector was made + if (this.previousScale > this.scale && zoomDirection == 0) { + this._collapseSector(); + } + + // check if we zoom in or out + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + // forming clusters when forced pulls outliers in. When not forced, the edge length of the + // outer nodes determines if it is being clustered + this._formClusters(force); + } + else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in + if (force == true) { + // _openClusters checks for each node if the formationScale of the cluster is smaller than + // the current scale and if so, declusters. When forced, all clusters are reduced by one step + this._openClusters(recursive,force); + } + else { + // if a cluster takes up a set percentage of the active window + this._openClustersBySize(); + } + } + this._updateNodeIndexList(); + + // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs + if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { + this._aggregateHubs(force); + this._updateNodeIndexList(); + } + + // we now reduce chains. + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + this.handleChains(); + this._updateNodeIndexList(); + } + + this.previousScale = this.scale; + + // rest of the update the index list, dynamic edges and labels + this._updateDynamicEdges(); + this.updateLabels(); + + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place + this.clusterSession += 1; + } + + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + }, + + /** + * This function handles the chains. It is called on every updateClusters(). + */ + handleChains : function() { + // after clustering we check how many chains there are + var chainPercentage = this._getChainFraction(); + if (chainPercentage > this.constants.clustering.chainThreshold) { + this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage) + + } + }, + + /** + * this functions starts clustering by hubs + * The minimum hub threshold is set globally + * + * @private + */ + _aggregateHubs : function(force) { + this._getHubSize(); + this._formClustersByHub(force,false); + }, + + + /** + * This function is fired by keypress. It forces hubs to form. + * + */ + forceAggregateHubs : function() { + var isMovingBeforeClustering = this.moving; + var amountOfNodes = this.nodeIndices.length; + + this._aggregateHubs(true); + + // update the index list, dynamic edges and labels + this._updateNodeIndexList(); + this._updateDynamicEdges(); + this.updateLabels(); + + // if a cluster was formed, we increase the clusterSession + if (this.nodeIndices.length != amountOfNodes) { + this.clusterSession += 1; + } + + // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded + if (this.moving != isMovingBeforeClustering) { + this.start(); + } + }, + + /** + * If a cluster takes up more than a set percentage of the screen, open the cluster + * + * @private + */ + _openClustersBySize : function() { + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.inView() == true) { + if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + this.openCluster(node); + } + } + } + } + }, + + + /** + * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it + * has to be opened based on the current zoom level. + * + * @private + */ + _openClusters : function(recursive,force) { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + this._expandClusterNode(node,recursive,force); + } + }, + + /** + * This function checks if a node has to be opened. This is done by checking the zoom level. + * If the node contains child nodes, this function is recursively called on the child nodes as well. + * This recursive behaviour is optional and can be set by the recursive argument. + * + * @param {Node} parentNode | to check for cluster and expand + * @param {Boolean} recursive | enabled or disable recursive calling + * @param {Boolean} force | enabled or disable forcing + * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released + * @private + */ + _expandClusterNode : function(parentNode, recursive, force, openAll) { + // first check if node is a cluster + if (parentNode.clusterSize > 1) { + // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 + if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) { + openAll = true; + } + recursive = openAll ? true : recursive; + + // if the last child has been added on a smaller scale than current scale decluster + if (parentNode.formationScale < this.scale || force == true) { + // we will check if any of the contained child nodes should be removed from the cluster + for (var containedNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) { + var childNode = parentNode.containedNodes[containedNodeId]; + + // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that + // the largest cluster is the one that comes from outside + if (force == true) { + if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] + || openAll) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } + } + else { + if (this._nodeInActiveArea(parentNode)) { + this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll); + } + } + } + } + } + } + }, + + /** + * ONLY CALLED FROM _expandClusterNode + * + * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove + * the child node from the parent contained_node object and put it back into the global nodes object. + * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. + * + * @param {Node} parentNode | the parent node + * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node + * @param {Boolean} recursive | This will also check if the child needs to be expanded. + * With force and recursive both true, the entire cluster is unpacked + * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent + * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released + * @private + */ + _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) { + var childNode = parentNode.containedNodes[containedNodeId]; + + // if child node has been added on smaller scale than current, kick out + if (childNode.formationScale < this.scale || force == true) { + // put the child node back in the global nodes object + this.nodes[containedNodeId] = childNode; + + // release the contained edges from this childNode back into the global edges + this._releaseContainedEdges(parentNode,childNode); + + // reconnect rerouted edges to the childNode + this._connectEdgeBackToChild(parentNode,childNode); + + // validate all edges in dynamicEdges + this._validateEdges(parentNode); + + // undo the changes from the clustering operation on the parent node + parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass; + parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; + parentNode.clusterSize -= childNode.clusterSize; + parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; + + // place the child node near the parent, not at the exact same location to avoid chaos in the system + childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; + childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; + + // remove node from the list + delete parentNode.containedNodes[containedNodeId]; + + // check if there are other childs with this clusterSession in the parent. + var othersPresent = false; + for (var childNodeId in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(childNodeId)) { + if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) { + othersPresent = true; + break; + } + } + } + // if there are no others, remove the cluster session from the list + if (othersPresent == false) { + parentNode.clusterSessions.pop(); + } + + // remove the clusterSession from the child node + childNode.clusterSession = 0; + + // restart the simulation to reorganise all nodes + this.moving = true; + + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); + } + + // check if a further expansion step is possible if recursivity is enabled + if (recursive == true) { + this._expandClusterNode(childNode,recursive,force,openAll); + } + }, + + + /** + * This function checks if any nodes at the end of their trees have edges below a threshold length + * This function is called only from updateClusters() + * forceLevelCollapse ignores the length of the edge and collapses one level + * This means that a node with only one edge will be clustered with its connected node + * + * @private + * @param {Boolean} force + */ + _formClusters : function(force) { + if (force == false) { + this._formClustersByZoom(); + } + else { + this._forceClustersByZoom(); + } + }, + + /** + * This function handles the clustering by zooming out, this is based on a minimum edge distance + * + * @private + */ + _formClustersByZoom : function() { + var dx,dy,length, + minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + + // check if any edges are shorter than minLength and start the clustering + // the clustering favours the node with the larger mass + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + var edge = this.edges[edgeId]; + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); + + + if (length < minLength) { + // first check which node is larger + var parentNode = edge.from; + var childNode = edge.to; + if (edge.to.mass > edge.from.mass) { + parentNode = edge.to; + childNode = edge.from; + } + + if (childNode.dynamicEdgesLength == 1) { + this._addToCluster(parentNode,childNode,false); + } + else if (parentNode.dynamicEdgesLength == 1) { + this._addToCluster(childNode,parentNode,false); + } + } + } + } + } + } + }, + + /** + * This function forces the graph to cluster all nodes with only one connecting edge to their + * connected node. + * + * @private + */ + _forceClustersByZoom : function() { + for (var nodeId in this.nodes) { + // another node could have absorbed this child. + if (this.nodes.hasOwnProperty(nodeId)) { + var childNode = this.nodes[nodeId]; + + // the edges can be swallowed by another decrease + if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { + var edge = childNode.dynamicEdges[0]; + var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; + + // group to the largest node + if (childNode.id != parentNode.id) { + if (parentNode.mass > childNode.mass) { + this._addToCluster(parentNode,childNode,true); + } + else { + this._addToCluster(childNode,parentNode,true); + } + } + } + } + } + }, + + + + /** + * This function forms clusters from hubs, it loops over all nodes + * + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @private + */ + _formClustersByHub : function(force, onlyEqual) { + // we loop over all nodes in the list + for (var nodeId in this.nodes) { + // we check if it is still available since it can be used by the clustering in this loop + if (this.nodes.hasOwnProperty(nodeId)) { + this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); + } + } + }, + + /** + * This function forms a cluster from a specific preselected hub node + * + * @param {Node} hubNode | the node we will cluster as a hub + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @param {Number} [absorptionSizeOffset] | + * @private + */ + _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) { + if (absorptionSizeOffset === undefined) { + absorptionSizeOffset = 0; + } + // we decide if the node is a hub + if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || + (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { + // initialize variables + var dx,dy,length; + var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale; + var allowCluster = false; + + // we create a list of edges because the dynamicEdges change over the course of this loop + var edgesIdarray = []; + var amountOfInitialEdges = hubNode.dynamicEdges.length; + for (var j = 0; j < amountOfInitialEdges; j++) { + edgesIdarray.push(hubNode.dynamicEdges[j].id); + } + + // if the hub clustering is not forces, we check if one of the edges connected + // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold + if (force == false) { + allowCluster = false; + for (j = 0; j < amountOfInitialEdges; j++) { + var edge = this.edges[edgesIdarray[j]]; + if (edge !== undefined) { + if (edge.connected) { + if (edge.toId != edge.fromId) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); + + if (length < minLength) { + allowCluster = true; + break; + } + } + } + } + } + } + + // start the clustering if allowed + if ((!force && allowCluster) || force) { + // we loop over all edges INITIALLY connected to this hub + for (j = 0; j < amountOfInitialEdges; j++) { + edge = this.edges[edgesIdarray[j]]; + // the edge can be clustered by this function in a previous loop + if (edge !== undefined) { + var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; + // we do not want hubs to merge with other hubs nor do we want to cluster itself. + if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) && + (childNode.id != hubNode.id)) { + this._addToCluster(hubNode,childNode,force); + } + } + } + } + } + }, + + + + /** + * This function adds the child node to the parent node, creating a cluster if it is not already. + * + * @param {Node} parentNode | this is the node that will house the child node + * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node + * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse + * @private + */ + _addToCluster : function(parentNode, childNode, force) { + // join child node in the parent node + parentNode.containedNodes[childNode.id] = childNode; + + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < childNode.dynamicEdges.length; i++) { + var edge = childNode.dynamicEdges[i]; + if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode + this._addToContainedEdges(parentNode,childNode,edge); + } + else { + this._connectEdgeToCluster(parentNode,childNode,edge); + } + } + // a contained node has no dynamic edges. + childNode.dynamicEdges = []; + + // remove circular edges from clusters + this._containCircularEdgesFromNode(parentNode,childNode); + + + // remove the childNode from the global nodes object + delete this.nodes[childNode.id]; + + // update the properties of the child and parent + var massBefore = parentNode.mass; + childNode.clusterSession = this.clusterSession; + parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass; + parentNode.clusterSize += childNode.clusterSize; + parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; + + // keep track of the clustersessions so we can open the cluster up as it has been formed. + if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { + parentNode.clusterSessions.push(this.clusterSession); + } + + // forced clusters only open from screen size and double tap + if (force == true) { + // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); + parentNode.formationScale = 0; + } + else { + parentNode.formationScale = this.scale; // The latest child has been added on this scale + } + + // recalculate the size of the node on the next time the node is rendered + parentNode.clearSizeCache(); + + // set the pop-out scale for the childnode + parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; + + // nullify the movement velocity of the child, this is to avoid hectic behaviour + childNode.clearVelocity(); + + // the mass has altered, preservation of energy dictates the velocity to be updated + parentNode.updateVelocity(massBefore); + + // restart the simulation to reorganise all nodes + this.moving = true; + }, + + + /** + * This function will apply the changes made to the remainingEdges during the formation of the clusters. + * This is a seperate function to allow for level-wise collapsing of the node tree. + * It has to be called if a level is collapsed. It is called by _formClusters(). + * @private + */ + _updateDynamicEdges : function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + node.dynamicEdgesLength = node.dynamicEdges.length; + + // this corrects for multiple edges pointing at the same other node + var correction = 0; + if (node.dynamicEdgesLength > 1) { + for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { + var edgeToId = node.dynamicEdges[j].toId; + var edgeFromId = node.dynamicEdges[j].fromId; + for (var k = j+1; k < node.dynamicEdgesLength; k++) { + if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || + (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { + correction += 1; + } + } + } + } + node.dynamicEdgesLength -= correction; + } + }, + + + /** + * This adds an edge from the childNode to the contained edges of the parent node + * + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object + * @private + */ + _addToContainedEdges : function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { + parentNode.containedEdges[childNode.id] = [] + } + // add this edge to the list + parentNode.containedEdges[childNode.id].push(edge); + + // remove the edge from the global edges object + delete this.edges[edge.id]; + + // remove the edge from the parent object + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + if (parentNode.dynamicEdges[i].id == edge.id) { + parentNode.dynamicEdges.splice(i,1); + break; + } + } + }, + + /** + * This function connects an edge that was connected to a child node to the parent node. + * It keeps track of which nodes it has been connected to with the originalId array. + * + * @param {Node} parentNode | Node object + * @param {Node} childNode | Node object + * @param {Edge} edge | Edge object + * @private + */ + _connectEdgeToCluster : function(parentNode, childNode, edge) { + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); + } + else { + if (edge.toId == childNode.id) { // edge connected to other node on the "to" side + edge.originalToId.push(childNode.id); + edge.to = parentNode; + edge.toId = parentNode.id; + } + else { // edge connected to other node with the "from" side + + edge.originalFromId.push(childNode.id); + edge.from = parentNode; + edge.fromId = parentNode.id; + } + + this._addToReroutedEdges(parentNode,childNode,edge); + } + }, + + + _containCircularEdgesFromNode : function(parentNode, childNode) { + // manage all the edges connected to the child and parent nodes + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + // handle circular edges + if (edge.toId == edge.fromId) { + this._addToContainedEdges(parentNode, childNode, edge); + } + } + }, + + + /** + * This adds an edge from the childNode to the rerouted edges of the parent node + * + * @param parentNode | Node object + * @param childNode | Node object + * @param edge | Edge object + * @private + */ + _addToReroutedEdges : function(parentNode, childNode, edge) { + // create an array object if it does not yet exist for this childNode + // we store the edge in the rerouted edges so we can restore it when the cluster pops open + if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { + parentNode.reroutedEdges[childNode.id] = []; + } + parentNode.reroutedEdges[childNode.id].push(edge); + + // this edge becomes part of the dynamicEdges of the cluster node + parentNode.dynamicEdges.push(edge); + }, + + + + /** + * This function connects an edge that was connected to a cluster node back to the child node. + * + * @param parentNode | Node object + * @param childNode | Node object + * @private + */ + _connectEdgeBackToChild : function(parentNode, childNode) { + if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { + for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { + var edge = parentNode.reroutedEdges[childNode.id][i]; + if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) { + edge.originalFromId.pop(); + edge.fromId = childNode.id; + edge.from = childNode; + } + else { + edge.originalToId.pop(); + edge.toId = childNode.id; + edge.to = childNode; + } + + // append this edge to the list of edges connecting to the childnode + childNode.dynamicEdges.push(edge); + + // remove the edge from the parent object + for (var j = 0; j < parentNode.dynamicEdges.length; j++) { + if (parentNode.dynamicEdges[j].id == edge.id) { + parentNode.dynamicEdges.splice(j,1); + break; + } + } + } + // remove the entry from the rerouted edges + delete parentNode.reroutedEdges[childNode.id]; + } + }, + + + /** + * When loops are clustered, an edge can be both in the rerouted array and the contained array. + * This function is called last to verify that all edges in dynamicEdges are in fact connected to the + * parentNode + * + * @param parentNode | Node object + * @private + */ + _validateEdges : function(parentNode) { + for (var i = 0; i < parentNode.dynamicEdges.length; i++) { + var edge = parentNode.dynamicEdges[i]; + if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { + parentNode.dynamicEdges.splice(i,1); + } + } + }, + + + /** + * This function released the contained edges back into the global domain and puts them back into the + * dynamic edges of both parent and child. + * + * @param {Node} parentNode | + * @param {Node} childNode | + * @private + */ + _releaseContainedEdges : function(parentNode, childNode) { + for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) { + var edge = parentNode.containedEdges[childNode.id][i]; + + // put the edge back in the global edges object + this.edges[edge.id] = edge; + + // put the edge back in the dynamic edges of the child and parent + childNode.dynamicEdges.push(edge); + parentNode.dynamicEdges.push(edge); + } + // remove the entry from the contained edges + delete parentNode.containedEdges[childNode.id]; + + }, + + + + + // ------------------- UTILITY FUNCTIONS ---------------------------- // + + + /** + * This updates the node labels for all nodes (for debugging purposes) + */ + updateLabels : function() { + var nodeId; + // update node labels + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + var node = this.nodes[nodeId]; + if (node.clusterSize > 1) { + node.label = "[".concat(String(node.clusterSize),"]"); + } + } + } + + // update node labels + for (nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + if (node.clusterSize == 1) { + if (node.originalLabel !== undefined) { + node.label = node.originalLabel; + } + else { + node.label = String(node.id); + } + } + } + } + + /* Debug Override */ + // for (nodeId in this.nodes) { + // if (this.nodes.hasOwnProperty(nodeId)) { + // node = this.nodes[nodeId]; + // node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale)); + // } + // } + + }, + + + /** + * This function determines if the cluster we want to decluster is in the active area + * this means around the zoom center + * + * @param {Node} node + * @returns {boolean} + * @private + */ + _nodeInActiveArea : function(node) { + return ( + Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale + && + Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale + ) + }, + + + /** + * This is an adaptation of the original repositioning function. This is called if the system is clustered initially + * It puts large clusters away from the center and randomizes the order. + * + */ + repositionNodes : function() { + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + if (!node.isFixed()) { + var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize); + var angle = 2 * Math.PI * Math.random(); + node.x = radius * Math.cos(angle); + node.y = radius * Math.sin(angle); + } + } + }, + + + + + + /** + * 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 + */ + _getHubSize : function() { + var average = 0; + var averageSquared = 0; + var hubCounter = 0; + var largestHub = 0; + + for (var i = 0; i < this.nodeIndices.length; i++) { + var node = this.nodes[this.nodeIndices[i]]; + if (node.dynamicEdgesLength > largestHub) { + largestHub = node.dynamicEdgesLength; + } + average += node.dynamicEdgesLength; + averageSquared += Math.pow(node.dynamicEdgesLength,2); + hubCounter += 1; + } + average = average / hubCounter; + averageSquared = averageSquared / hubCounter; + + var variance = averageSquared - Math.pow(average,2); + + var standardDeviation = Math.sqrt(variance); + + this.hubThreshold = Math.floor(average + 2*standardDeviation); + + // always have at least one to cluster + if (this.hubThreshold > largestHub) { + this.hubThreshold = largestHub; + } + + // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); + // console.log("hubThreshold:",this.hubThreshold); + }, + + + /** + * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. + * + * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce + * @private + */ + _reduceAmountOfChains : function(fraction) { + this.hubThreshold = 2; + var reduceAmount = Math.floor(this.nodeIndices.length * fraction); + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + if (reduceAmount > 0) { + this._formClusterFromHub(this.nodes[nodeId],true,true,1); + reduceAmount -= 1; + } + } + } + } + }, + + /** + * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these chains. + * + * @private + */ + _getChainFraction : function() { + var chains = 0; + var total = 0; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) { + chains += 1; + } + total += 1; + } + } + return chains/total; + } +}; diff --git a/src/graph/Edge.js b/src/graph/Edge.js index 28aaa7cc..574685e8 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -26,7 +26,7 @@ function Edge (properties, graph, constants) { // initialize variables this.id = undefined; this.fromId = undefined; - this.toId = undefined; + this.toId = undefined; this.style = constants.edges.style; this.title = undefined; this.width = constants.edges.width; @@ -35,6 +35,12 @@ function Edge (properties, graph, constants) { this.from = null; // a node this.to = null; // a node + + // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster + // by storing the original information we can revert to the original connection when the cluser is opened. + this.originalFromId = []; + this.originalToId = []; + this.connected = false; // Added to support dashed lines @@ -48,6 +54,7 @@ function Edge (properties, graph, constants) { this.lengthFixed = false; this.setProperties(properties, constants); + } /** @@ -60,41 +67,41 @@ Edge.prototype.setProperties = function(properties, constants) { return; } - if (properties.from != undefined) {this.fromId = properties.from;} - if (properties.to != undefined) {this.toId = properties.to;} + if (properties.from !== undefined) {this.fromId = properties.from;} + if (properties.to !== undefined) {this.toId = properties.to;} - if (properties.id != undefined) {this.id = properties.id;} - if (properties.style != undefined) {this.style = properties.style;} - if (properties.label != undefined) {this.label = properties.label;} + if (properties.id !== undefined) {this.id = properties.id;} + if (properties.style !== undefined) {this.style = properties.style;} + if (properties.label !== undefined) {this.label = properties.label;} if (this.label) { this.fontSize = constants.edges.fontSize; this.fontFace = constants.edges.fontFace; this.fontColor = constants.edges.fontColor; - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;} } - if (properties.title != undefined) {this.title = properties.title;} - if (properties.width != undefined) {this.width = properties.width;} - if (properties.value != undefined) {this.value = properties.value;} - if (properties.length != undefined) {this.length = properties.length;} + if (properties.title !== undefined) {this.title = properties.title;} + if (properties.width !== undefined) {this.width = properties.width;} + if (properties.value !== undefined) {this.value = properties.value;} + if (properties.length !== undefined) {this.length = properties.length;} // Added to support dashed lines // David Jordan // 2012-08-08 if (properties.dash) { - if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;} - if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;} - if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;} + if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;} + if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;} + if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;} } - if (properties.color != undefined) {this.color = properties.color;} + if (properties.color !== undefined) {this.color = properties.color;} // A node is connected when it has a from and to node. this.connect(); - this.widthFixed = this.widthFixed || (properties.width != undefined); - this.lengthFixed = this.lengthFixed || (properties.length != undefined); + this.widthFixed = this.widthFixed || (properties.width !== undefined); + this.lengthFixed = this.lengthFixed || (properties.length !== undefined); this.stiffness = 1 / this.length; // set draw method based on style @@ -262,10 +269,10 @@ Edge.prototype._drawLine = function(ctx) { */ Edge.prototype._getLineWidth = function() { if (this.from.selected || this.to.selected) { - return Math.min(this.width * 2, this.widthMax); + return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv; } else { - return this.width; + return this.width*this.graphScaleInv; } }; @@ -343,12 +350,12 @@ Edge.prototype._drawDashLine = function(ctx) { // draw dashed line ctx.beginPath(); ctx.lineCap = 'round'; - if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value + if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value { ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]); } - else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value + else if (this.dash.length !== undefined && this.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value { ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y, [this.dash.length,this.dash.gap]); @@ -600,3 +607,14 @@ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point return Math.sqrt(dx*dx + dy*dy); }; + + + +/** + * This allows the zoom level of the graph to influence the rendering + * + * @param scale + */ +Edge.prototype.setScale = function(scale) { + this.graphScaleInv = 1.0/scale; +}; \ No newline at end of file diff --git a/src/graph/Graph.js b/src/graph/Graph.js index 9b73aaf8..7d81240e 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -14,10 +14,14 @@ function Graph (container, data, options) { this.containerElement = container; this.width = '100%'; this.height = '100%'; - this.refreshRate = 50; // milliseconds + // to give everything a nice fluidity, we seperate the rendering and calculating of the forces + this.renderRefreshRate = 60; // hz (fps) + this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on this.stabilize = true; // stabilize before displaying the graph this.selectable = true; + this.forceFactor = 50000; + // set constant values this.constants = { nodes: { @@ -34,8 +38,8 @@ function Graph (container, data, options) { //fontFace: verdana, fontFace: 'arial', color: { - border: '#2B7CE9', - background: '#97C2FC', + border: '#2B7CE9', + background: '#97C2FC', highlight: { border: '#2B7CE9', background: '#D2E5FF' @@ -63,14 +67,78 @@ function Graph (container, data, options) { altLength: undefined } }, - minForce: 0.05, - minVelocity: 0.02, // px/s + clustering: { // Per Node in Cluster = PNiC + enabled: false, // (Boolean) | global on/off switch for clustering. + initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold. + clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes + reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this + chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains). + clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered. + sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector. + screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node. + fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px). + forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster). + distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster). + edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength. + nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster. + height: 10, // (px PNiC) | growth of the height per node in cluster. + radius: 10}, // (px PNiC) | growth of the radius per node in cluster. + activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open. + massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass + }, + navigation: { + enabled: false, + iconPath: this._getScriptPath() + '/img' + }, + keyboard: { + enabled: false, + speed: {x: 10, y: 10, zoom: 0.02} + }, + minVelocity: 2, // px/s maxIterations: 1000 // maximum number of iteration to stabilize }; + // Node variables + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function () { + graph._redraw(); + }); + + // navigation variables + this.xIncrement = 0; + this.yIncrement = 0; + this.zoomIncrement = 0; + + // create a frame and canvas + this._create(); + + // load the sector system. (mandatory, fully integrated with Graph) + this._loadSectorSystem(); + + // apply options + this.setOptions(options); + + // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) + this._loadClusterSystem(); + + // load the selection system. (mandatory, required by Graph) + this._loadSelectionSystem(); + + // other vars var graph = this; - this.nodes = {}; // object with Node objects - this.edges = {}; // object with Edge objects + this.freezeSimulation = false;// freeze the simulation + + this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation + this.nodes = {}; // object with Node objects + this.edges = {}; // object with Edge objects + + this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw. + this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw + + this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action + this.scale = 1; // defining the global scale variable in the constructor + this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out // TODO: create a counter to keep track on the number of nodes having values // TODO: create a counter to keep track on the number of nodes currently moving // TODO: create a counter to keep track on the number of edges having values @@ -109,38 +177,164 @@ function Graph (container, data, options) { } }; - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - graph._redraw(); - }); - // properties of the data this.moving = false; // True if any of the nodes have an undefined position - - this.selection = []; this.timer = undefined; - // create a frame and canvas - this._create(); + // load data (the disable start variable will be the same as the enabled clustering) + this.setData(data,this.constants.clustering.enabled); - // apply options - this.setOptions(options); + // zoom so all data will fit on the screen + this.zoomToFit(true); - // draw data - this.setData(data); + // if clustering is disabled, the simulation will have started in the setData function + if (this.constants.clustering.enabled) { + this.startWithClustering(); + } } +/** + * Get the script path where the vis.js library is located + * + * @returns {string | null} path Path or null when not found. Path does not + * end with a slash. + * @private + */ +Graph.prototype._getScriptPath = function() { + var scripts = document.getElementsByTagName( 'script' ); + + // find script named vis.js or vis.min.js + for (var i = 0; i < scripts.length; i++) { + var src = scripts[i].src; + var match = src && /\/?vis(.min)?\.js$/.exec(src); + if (match) { + // return path without the script name + return src.substring(0, src.length - match[0].length); + } + } + + return null; +}; + + +/** + * Find the center position of the graph + * @private + */ +Graph.prototype._getRange = function() { + var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; + for (var i = 0; i < this.nodeIndices.length; i++) { + node = this.nodes[this.nodeIndices[i]]; + if (minX > (node.x - node.width)) {minX = node.x - node.width;} + if (maxX < (node.x + node.width)) {maxX = node.x + node.width;} + if (minY > (node.y - node.height)) {minY = node.y - node.height;} + if (maxY < (node.y + node.height)) {maxY = node.y + node.height;} + } + return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; +}; + + +/** + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + * @returns {{x: number, y: number}} + * @private + */ +Graph.prototype._findCenter = function(range) { + var center = {x: (0.5 * (range.maxX + range.minX)), + y: (0.5 * (range.maxY + range.minY))}; + return center; +}; + + +/** + * center the graph + * + * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; + */ +Graph.prototype._centerGraph = function(range) { + var center = this._findCenter(range); + + center.x *= this.scale; + center.y *= this.scale; + center.x -= 0.5 * this.frame.canvas.clientWidth; + center.y -= 0.5 * this.frame.canvas.clientHeight; + + this._setTranslation(-center.x,-center.y); // set at 0,0 +}; + + +/** + * This function zooms out to fit all data on screen based on amount of nodes + * + * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; + */ +Graph.prototype.zoomToFit = function(initialZoom) { + if (initialZoom === undefined) { + initialZoom = false; + } + + var numberOfNodes = this.nodeIndices.length; + var range = this._getRange(); + + if (initialZoom == true) { + if (this.constants.clustering.enabled == true && + numberOfNodes >= this.constants.clustering.initialMaxNodes) { + var zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + else { + var zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. + } + } + else { + var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1; + var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1; + + var xZoomLevel = this.frame.canvas.clientWidth / xDistance; + var yZoomLevel = this.frame.canvas.clientHeight / yDistance; + + zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; + } + + if (zoomLevel > 1.0) { + zoomLevel = 1.0; + } + + this.pinch.mousewheelScale = zoomLevel; + this._setScale(zoomLevel); + this._centerGraph(range); + this.start(); +}; + + +/** + * Update the this.nodeIndices with the most recent node index list + * @private + */ +Graph.prototype._updateNodeIndexList = function() { + this._clearNodeIndexList(); + for (var idx in this.nodes) { + if (this.nodes.hasOwnProperty(idx)) { + this.nodeIndices.push(idx); + } + } +}; + + /** * 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 - * {Options} [options] Object with options + * @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 + * {Options} [options] Object with options + * @param {Boolean} [disableStart] | optional: disable the calling of the start function. */ -Graph.prototype.setData = function(data) { +Graph.prototype.setData = function(data, disableStart) { + if (disableStart === undefined) { + disableStart = false; + } + if (data && data.dot && (data.nodes || data.edges)) { throw new SyntaxError('Data must contain either parameter "dot" or ' + ' parameter pair "nodes" and "edges", but not both.'); @@ -163,11 +357,16 @@ Graph.prototype.setData = function(data) { this._setEdges(data && data.edges); } - // find a stable position or start animating to a stable position - if (this.stabilize) { - this._doStabilize(); + this._putDataInSector(); + + if (!disableStart) { + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.moving = true; + this.start(); } - this.start(); }; /** @@ -177,21 +376,58 @@ Graph.prototype.setData = function(data) { Graph.prototype.setOptions = function (options) { if (options) { // retrieve parameter values - if (options.width != undefined) {this.width = options.width;} - if (options.height != undefined) {this.height = options.height;} - if (options.stabilize != undefined) {this.stabilize = options.stabilize;} - if (options.selectable != undefined) {this.selectable = options.selectable;} + if (options.width !== undefined) {this.width = options.width;} + if (options.height !== undefined) {this.height = options.height;} + if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} + if (options.selectable !== undefined) {this.selectable = options.selectable;} + + if (options.clustering) { + this.constants.clustering.enabled = true; + for (var prop in options.clustering) { + if (options.clustering.hasOwnProperty(prop)) { + this.constants.clustering[prop] = options.clustering[prop]; + } + } + } + else if (options.clustering !== undefined) { + this.constants.clustering.enabled = false; + } + + if (options.navigation) { + this.constants.navigation.enabled = true; + for (var prop in options.navigation) { + if (options.navigation.hasOwnProperty(prop)) { + this.constants.navigation[prop] = options.navigation[prop]; + } + } + } + else if (options.navigation !== undefined) { + this.constants.navigation.enabled = false; + } + + if (options.keyboard) { + this.constants.keyboard.enabled = true; + for (var prop in options.keyboard) { + if (options.keyboard.hasOwnProperty(prop)) { + this.constants.keyboard[prop] = options.keyboard[prop]; + } + } + } + else if (options.keyboard !== undefined) { + this.constants.keyboard.enabled = false; + } + // TODO: work out these options and document them if (options.edges) { - for (var prop in options.edges) { + for (prop in options.edges) { if (options.edges.hasOwnProperty(prop)) { this.constants.edges[prop] = options.edges[prop]; } } - if (options.edges.length != undefined && - options.nodes && options.nodes.distance == undefined) { + if (options.edges.length !== undefined && + options.nodes && options.nodes.distance === undefined) { this.constants.edges.length = options.edges.length; this.constants.nodes.distance = options.edges.length * 1.25; } @@ -204,13 +440,13 @@ Graph.prototype.setOptions = function (options) { // David Jordan // 2012-08-08 if (options.edges.dash) { - if (options.edges.dash.length != undefined) { + if (options.edges.dash.length !== undefined) { this.constants.edges.dash.length = options.edges.dash.length; } - if (options.edges.dash.gap != undefined) { + if (options.edges.dash.gap !== undefined) { this.constants.edges.dash.gap = options.edges.dash.gap; } - if (options.edges.dash.altLength != undefined) { + if (options.edges.dash.altLength !== undefined) { this.constants.edges.dash.altLength = options.edges.dash.altLength; } } @@ -232,7 +468,6 @@ Graph.prototype.setOptions = function (options) { if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax; */ } - if (options.groups) { for (var groupname in options.groups) { if (options.groups.hasOwnProperty(groupname)) { @@ -246,6 +481,41 @@ Graph.prototype.setOptions = function (options) { this.setSize(this.width, this.height); this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); this._setScale(1); + + // load the navigation system. + this._loadNavigationControls(); + + // bind keys. If disabled, this will not do anything; + this._createKeyBinds(); + + this._redraw(); +}; + +/** + * Add event listener + * @param {String} event Event name. Available events: + * 'select' + * @param {function} callback Callback function, invoked as callback(properties) + * where properties is an optional object containing + * event specific properties. + */ +Graph.prototype.on = function on (event, callback) { + var available = ['select']; + + if (available.indexOf(event) == -1) { + throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); + } + + events.addListener(this, event, callback); +}; + +/** + * Remove an event listener + * @param {String} event Event name + * @param {function} callback Callback function + */ +Graph.prototype.off = function off (event, callback) { + events.removeListener(this, event, callback); }; /** @@ -297,12 +567,14 @@ Graph.prototype._create = function () { prevent_default: true }); this.hammer.on('tap', me._onTap.bind(me) ); + this.hammer.on('doubletap', me._onDoubleTap.bind(me) ); this.hammer.on('hold', me._onHold.bind(me) ); this.hammer.on('pinch', me._onPinch.bind(me) ); this.hammer.on('touch', me._onTouch.bind(me) ); this.hammer.on('dragstart', me._onDragStart.bind(me) ); this.hammer.on('drag', me._onDrag.bind(me) ); this.hammer.on('dragend', me._onDragEnd.bind(me) ); + this.hammer.on('release', me._onRelease.bind(me) ); this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); @@ -311,29 +583,48 @@ Graph.prototype._create = function () { this.containerElement.appendChild(this.frame); }; + /** - * - * @param {{x: Number, y: Number}} pointer - * @return {Number | null} node + * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin * @private */ -Graph.prototype._getNodeAt = function (pointer) { - var x = this._canvasToX(pointer.x); - var y = this._canvasToY(pointer.y); - - var obj = { - left: x, - top: y, - right: x, - bottom: y - }; - - // if there are overlapping nodes, select the last one, this is the - // one which is drawn on top of the others - var overlappingNodes = this._getNodesOverlappingWith(obj); - return (overlappingNodes.length > 0) ? - overlappingNodes[overlappingNodes.length - 1] : null; -}; +Graph.prototype._createKeyBinds = function() { + var me = this; + this.mousetrap = mousetrap; + + this.mousetrap.reset(); + + if (this.constants.keyboard.enabled == true) { + this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown"); + this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup"); + this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown"); + this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup"); + this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown"); + this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup"); + this.mousetrap.bind("right",this._moveRight.bind(me), "keydown"); + this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup"); + this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown"); + this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown"); + this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown"); + this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown"); + this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown"); + this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup"); + this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); + this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); + } + /* + this.mousetrap.bind("=",this.decreaseClusterLevel.bind(me)); + this.mousetrap.bind("-",this.increaseClusterLevel.bind(me)); + this.mousetrap.bind("s",this.singleStep.bind(me)); + this.mousetrap.bind("h",this.updateClustersDefault.bind(me)); + this.mousetrap.bind("c",this._collapseSector.bind(me)); + this.mousetrap.bind("f",this.toggleFreeze.bind(me)); + */ +} /** * Get the pointer location from a touch location @@ -357,6 +648,8 @@ Graph.prototype._onTouch = function (event) { this.drag.pointer = this._getPointer(event.gesture.touches[0]); this.drag.pinched = false; this.pinch.scale = this._getScale(); + + this._handleTouch(this.drag.pointer); }; /** @@ -365,17 +658,19 @@ Graph.prototype._onTouch = function (event) { */ Graph.prototype._onDragStart = function () { var drag = this.drag; + var node = this._getNodeAt(drag.pointer); + // note: drag.pointer is set in _onTouch to get the initial touch location + drag.dragging = true; drag.selection = []; drag.translation = this._getTranslation(); - drag.nodeId = this._getNodeAt(drag.pointer); - // note: drag.pointer is set in _onTouch to get the initial touch location + drag.nodeId = null; - var node = this.nodes[drag.nodeId]; - if (node) { + if (node != null) { + drag.nodeId = node.id; // select the clicked node if not yet selected if (!node.isSelected()) { - this._selectNodes([drag.nodeId]); + this._selectNode(node,false); } // create an array with the selected nodes and their original location and status @@ -400,7 +695,6 @@ Graph.prototype._onDragStart = function () { drag.selection.push(s); } }); - } }; @@ -451,7 +745,6 @@ Graph.prototype._onDrag = function (event) { this.drag.translation.x + diffX, this.drag.translation.y + diffY); this._redraw(); - this.moved = true; } }; @@ -461,6 +754,7 @@ Graph.prototype._onDrag = function (event) { * @private */ Graph.prototype._onDragEnd = function () { + this.drag.dragging = false; var selection = this.drag.selection; if (selection) { selection.forEach(function (s) { @@ -477,49 +771,38 @@ Graph.prototype._onDragEnd = function () { */ Graph.prototype._onTap = function (event) { var pointer = this._getPointer(event.gesture.touches[0]); + this._handleTap(pointer); +}; - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - // select this node - this._selectNodes([nodeId]); - if (!this.moving) { - this._redraw(); - } - } - else { - // remove selection - this._unselectNodes(); - this._redraw(); - } +/** + * handle doubletap event + * @private + */ +Graph.prototype._onDoubleTap = function (event) { + var pointer = this._getPointer(event.gesture.touches[0]); + this._handleDoubleTap(pointer); + }; + /** * handle long tap event: multi select nodes * @private */ Graph.prototype._onHold = function (event) { var pointer = this._getPointer(event.gesture.touches[0]); - var nodeId = this._getNodeAt(pointer); - var node = this.nodes[nodeId]; - if (node) { - if (!node.isSelected()) { - // select this node, keep previous selection - var append = true; - this._selectNodes([nodeId], append); - } - else { - this._unselectNodes([nodeId]); - } + this._handleOnHold(pointer); +}; - if (!this.moving) { - this._redraw(); - } - } - else { - // Do nothing - } +/** + * handle the release of the screen + * + * @param event + * @private + */ +Graph.prototype._onRelease = function (event) { + this._handleOnRelease(); }; /** @@ -535,7 +818,7 @@ Graph.prototype._onPinch = function (event) { this.pinch.scale = 1; } - // TODO: enable moving while pinching? + // TODO: enabled moving while pinching? var scale = this.pinch.scale * event.gesture.scale; this._zoom(scale, pointer) }; @@ -543,26 +826,34 @@ Graph.prototype._onPinch = function (event) { /** * Zoom the graph in or out * @param {Number} scale a number around 1, and between 0.01 and 10 - * @param {{x: Number, y: Number}} pointer + * @param {{x: Number, y: Number}} pointer Position on screen * @return {Number} appliedScale scale is limited within the boundaries * @private */ Graph.prototype._zoom = function(scale, pointer) { var scaleOld = this._getScale(); - if (scale < 0.01) { - scale = 0.01; + if (scale < 0.00001) { + scale = 0.00001; } if (scale > 10) { scale = 10; } - +// + this.frame.canvas.clientHeight / 2 var translation = this._getTranslation(); + var scaleFrac = scale / scaleOld; var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; + this.areaCenter = {"x" : this._canvasToX(pointer.x), + "y" : this._canvasToY(pointer.y)}; + + // this.areaCenter = {"x" : pointer.x,"y" : pointer.y }; +// console.log(translation.x,translation.y,pointer.x,pointer.y,scale); + this.pinch.mousewheelScale = scale; this._setScale(scale); this._setTranslation(tx, ty); + this.updateClustersDefault(); this._redraw(); return scale; @@ -590,12 +881,12 @@ Graph.prototype._onMouseWheel = function(event) { // Basically, delta is now positive if wheel was scrolled up, // and negative, if wheel was scrolled down. if (delta) { - if (!('mouswheelScale' in this.pinch)) { - this.pinch.mouswheelScale = 1; + if (!('mousewheelScale' in this.pinch)) { + this.pinch.mousewheelScale = 1; } // calculate the new scale - var scale = this.pinch.mouswheelScale; + var scale = this.pinch.mousewheelScale; var zoom = delta / 10; if (delta < 0) { zoom = zoom / (1 - zoom); @@ -609,8 +900,8 @@ Graph.prototype._onMouseWheel = function(event) { // apply the new scale scale = this._zoom(scale, pointer); - // store the new, applied scale - this.pinch.mouswheelScale = scale; + // store the new, applied scale -- this is now done in _zoom +// this.pinch.mousewheelScale = scale; } // Prevent default actions caused by mouse wheel. @@ -639,9 +930,9 @@ Graph.prototype._onMouseMoveTitle = function (event) { me._checkShowPopup(pointer); }; if (this.popupTimer) { - clearInterval(this.popupTimer); // stop any running timer + clearInterval(this.popupTimer); // stop any running calculationTimer } - if (!this.leftButtonDown) { + if (!this.drag.dragging) { this.popupTimer = setTimeout(checkShow, 300); } }; @@ -671,7 +962,7 @@ Graph.prototype._checkShowPopup = function (pointer) { for (id in nodes) { if (nodes.hasOwnProperty(id)) { var node = nodes[id]; - if (node.getTitle() != undefined && node.isOverlappingWith(obj)) { + if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) { this.popupNode = node; break; } @@ -679,13 +970,13 @@ Graph.prototype._checkShowPopup = function (pointer) { } } - if (this.popupNode == undefined) { + if (this.popupNode === undefined) { // search the edges for overlap var edges = this.edges; for (id in edges) { if (edges.hasOwnProperty(id)) { var edge = edges[id]; - if (edge.connected && (edge.getTitle() != undefined) && + if (edge.connected && (edge.getTitle() !== undefined) && edge.isOverlappingWith(obj)) { this.popupNode = edge; break; @@ -732,191 +1023,6 @@ Graph.prototype._checkHidePopup = function (pointer) { } }; -/** - * Unselect selected nodes. If no selection array is provided, all nodes - * are unselected - * @param {Object[]} selection Array with selection objects, each selection - * object has a parameter row. Optional - * @param {Boolean} triggerSelect If true (default), the select event - * is triggered when nodes are unselected - * @return {Boolean} changed True if the selection is changed - * @private - */ -Graph.prototype._unselectNodes = function(selection, triggerSelect) { - var changed = false; - var i, iMax, id; - - if (selection) { - // remove provided selections - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - this.nodes[id].unselect(); - - var j = 0; - while (j < this.selection.length) { - if (this.selection[j] == id) { - this.selection.splice(j, 1); - changed = true; - } - else { - j++; - } - } - } - } - else if (this.selection && this.selection.length) { - // remove all selections - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - changed = true; - } - this.selection = []; - } - - if (changed && (triggerSelect == true || triggerSelect == undefined)) { - // fire the select event - this._trigger('select'); - } - - return changed; -}; - -/** - * select all nodes on given location x, y - * @param {Array} selection an array with node ids - * @param {boolean} append If true, the new selection will be appended to the - * current selection (except for duplicate entries) - * @return {Boolean} changed True if the selection is changed - * @private - */ -Graph.prototype._selectNodes = function(selection, append) { - var changed = false; - var i, iMax; - - // TODO: the selectNodes method is a little messy, rework this - - // check if the current selection equals the desired selection - var selectionAlreadyThere = true; - if (selection.length != this.selection.length) { - selectionAlreadyThere = false; - } - else { - for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { - if (selection[i] != this.selection[i]) { - selectionAlreadyThere = false; - break; - } - } - } - if (selectionAlreadyThere) { - return changed; - } - - if (append == undefined || append == false) { - // first deselect any selected node - var triggerSelect = false; - changed = this._unselectNodes(undefined, triggerSelect); - } - - for (i = 0, iMax = selection.length; i < iMax; i++) { - // add each of the new selections, but only when they are not duplicate - var id = selection[i]; - var isDuplicate = (this.selection.indexOf(id) != -1); - if (!isDuplicate) { - this.nodes[id].select(); - this.selection.push(id); - changed = true; - } - } - - if (changed) { - // fire the select event - this._trigger('select'); - } - - return changed; -}; - -/** - * retrieve all nodes overlapping with given object - * @param {Object} obj An object with parameters left, top, right, bottom - * @return {Number[]} An array with id's of the overlapping nodes - * @private - */ -Graph.prototype._getNodesOverlappingWith = function (obj) { - var nodes = this.nodes, - overlappingNodes = []; - - for (var id in nodes) { - if (nodes.hasOwnProperty(id)) { - if (nodes[id].isOverlappingWith(obj)) { - overlappingNodes.push(id); - } - } - } - - return overlappingNodes; -}; - -/** - * retrieve the currently selected nodes - * @return {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ -Graph.prototype.getSelection = function() { - return this.selection.concat([]); -}; - -/** - * select zero or more nodes - * @param {Number[] | String[]} selection An array with the ids of the - * selected nodes. - */ -Graph.prototype.setSelection = function(selection) { - var i, iMax, id; - - if (!selection || (selection.length == undefined)) - throw 'Selection must be an array with ids'; - - // first unselect any selected node - for (i = 0, iMax = this.selection.length; i < iMax; i++) { - id = this.selection[i]; - this.nodes[id].unselect(); - } - - this.selection = []; - - for (i = 0, iMax = selection.length; i < iMax; i++) { - id = selection[i]; - - var node = this.nodes[id]; - if (!node) { - throw new RangeError('Node with id "' + id + '" not found'); - } - node.select(); - this.selection.push(id); - } - - this.redraw(); -}; - -/** - * Validate the selection: remove ids of nodes which no longer exist - * @private - */ -Graph.prototype._updateSelection = function () { - var i = 0; - while (i < this.selection.length) { - var id = this.selection[i]; - if (!this.nodes[id]) { - this.selection.splice(i, 1); - } - else { - i++; - } - } -}; /** * Temporary method to test calculating a hub value for the nodes @@ -997,7 +1103,6 @@ Graph.prototype._getConnectionCount = function(level) { return hubs; }; - /** * Set a new size for the graph * @param {string} width Width in pixels or percentage (for example '800px' @@ -1014,6 +1119,10 @@ Graph.prototype.setSize = function(width, height) { this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.height = this.frame.canvas.clientHeight; + + if (this.constants.navigation.enabled == true) { + this._relocateNavigation(); + } }; /** @@ -1059,7 +1168,6 @@ Graph.prototype._setNodes = function(nodes) { var ids = this.nodesData.getIds(); this._addNodes(ids); } - this._updateSelection(); }; @@ -1089,7 +1197,7 @@ Graph.prototype._addNodes = function(ids) { this.moving = true; } } - + this._updateNodeIndexList(); this._reconnectEdges(); this._updateValueRange(this.nodes); }; @@ -1120,7 +1228,7 @@ Graph.prototype._updateNodes = function(ids) { } } } - + this._updateNodeIndexList(); this._reconnectEdges(); this._updateValueRange(nodes); }; @@ -1136,7 +1244,7 @@ Graph.prototype._removeNodes = function(ids) { var id = ids[i]; delete nodes[id]; } - + this._updateNodeIndexList(); this._reconnectEdges(); this._updateSelection(); this._updateValueRange(nodes); @@ -1198,6 +1306,7 @@ Graph.prototype._setEdges = function(edges) { Graph.prototype._addEdges = function (ids) { var edges = this.edges, edgesData = this.edgesData; + for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; @@ -1206,7 +1315,7 @@ Graph.prototype._addEdges = function (ids) { oldEdge.disconnect(); } - var data = edgesData.get(id); + var data = edgesData.get(id, {"showInternalIds" : true}); edges[id] = new Edge(data, this, this.constants); } @@ -1338,7 +1447,6 @@ Graph.prototype.redraw = function() { */ Graph.prototype._redraw = function() { var ctx = this.frame.canvas.getContext('2d'); - // clear the canvas var w = this.frame.canvas.width; var h = this.frame.canvas.height; @@ -1349,11 +1457,25 @@ Graph.prototype._redraw = function() { ctx.translate(this.translation.x, this.translation.y); ctx.scale(this.scale, this.scale); - this._drawEdges(ctx); - this._drawNodes(ctx); + this.canvasTopLeft = { + "x": this._canvasToX(0), + "y": this._canvasToY(0) + }; + this.canvasBottomRight = { + "x": this._canvasToX(this.frame.canvas.clientWidth), + "y": this._canvasToY(this.frame.canvas.clientHeight) + }; + + this._doInAllSectors("_drawAllSectorNodes",ctx); + this._doInAllSectors("_drawEdges",ctx); + this._doInAllSectors("_drawNodes",ctx); // restore original scaling and translation ctx.restore(); + + if (this.constants.navigation.enabled == true) { + this._doInNavigationSector("_drawNodes",ctx,true); + } }; /** @@ -1398,6 +1520,7 @@ Graph.prototype._getTranslation = function() { Graph.prototype._setScale = function(scale) { this.scale = scale; }; + /** * Get the current scale of the graph * @return {Number} scale Scaling factor 1.0 is unscaled @@ -1451,26 +1574,37 @@ Graph.prototype._yToCanvas = function(y) { * Redraw all nodes * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] * @private */ -Graph.prototype._drawNodes = function(ctx) { +Graph.prototype._drawNodes = function(ctx,alwaysShow) { + if (alwaysShow === undefined) { + alwaysShow = false; + } + // first draw the unselected nodes var nodes = this.nodes; var selected = []; + for (var id in nodes) { if (nodes.hasOwnProperty(id)) { + nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight); if (nodes[id].isSelected()) { selected.push(id); } else { - nodes[id].draw(ctx); + if (nodes[id].inArea() || alwaysShow) { + nodes[id].draw(ctx); + } } } } // draw the selected nodes on top for (var s = 0, sMax = selected.length; s < sMax; s++) { - nodes[selected[s]].draw(ctx); + if (nodes[selected[s]].inArea() || alwaysShow) { + nodes[selected[s]].draw(ctx); + } } }; @@ -1485,6 +1619,7 @@ Graph.prototype._drawEdges = function(ctx) { for (var id in edges) { if (edges.hasOwnProperty(id)) { var edge = edges[id]; + edge.setScale(this.scale); if (edge.connected) { edges[id].draw(ctx); } @@ -1497,138 +1632,208 @@ Graph.prototype._drawEdges = function(ctx) { * @private */ Graph.prototype._doStabilize = function() { - var start = new Date(); + //var start = new Date(); // find stable position var count = 0; var vmin = this.constants.minVelocity; var stable = false; while (!stable && count < this.constants.maxIterations) { - this._calculateForces(); + this._initializeForceCalculation(); this._discreteStepNodes(); stable = !this._isMoving(vmin); count++; } + this.zoomToFit(); - var end = new Date(); + // var end = new Date(); // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup }; + +/** + * Before calculating the forces, we check if we need to cluster to keep up performance and we check + * if there is more than one node. If it is just one node, we dont calculate anything. + * + * @private + */ +Graph.prototype._initializeForceCalculation = function() { + // stop calculation if there is only one node + if (this.nodeIndices.length == 1) { + this.nodes[this.nodeIndices[0]]._setForce(0,0); + } + else { + // if there are too many nodes on screen, we cluster without repositioning + if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { + this.clusterToFit(this.constants.clustering.reduceToNodes, false); + } + + // we now start the force calculation + this._calculateForces(); + } +}; + + /** * Calculate the external forces acting on the nodes * Forces are caused by: edges, repulsing forces between nodes, gravity * @private */ Graph.prototype._calculateForces = function() { +// var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)), +// "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))} // create a local edge to the nodes and edges, that is faster - var id, dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - nodes = this.nodes, - edges = this.edges; + var dx, dy, angle, distance, fx, fy, + repulsingForce, springForce, length, edgeLength, + node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; + var clusterSize; + var nodes = this.nodes; + var edges = this.edges; - // gravity, add a small constant force to pull the nodes towards the center of - // the graph - // Also, the forces are reset to zero in this loop by using _setForce instead + // Gravity is required to keep separated groups from floating off + // the forces are reset to zero in this loop by using _setForce instead // of _addForce - var gravity = 0.01, - gx = this.frame.canvas.clientWidth / 2, - gy = this.frame.canvas.clientHeight / 2; - for (id in nodes) { - if (nodes.hasOwnProperty(id)) { - var node = nodes[id]; - dx = gx - node.x; - dy = gy - node.y; - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; + var gravity = 0.08 * this.forceFactor; + for (i = 0; i < this.nodeIndices.length; i++) { + node = nodes[this.nodeIndices[i]]; + // gravity does not apply when we are in a pocket sector + if (this._sector() == "default") { + dx = -node.x;// + screenCenterPos.x; + dy = -node.y;// + screenCenterPos.y; + angle = Math.atan2(dy, dx); + fx = Math.cos(angle) * gravity; + fy = Math.sin(angle) * gravity; + } + else { + fx = 0; + fy = 0; + } node._setForce(fx, fy); - } + + node.updateDamping(this.nodeIndices.length); } // repulsing forces between nodes var minimumDistance = this.constants.nodes.distance, steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - for (var id1 in nodes) { - if (nodes.hasOwnProperty(id1)) { - var node1 = nodes[id1]; - for (var id2 in nodes) { - if (nodes.hasOwnProperty(id2)) { - var node2 = nodes[id2]; - // calculate normally distributed force - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); + // 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 < this.nodeIndices.length-1; i++) { + node1 = nodes[this.nodeIndices[i]]; + for (j = i+1; j < this.nodeIndices.length; j++) { + node2 = nodes[this.nodeIndices[j]]; + clusterSize = (node1.clusterSize + node2.clusterSize - 2); + dx = node2.x - node1.x; + dy = node2.y - node1.y; + distance = Math.sqrt(dx * dx + dy * dy); + + + // clusters have a larger region of influence + minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); + + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; + } + else { // TODO: correct factor for repulsing force //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; - - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); } + // amplify the repulsion for clusters. + repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; + repulsingForce *= this.forceFactor; + + + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce ; + + node1._addForce(-fx, -fy); + node2._addForce(fx, fy); } } } - /* TODO: re-implement repulsion of edges - for (var n = 0; n < nodes.length; n++) { - for (var l = 0; l < edges.length; l++) { - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, +/* + // repulsion of the edges on the nodes and + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + node = nodes[nodeId]; + for(var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; - // calculate normally distributed force - dx = nodes[n].x - lx, - dy = nodes[n].y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), + // get the center of the edge + xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; + yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; + // calculate normally distributed force + dx = node.x - xCenter; + dy = node.y - yCenter; + distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - nodes[n]._addForce(fx, fy); - edges[l].from._addForce(-fx/2,-fy/2); - edges[l].to._addForce(-fx/2,-fy/2); - } - } - */ + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; + } + else { + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force + } + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; + node._addForce(fx, fy); + edge.from._addForce(-fx/2,-fy/2); + edge.to._addForce(-fx/2,-fy/2); + } + } + } + } + } +*/ // forces caused by the edges, modelled as springs - for (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; + for (edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; if (edge.connected) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin - //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin - //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; - edgeLength = edge.length; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); + // only calculate forces if nodes are in the same sector + if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { + clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; + edgeLength = edge.length; + // this implies that the edges between big clusters are longer + edgeLength += clusterSize * this.constants.clustering.edgeGrowth; + length = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); - springForce = edge.stiffness * (edgeLength - length); + springForce = edge.stiffness * (edgeLength - length) * this.forceFactor; - fx = Math.cos(angle) * springForce; - fy = Math.sin(angle) * springForce; + fx = Math.cos(angle) * springForce; + fy = Math.sin(angle) * springForce; - edge.from._addForce(-fx, -fy); - edge.to._addForce(fx, fy); + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); + } } } } +/* + // TODO: re-implement repulsion of edges - /* TODO: re-implement repulsion of edges // repulsing forces between edges var minimumDistance = this.constants.edges.distance, steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance @@ -1663,7 +1868,7 @@ Graph.prototype._calculateForces = function() { edges[l2].to._addForce(fx, fy); } } - */ +*/ }; @@ -1674,10 +1879,10 @@ Graph.prototype._calculateForces = function() { * @private */ Graph.prototype._isMoving = function(vmin) { - // TODO: ismoving does not work well: should check the kinetic energy, not its velocity + var vminCorrected = vmin / this.scale; var nodes = this.nodes; for (var id in nodes) { - if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) { + if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) { return true; } } @@ -1686,53 +1891,221 @@ Graph.prototype._isMoving = function(vmin) { /** + * /** * Perform one discrete step for all nodes + * + * @param interval * @private */ Graph.prototype._discreteStepNodes = function() { - var interval = this.refreshRate / 1000.0; // in seconds + var interval = 0.01; var nodes = this.nodes; for (var id in nodes) { if (nodes.hasOwnProperty(id)) { nodes[id].discreteStep(interval); } } + + var vmin = this.constants.minVelocity; + this.moving = this._isMoving(vmin); }; + + /** * Start animating nodes and edges + * + * @poram {Boolean} runCalculationStep */ Graph.prototype.start = function() { + if (!this.freezeSimulation) { + + if (this.moving) { + this._doInAllActiveSectors("_initializeForceCalculation"); + this._doInAllActiveSectors("_discreteStepNodes"); + this._findCenter(this._getRange()) + } + + if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) { + // start animation. only start calculationTimer if it is not already running + if (!this.timer) { + var graph = this; + this.timer = window.setTimeout(function () { + graph.timer = undefined; + + // keyboad movement + if (graph.xIncrement != 0 || graph.yIncrement != 0) { + var translation = graph._getTranslation(); + graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement); + } + if (graph.zoomIncrement != 0) { + var center = { + x: graph.frame.canvas.clientWidth / 2, + y: graph.frame.canvas.clientHeight / 2 + }; + graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); + } + + graph.start(); + graph._redraw(); + + //this.end = window.performance.now(); + //this.time = this.end - this.startTime; + //console.log('refresh time: ' + this.time); + //this.startTime = window.performance.now(); + + }, this.renderTimestep); + } + } + else { + this._redraw(); + } + } +}; + + + + +Graph.prototype.singleStep = function() { if (this.moving) { - this._calculateForces(); + this._initializeForceCalculation(); this._discreteStepNodes(); var vmin = this.constants.minVelocity; this.moving = this._isMoving(vmin); + this._redraw(); } +}; - if (this.moving) { - // start animation. only start timer if it is not already running - if (!this.timer) { - var graph = this; - this.timer = window.setTimeout(function () { - graph.timer = undefined; - graph.start(); - graph._redraw(); - }, this.refreshRate); - } + + +/** + * Freeze the animation + */ +Graph.prototype.toggleFreeze = function() { + if (this.freezeSimulation == false) { + this.freezeSimulation = true; } else { - this._redraw(); + this.freezeSimulation = false; + this.start(); } }; /** - * Stop animating nodes and edges. + * Mixin the cluster system and initialize the parameters required. + * + * @private + */ +Graph.prototype._loadClusterSystem = function() { + this.clusterSession = 0; + this.hubThreshold = 5; + + for (var mixinFunction in ClusterMixin) { + if (ClusterMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction]; + } + } +} + +/** + * Mixin the sector system and initialize the parameters required + * + * @private */ -Graph.prototype.stop = function () { - if (this.timer) { - window.clearInterval(this.timer); - this.timer = undefined; +Graph.prototype._loadSectorSystem = function() { + this.sectors = {}; + this.activeSector = ["default"]; + this.sectors["active"] = {}; + this.sectors["active"]["default"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; + this.sectors["frozen"] = {}; + this.sectors["navigation"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; + + this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields + for (var mixinFunction in SectorMixin) { + if (SectorMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = SectorMixin[mixinFunction]; + } } }; + + +/** + * Mixin the selection system and initialize the parameters required + * + * @private + */ +Graph.prototype._loadSelectionSystem = function() { + this.selection = []; + this.selectionObj = {}; + + for (var mixinFunction in SelectionMixin) { + if (SelectionMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction]; + } + } +} + + +/** + * Mixin the navigation (User Interface) system and initialize the parameters required + * + * @private + */ +Graph.prototype._loadNavigationControls = function() { + for (var mixinFunction in NavigationMixin) { + if (NavigationMixin.hasOwnProperty(mixinFunction)) { + Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction]; + } + } + + if (this.constants.navigation.enabled == true) { + this._loadNavigationElements(); + } +} + +/** + * this function exists to avoid errors when not loading the navigation system + */ +Graph.prototype._relocateNavigation = function() { + // empty, is overloaded by navigation system +} + +/** + * * this function exists to avoid errors when not loading the navigation system + */ +Graph.prototype._unHighlightAll = function() { + // empty, is overloaded by the navigation system +} + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/graph/NavigationMixin.js b/src/graph/NavigationMixin.js new file mode 100644 index 00000000..20540622 --- /dev/null +++ b/src/graph/NavigationMixin.js @@ -0,0 +1,245 @@ +/** + * Created by Alex on 1/22/14. + */ + +var NavigationMixin = { + + /** + * This function moves the navigation controls if the canvas size has been changed. If the arugments + * verticaAlignTop and horizontalAlignLeft are false, the correction will be made + * + * @private + */ + _relocateNavigation : function() { + if (this.sectors !== undefined) { + var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth; + var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight; + this.navigationClientWidth = this.frame.canvas.clientWidth; + this.navigationClientHeight = this.frame.canvas.clientHeight; + var node = null; + + for (var nodeId in this.sectors["navigation"]["nodes"]) { + if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) { + node = this.sectors["navigation"]["nodes"][nodeId]; + if (!node.horizontalAlignLeft) { + node.x -= xOffset; + } + if (!node.verticalAlignTop) { + node.y -= yOffset; + } + } + } + } + }, + + + /** + * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation + * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent + * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. + * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. + * + * @private + */ + _loadNavigationElements : function() { + var DIR = this.constants.navigation.iconPath; + this.navigationClientWidth = this.frame.canvas.clientWidth; + this.navigationClientHeight = this.frame.canvas.clientHeight; + if (this.navigationClientWidth === undefined) { + this.navigationClientWidth = 0; + this.navigationClientHeight = 0; + } + var offset = 15; + var intermediateOffset = 7; + var navigationNodes = [ + {id: 'navigation_up', shape: 'image', image: DIR + '/uparrow.png', triggerFunction: "_moveUp", + verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}, + {id: 'navigation_down', shape: 'image', image: DIR + '/downarrow.png', triggerFunction: "_moveDown", + verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_left', shape: 'image', image: DIR + '/leftarrow.png', triggerFunction: "_moveLeft", + verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_right', shape: 'image', image: DIR + '/rightarrow.png',triggerFunction: "_moveRight", + verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset}, + + {id: 'navigation_plus', shape: 'image', image: DIR + '/plus.png', triggerFunction: "_zoomIn", + verticalAlignTop: false, horizontalAlignLeft: false, + x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_min', shape: 'image', image: DIR + '/minus.png', triggerFunction: "_zoomOut", + verticalAlignTop: false, horizontalAlignLeft: false, + x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset}, + {id: 'navigation_zoomExtends', shape: 'image', image: DIR + '/zoomExtends.png', triggerFunction: "zoomToFit", + verticalAlignTop: false, horizontalAlignLeft: false, + x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset} + ]; + + var nodeObj = null; + for (var i = 0; i < navigationNodes.length; i++) { + nodeObj = this.sectors["navigation"]['nodes']; + nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[i], this.images, this.groups, this.constants); + } + }, + + + /** + * By setting the clustersize to be larger than 1, we use the clustering drawing method + * to illustrate the buttons are presed. We call this highlighting. + * + * @param {String} elementId + * @private + */ + _highlightNavigationElement : function(elementId) { + if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { + this.sectors["navigation"]["nodes"][elementId].clusterSize = 2; + } + }, + + + /** + * Reverting back to a normal button + * + * @param {String} elementId + * @private + */ + _unHighlightNavigationElement : function(elementId) { + if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) { + this.sectors["navigation"]["nodes"][elementId].clusterSize = 1; + } + }, + + /** + * un-highlight (for lack of a better term) all navigation controls elements + * @private + */ + _unHighlightAll : function() { + for (var nodeId in this.sectors['navigation']['nodes']) { + if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) { + this._unHighlightNavigationElement(nodeId); + } + } + }, + + + _preventDefault : function(event) { + if (event !== undefined) { + if (event.preventDefault) { + event.preventDefault(); + } else { + event.returnValue = false; + } + } + }, + + + /** + * move the screen up + * By using the increments, instead of adding a fixed number to the translation, we keep fluent and + * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently + * To avoid this behaviour, we do the translation in the start loop. + * + * @private + */ + _moveUp : function(event) { + this._highlightNavigationElement("navigation_up"); + this.yIncrement = this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * move the screen down + * @private + */ + _moveDown : function(event) { + this._highlightNavigationElement("navigation_down"); + this.yIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * move the screen left + * @private + */ + _moveLeft : function(event) { + this._highlightNavigationElement("navigation_left"); + this.xIncrement = this.constants.keyboard.speed.x; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * move the screen right + * @private + */ + _moveRight : function(event) { + this._highlightNavigationElement("navigation_right"); + this.xIncrement = -this.constants.keyboard.speed.y; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * Zoom in, using the same method as the movement. + * @private + */ + _zoomIn : function(event) { + this._highlightNavigationElement("navigation_plus"); + this.zoomIncrement = this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * Zoom out + * @private + */ + _zoomOut : function() { + this._highlightNavigationElement("navigation_min"); + this.zoomIncrement = -this.constants.keyboard.speed.zoom; + this.start(); // if there is no node movement, the calculation wont be done + this._preventDefault(event); + }, + + + /** + * Stop zooming and unhighlight the zoom controls + * @private + */ + _stopZoom : function() { + this._unHighlightNavigationElement("navigation_plus"); + this._unHighlightNavigationElement("navigation_min"); + + this.zoomIncrement = 0; + }, + + + /** + * Stop moving in the Y direction and unHighlight the up and down + * @private + */ + _yStopMoving : function() { + this._unHighlightNavigationElement("navigation_up"); + this._unHighlightNavigationElement("navigation_down"); + + this.yIncrement = 0; + }, + + + /** + * Stop moving in the X direction and unHighlight left and right. + * @private + */ + _xStopMoving : function() { + this._unHighlightNavigationElement("navigation_left"); + this._unHighlightNavigationElement("navigation_right"); + + this.xIncrement = 0; + } + + +}; diff --git a/src/graph/Node.js b/src/graph/Node.js index 934c1399..513aaeb8 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -26,6 +26,8 @@ function Node(properties, imagelist, grouplist, constants) { this.selected = false; this.edges = []; // all edges connected to this node + this.dynamicEdges = []; + this.reroutedEdges = {}; this.group = constants.nodes.group; this.fontSize = constants.nodes.fontSize; @@ -42,24 +44,53 @@ function Node(properties, imagelist, grouplist, constants) { this.y = 0; this.xFixed = false; this.yFixed = false; + this.horizontalAlignLeft = true; // these are for the navigation controls + this.verticalAlignTop = true; // these are for the navigation controls this.radius = constants.nodes.radius; + this.baseRadiusValue = constants.nodes.radius; this.radiusFixed = false; this.radiusMin = constants.nodes.radiusMin; this.radiusMax = constants.nodes.radiusMax; this.imagelist = imagelist; + this.grouplist = grouplist; this.setProperties(properties, constants); + // creating the variables for clustering + this.resetCluster(); + this.dynamicEdgesLength = 0; + this.clusterSession = 0; + this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width; + this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height; + this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius; + // mass, force, velocity - this.mass = 50; // kg (mass is adjusted for the number of connected edges) + this.mass = 1; // kg (mass is adjusted for the number of connected edges) this.fx = 0.0; // external force x this.fy = 0.0; // external force y this.vx = 0.0; // velocity x this.vy = 0.0; // velocity y this.minForce = constants.minForce; - this.damping = 0.9; // damping factor + this.damping = 0.9; + this.dampingFactor = 75; + + this.graphScaleInv = 1; + this.canvasTopLeft = {"x": -300, "y": -300}; + this.canvasBottomRight = {"x": 300, "y": 300}; +} + +/** + * (re)setting the clustering variables and objects + */ +Node.prototype.resetCluster = function() { + // clustering variables + this.formationScale = undefined; // this is used to determine when to open the cluster + this.clusterSize = 1; // this signifies the total amount of nodes in this cluster + this.containedNodes = {}; + this.containedEdges = {}; + this.clusterSessions = []; }; /** @@ -70,6 +101,10 @@ Node.prototype.attachEdge = function(edge) { if (this.edges.indexOf(edge) == -1) { this.edges.push(edge); } + if (this.dynamicEdges.indexOf(edge) == -1) { + this.dynamicEdges.push(edge); + } + this.dynamicEdgesLength = this.dynamicEdges.length; this._updateMass(); }; @@ -81,7 +116,9 @@ Node.prototype.detachEdge = function(edge) { var index = this.edges.indexOf(edge); if (index != -1) { this.edges.splice(index, 1); + this.dynamicEdges.splice(index, 1); } + this.dynamicEdgesLength = this.dynamicEdges.length; this._updateMass(); }; @@ -91,7 +128,7 @@ Node.prototype.detachEdge = function(edge) { * @private */ Node.prototype._updateMass = function() { - this.mass = 50 + 20 * this.edges.length; // kg + this.mass = 1 + 0.6 * this.edges.length; // kg }; /** @@ -103,15 +140,20 @@ Node.prototype.setProperties = function(properties, constants) { if (!properties) { return; } - + this.originalLabel = undefined; // basic properties - if (properties.id != undefined) {this.id = properties.id;} - if (properties.label != undefined) {this.label = properties.label;} - if (properties.title != undefined) {this.title = properties.title;} - if (properties.group != undefined) {this.group = properties.group;} - if (properties.x != undefined) {this.x = properties.x;} - if (properties.y != undefined) {this.y = properties.y;} - if (properties.value != undefined) {this.value = properties.value;} + if (properties.id !== undefined) {this.id = properties.id;} + if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;} + if (properties.title !== undefined) {this.title = properties.title;} + if (properties.group !== undefined) {this.group = properties.group;} + if (properties.x !== undefined) {this.x = properties.x;} + if (properties.y !== undefined) {this.y = properties.y;} + if (properties.value !== undefined) {this.value = properties.value;} + + // navigation controls properties + if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} + if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} + if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;} if (this.id === undefined) { throw "Node must have an id"; @@ -128,17 +170,16 @@ Node.prototype.setProperties = function(properties, constants) { } // individual shape properties - if (properties.shape != undefined) {this.shape = properties.shape;} - if (properties.image != undefined) {this.image = properties.image;} - if (properties.radius != undefined) {this.radius = properties.radius;} - if (properties.color != undefined) {this.color = Node.parseColor(properties.color);} - - if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;} - if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;} - if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;} + if (properties.shape !== undefined) {this.shape = properties.shape;} + if (properties.image !== undefined) {this.image = properties.image;} + if (properties.radius !== undefined) {this.radius = properties.radius;} + if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);} + if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;} + if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;} + if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;} - if (this.image != undefined) { + if (this.image !== undefined) { if (this.imagelist) { this.imageObj = this.imagelist.load(this.image); } @@ -147,9 +188,9 @@ Node.prototype.setProperties = function(properties, constants) { } } - this.xFixed = this.xFixed || (properties.x != undefined); - this.yFixed = this.yFixed || (properties.y != undefined); - this.radiusFixed = this.radiusFixed || (properties.radius != undefined); + this.xFixed = this.xFixed || (properties.x !== undefined); + this.yFixed = this.yFixed || (properties.y !== undefined); + this.radiusFixed = this.radiusFixed || (properties.radius !== undefined); if (this.shape == 'image') { this.radiusMin = constants.nodes.widthMin; @@ -172,7 +213,6 @@ Node.prototype.setProperties = function(properties, constants) { case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break; default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break; } - // reset the size of the node, this can be changed this._reset(); }; @@ -200,6 +240,7 @@ Node.parseColor = function(color) { c = {}; c.background = color.background || 'white'; c.border = color.border || c.background; + if (util.isString(color.highlight)) { c.highlight = { border: color.highlight, @@ -212,6 +253,7 @@ Node.parseColor = function(color) { c.highlight.border = color.highlight && color.highlight.border || c.border; } } + return c; }; @@ -231,6 +273,14 @@ Node.prototype.unselect = function() { this._reset(); }; + +/** + * Reset the calculated size of the node, forces it to recalculate its size + */ +Node.prototype.clearSizeCache = function() { + this._reset(); +}; + /** * Reset the calculated size of the node, forces it to recalculate its size * @private @@ -327,15 +377,15 @@ Node.prototype.discreteStep = function(interval) { if (!this.xFixed) { var dx = -this.damping * this.vx; // damping force var ax = (this.fx + dx) / this.mass; // acceleration - this.vx += ax / interval; // velocity - this.x += this.vx / interval; // position + this.vx += ax * interval; // velocity + this.x += this.vx * interval; // position } if (!this.yFixed) { var dy = -this.damping * this.vy; // damping force var ay = (this.fy + dy) / this.mass; // acceleration - this.vy += ay / interval; // velocity - this.y += this.vy / interval; // position + this.vy += ay * interval; // velocity + this.y += this.vy * interval; // position } }; @@ -355,9 +405,16 @@ Node.prototype.isFixed = function() { */ // TODO: replace this method with calculating the kinetic energy Node.prototype.isMoving = function(vmin) { - return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin || - (!this.xFixed && Math.abs(this.fx) > this.minForce) || - (!this.yFixed && Math.abs(this.fy) > this.minForce)); + + if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) { +// console.log(vmin,this.vx,this.vy); + return true; + } + else { + this.vx = 0; this.vy = 0; + return false; + } + //return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin); }; /** @@ -405,6 +462,7 @@ Node.prototype.setValueRange = function(min, max) { this.radius = (this.value - min) * scale + this.radiusMin; } } + this.baseRadiusValue = this.radius; }; /** @@ -431,20 +489,28 @@ Node.prototype.resize = function(ctx) { * @return {boolean} True if location is located on node */ Node.prototype.isOverlappingWith = function(obj) { - return (this.left < obj.right && - this.left + this.width > obj.left && - this.top < obj.bottom && - this.top + this.height > obj.top); + return (this.left < obj.right && + this.left + this.width > obj.left && + this.top < obj.bottom && + this.top + this.height > obj.top); }; Node.prototype._resizeImage = function (ctx) { // TODO: pre calculate the image size - if (!this.width) { // undefined or 0 + + if (!this.width || !this.height) { // undefined or 0 var width, height; if (this.value) { + this.radius = this.baseRadiusValue; var scale = this.imageObj.height / this.imageObj.width; - width = this.radius || this.imageObj.width; - height = this.radius * scale || this.imageObj.height; + if (scale !== undefined) { + width = this.radius || this.imageObj.width; + height = this.radius * scale || this.imageObj.height; + } + else { + width = 0; + height = 0; + } } else { width = this.imageObj.width; @@ -452,7 +518,14 @@ Node.prototype._resizeImage = function (ctx) { } this.width = width; this.height = height; + + if (this.width > 0 && this.height > 0) { + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; + } } + }; Node.prototype._drawImage = function (ctx) { @@ -462,7 +535,19 @@ Node.prototype._drawImage = function (ctx) { this.top = this.y - this.height / 2; var yLabel; - if (this.imageObj) { + if (this.imageObj.width != 0 ) { + // draw the shade + if (this.clusterSize > 1) { + var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0); + lineWidth *= this.graphScaleInv; + lineWidth = Math.min(0.2 * this.width,lineWidth); + + ctx.globalAlpha = 0.5; + ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth); + } + + // draw the image + ctx.globalAlpha = 1.0; ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); yLabel = this.y + this.height / 2; } @@ -481,6 +566,10 @@ Node.prototype._resizeBox = function (ctx) { var textSize = this.getTextSize(ctx); this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; + + this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; +// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -490,9 +579,26 @@ Node.prototype._drawBox = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.roundRect(this.left, this.top, this.width, this.height, this.radius); ctx.fill(); ctx.stroke(); @@ -508,6 +614,11 @@ Node.prototype._resizeDatabase = function (ctx) { var size = textSize.width + 2 * margin; this.width = size; this.height = size; + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -516,9 +627,25 @@ Node.prototype._drawDatabase = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); ctx.fill(); ctx.stroke(); @@ -536,6 +663,11 @@ Node.prototype._resizeCircle = function (ctx) { this.width = diameter; this.height = diameter; + + // scaling used for clustering +// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; +// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -544,9 +676,25 @@ Node.prototype._drawCircle = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; ctx.circle(this.x, this.y, this.radius); ctx.fill(); ctx.stroke(); @@ -563,6 +711,11 @@ Node.prototype._resizeEllipse = function (ctx) { if (this.width < this.height) { this.width = this.height; } + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -571,13 +724,29 @@ Node.prototype._drawEllipse = function (ctx) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; + ctx.ellipse(this.left, this.top, this.width, this.height); ctx.fill(); ctx.stroke(); - this._label(ctx, this.label, this.x, this.y); }; @@ -603,9 +772,15 @@ Node.prototype._drawStar = function (ctx) { Node.prototype._resizeShape = function (ctx) { if (!this.width) { + this.radius = this.baseRadiusValue; var size = 2 * this.radius; this.width = size; this.height = size; + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -615,9 +790,35 @@ Node.prototype._drawShape = function (ctx, shape) { this.left = this.x - this.width / 2; this.top = this.y - this.height / 2; + var clusterLineWidth = 2.5; + var selectionLineWidth = 2; + var radiusMultiplier = 2; + + // choose draw method depending on the shape + switch (shape) { + case 'dot': radiusMultiplier = 2; break; + case 'square': radiusMultiplier = 2; break; + case 'triangle': radiusMultiplier = 3; break; + case 'triangleDown': radiusMultiplier = 3; break; + case 'star': radiusMultiplier = 4; break; + } + ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border; + + // draw the outer border + if (this.clusterSize > 1) { + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + + ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth); + ctx.stroke(); + } + ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0); + ctx.lineWidth *= this.graphScaleInv; + ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth); + ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background; - ctx.lineWidth = this.selected ? 2.0 : 1.0; ctx[shape](this.x, this.y, this.radius); ctx.fill(); @@ -634,6 +835,11 @@ Node.prototype._resizeText = function (ctx) { var textSize = this.getTextSize(ctx); this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; + + // scaling used for clustering + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -667,7 +873,7 @@ Node.prototype._label = function (ctx, text, x, y, align, baseline) { Node.prototype.getTextSize = function(ctx) { - if (this.label != undefined) { + if (this.label !== undefined) { ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace; var lines = this.label.split('\n'), @@ -684,3 +890,89 @@ Node.prototype.getTextSize = function(ctx) { return {"width": 0, "height": 0}; } }; + +/** + * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn. + * there is a safety margin of 0.3 * width; + * + * @returns {boolean} + */ +Node.prototype.inArea = function() { + if (this.width !== undefined) { + return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x && + this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x && + this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y && + this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y); + } + else { + return true; + } +} + +/** + * checks if the core of the node is in the display area, this is used for opening clusters around zoom + * @returns {boolean} + */ +Node.prototype.inView = function() { + return (this.x >= this.canvasTopLeft.x && + this.x < this.canvasBottomRight.x && + this.y >= this.canvasTopLeft.y && + this.y < this.canvasBottomRight.y); +} + +/** + * This allows the zoom level of the graph to influence the rendering + * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas + * + * @param scale + * @param canvasTopLeft + * @param canvasBottomRight + */ +Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) { + this.graphScaleInv = 1.0/scale; + this.canvasTopLeft = canvasTopLeft; + this.canvasBottomRight = canvasBottomRight; +}; + + +/** + * This allows the zoom level of the graph to influence the rendering + * + * @param scale + */ +Node.prototype.setScale = function(scale) { + this.graphScaleInv = 1.0/scale; +}; + +/** + * This function updates the damping parameter for clusters, based ont he + * + * @param {Number} numberOfNodes + */ +Node.prototype.updateDamping = function(numberOfNodes) { + this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2))); + this.damping *= this.dampingFactor; +}; + + +/** + * set the velocity at 0. Is called when this node is contained in another during clustering + */ +Node.prototype.clearVelocity = function() { + this.vx = 0; + this.vy = 0; +}; + + +/** + * Basic preservation of (kinectic) energy + * + * @param massBeforeClustering + */ +Node.prototype.updateVelocity = function(massBeforeClustering) { + var energyBefore = this.vx * this.vx * massBeforeClustering; + this.vx = Math.sqrt(energyBefore/this.mass); + energyBefore = this.vy * this.vy * massBeforeClustering; + this.vy = Math.sqrt(energyBefore/this.mass); +}; + diff --git a/src/graph/Popup.js b/src/graph/Popup.js index 7dc8ffe8..ab626fd4 100644 --- a/src/graph/Popup.js +++ b/src/graph/Popup.js @@ -38,7 +38,7 @@ function Popup(container, x, y, text) { style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)"; style.whiteSpace = "nowrap"; this.container.appendChild(this.frame); -}; +} /** * @param {number} x Horizontal position of the popup window diff --git a/src/graph/SectorsMixin.js b/src/graph/SectorsMixin.js new file mode 100644 index 00000000..f242dcfb --- /dev/null +++ b/src/graph/SectorsMixin.js @@ -0,0 +1,547 @@ +/** + * Creation of the SectorMixin var. + * + * This contains all the functions the Graph object can use to employ the sector system. + * The sector system is always used by Graph, though the benefits only apply to the use of clustering. + * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. + * + * Alex de Mulder + * 21-01-2013 + */ +var SectorMixin = { + + /** + * This function is only called by the setData function of the Graph object. + * This loads the global references into the active sector. This initializes the sector. + * + * @private + */ + _putDataInSector : function() { + this.sectors["active"][this._sector()].nodes = this.nodes; + this.sectors["active"][this._sector()].edges = this.edges; + this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; + }, + + + /** + * /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied (active) sector. If a type is defined, do the specific type + * + * @param {String} sectorId + * @param {String} [sectorType] | "active" or "frozen" + * @private + */ + _switchToSector : function(sectorId, sectorType) { + if (sectorType === undefined || sectorType == "active") { + this._switchToActiveSector(sectorId); + } + else { + this._switchToFrozenSector(sectorId); + } + }, + + + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied active sector. + * + * @param sectorId + * @private + */ + _switchToActiveSector : function(sectorId) { + this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["active"][sectorId]["nodes"]; + this.edges = this.sectors["active"][sectorId]["edges"]; + }, + + + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the supplied frozen sector. + * + * @param sectorId + * @private + */ + _switchToFrozenSector : function(sectorId) { + this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; + this.nodes = this.sectors["frozen"][sectorId]["nodes"]; + this.edges = this.sectors["frozen"][sectorId]["edges"]; + }, + + + /** + * This function sets the global references to nodes, edges and nodeIndices to + * those of the navigation controls sector. + * + * @private + */ + _switchToNavigationSector : function() { + this.nodeIndices = this.sectors["navigation"]["nodeIndices"]; + this.nodes = this.sectors["navigation"]["nodes"]; + this.edges = this.sectors["navigation"]["edges"]; + }, + + + /** + * This function sets the global references to nodes, edges and nodeIndices back to + * those of the currently active sector. + * + * @private + */ + _loadLatestSector : function() { + this._switchToSector(this._sector()); + }, + + + /** + * This function returns the currently active sector Id + * + * @returns {String} + * @private + */ + _sector : function() { + return this.activeSector[this.activeSector.length-1]; + }, + + + /** + * This function returns the previously active sector Id + * + * @returns {String} + * @private + */ + _previousSector : function() { + if (this.activeSector.length > 1) { + return this.activeSector[this.activeSector.length-2]; + } + else { + throw new TypeError('there are not enough sectors in the this.activeSector array.'); + } + }, + + + /** + * We add the active sector at the end of the this.activeSector array + * This ensures it is the currently active sector returned by _sector() and it reaches the top + * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. + * + * @param newId + * @private + */ + _setActiveSector : function(newId) { + this.activeSector.push(newId); + }, + + + /** + * We remove the currently active sector id from the active sector stack. This happens when + * we reactivate the previously active sector + * + * @private + */ + _forgetLastSector : function() { + this.activeSector.pop(); + }, + + + /** + * This function creates a new active sector with the supplied newId. This newId + * is the expanding node id. + * + * @param {String} newId | Id of the new active sector + * @private + */ + _createNewSector : function(newId) { + // create the new sector + this.sectors["active"][newId] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": this.scale, + "drawingNode": undefined}; + + // create the new sector render node. This gives visual feedback that you are in a new sector. + this.sectors["active"][newId]['drawingNode'] = new Node( + {id:newId, + color: { + background: "#eaefef", + border: "495c5e" + } + },{},{},this.constants); + this.sectors["active"][newId]['drawingNode'].clusterSize = 2; + }, + + + /** + * This function removes the currently active sector. This is called when we create a new + * active sector. + * + * @param {String} sectorId | Id of the active sector that will be removed + * @private + */ + _deleteActiveSector : function(sectorId) { + delete this.sectors["active"][sectorId]; + }, + + + /** + * This function removes the currently active sector. This is called when we reactivate + * the previously active sector. + * + * @param {String} sectorId | Id of the active sector that will be removed + * @private + */ + _deleteFrozenSector : function(sectorId) { + delete this.sectors["frozen"][sectorId]; + }, + + + /** + * Freezing an active sector means moving it from the "active" object to the "frozen" object. + * We copy the references, then delete the active entree. + * + * @param sectorId + * @private + */ + _freezeSector : function(sectorId) { + // we move the set references from the active to the frozen stack. + this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; + + // we have moved the sector data into the frozen set, we now remove it from the active set + this._deleteActiveSector(sectorId); + }, + + + /** + * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" + * object to the "active" object. + * + * @param sectorId + * @private + */ + _activateSector : function(sectorId) { + // we move the set references from the frozen to the active stack. + this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; + + // we have moved the sector data into the active set, we now remove it from the frozen stack + this._deleteFrozenSector(sectorId); + }, + + + /** + * This function merges the data from the currently active sector with a frozen sector. This is used + * in the process of reverting back to the previously active sector. + * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it + * upon the creation of a new active sector. + * + * @param sectorId + * @private + */ + _mergeThisWithFrozen : function(sectorId) { + // copy all nodes + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; + } + } + + // copy all edges (if not fully clustered, else there are no edges) + for (var edgeId in this.edges) { + if (this.edges.hasOwnProperty(edgeId)) { + this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; + } + } + + // merge the nodeIndices + for (var i = 0; i < this.nodeIndices.length; i++) { + this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); + } + }, + + + /** + * This clusters the sector to one cluster. It was a single cluster before this process started so + * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. + * + * @private + */ + _collapseThisToSingleCluster : function() { + this.clusterToFit(1,false); + }, + + + /** + * We create a new active sector from the node that we want to open. + * + * @param node + * @private + */ + _addSector : function(node) { + // this is the currently active sector + var sector = this._sector(); + +// // this should allow me to select nodes from a frozen set. +// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { +// console.log("the node is part of the active sector"); +// } +// else { +// console.log("I dont know what the fuck happened!!"); +// } + + // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. + delete this.nodes[node.id]; + + var unqiueIdentifier = util.randomUUID(); + + // we fully freeze the currently active sector + this._freezeSector(sector); + + // we create a new active sector. This sector has the Id of the node to ensure uniqueness + this._createNewSector(unqiueIdentifier); + + // we add the active sector to the sectors array to be able to revert these steps later on + this._setActiveSector(unqiueIdentifier); + + // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier + this._switchToSector(this._sector()); + + // finally we add the node we removed from our previous active sector to the new active sector + this.nodes[node.id] = node; + }, + + + /** + * We close the sector that is currently open and revert back to the one before. + * If the active sector is the "default" sector, nothing happens. + * + * @private + */ + _collapseSector : function() { + // the currently active sector + var sector = this._sector(); + + // we cannot collapse the default sector + if (sector != "default") { + if ((this.nodeIndices.length == 1) || + (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || + (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { + var previousSector = this._previousSector(); + + // we collapse the sector back to a single cluster + this._collapseThisToSingleCluster(); + + // we move the remaining nodes, edges and nodeIndices to the previous sector. + // This previous sector is the one we will reactivate + this._mergeThisWithFrozen(previousSector); + + // the previously active (frozen) sector now has all the data from the currently active sector. + // we can now delete the active sector. + this._deleteActiveSector(sector); + + // we activate the previously active (and currently frozen) sector. + this._activateSector(previousSector); + + // we load the references from the newly active sector into the global references + this._switchToSector(previousSector); + + // we forget the previously active sector because we reverted to the one before + this._forgetLastSector(); + + // finally, we update the node index list. + this._updateNodeIndexList(); + } + } + }, + + + /** + * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we dont pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInAllActiveSectors : function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + this[runFunction](); + } + } + } + else { + for (var sector in this.sectors["active"]) { + if (this.sectors["active"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToActiveSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + } + } + // we revert the global references back to our active sector + this._loadLatestSector(); + }, + + + /** + * This runs a function in all frozen sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInAllFrozenSectors : function(runFunction,argument) { + if (argument === undefined) { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + this[runFunction](); + } + } + } + else { + for (var sector in this.sectors["frozen"]) { + if (this.sectors["frozen"].hasOwnProperty(sector)) { + // switch the global references to those of this sector + this._switchToFrozenSector(sector); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + } + } + this._loadLatestSector(); + }, + + + /** + * This runs a function in the navigation controls sector. + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInNavigationSector : function(runFunction,argument) { + this._switchToNavigationSector(); + if (argument === undefined) { + this[runFunction](); + } + else { + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + this._loadLatestSector(); + }, + + + /** + * This runs a function in all sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't pass the function itself because then the "this" is the window object + * | instead of the Graph object + * @param {*} [argument] | Optional: arguments to pass to the runFunction + * @private + */ + _doInAllSectors : function(runFunction,argument) { + var args = Array.prototype.splice.call(arguments, 1); + if (argument === undefined) { + this._doInAllActiveSectors(runFunction); + this._doInAllFrozenSectors(runFunction); + } + else { + if (args.length > 1) { + this._doInAllActiveSectors(runFunction,args[0],args[1]); + this._doInAllFrozenSectors(runFunction,args[0],args[1]); + } + else { + this._doInAllActiveSectors(runFunction,argument); + this._doInAllFrozenSectors(runFunction,argument); + } + } + + }, + + + /** + * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the + * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. + * + * @private + */ + _clearNodeIndexList : function() { + var sector = this._sector(); + this.sectors["active"][sector]["nodeIndices"] = []; + this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; + }, + + + /** + * Draw the encompassing sector node + * + * @param ctx + * @param sectorType + * @private + */ + _drawSectorNodes : function(ctx,sectorType) { + var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; + for (var sector in this.sectors[sectorType]) { + if (this.sectors[sectorType].hasOwnProperty(sector)) { + if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { + + this._switchToSector(sector,sectorType); + + minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; + for (var nodeId in this.nodes) { + if (this.nodes.hasOwnProperty(nodeId)) { + node = this.nodes[nodeId]; + node.resize(ctx); + if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} + if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} + if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} + if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} + } + } + node = this.sectors[sectorType][sector]["drawingNode"]; + node.x = 0.5 * (maxX + minX); + node.y = 0.5 * (maxY + minY); + node.width = 2 * (node.x - minX); + node.height = 2 * (node.y - minY); + node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); + node.setScale(this.scale); + node._drawCircle(ctx); + } + } + } + }, + + _drawAllSectorNodes : function(ctx) { + this._drawSectorNodes(ctx,"frozen"); + this._drawSectorNodes(ctx,"active"); + this._loadLatestSector(); + } +}; \ No newline at end of file diff --git a/src/graph/SelectionMixin.js b/src/graph/SelectionMixin.js new file mode 100644 index 00000000..203ae6ac --- /dev/null +++ b/src/graph/SelectionMixin.js @@ -0,0 +1,515 @@ + +var SelectionMixin = { + + /** + * This function can be called from the _doInAllSectors function + * + * @param object + * @param overlappingNodes + * @private + */ + _getNodesOverlappingWith : function(object, overlappingNodes) { + var nodes = this.nodes; + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (nodes[nodeId].isOverlappingWith(object)) { + overlappingNodes.push(nodeId); + } + } + } + }, + + /** + * retrieve all nodes overlapping with given object + * @param {Object} object An object with parameters left, top, right, bottom + * @return {Number[]} An array with id's of the overlapping nodes + * @private + */ + _getAllNodesOverlappingWith : function (object) { + var overlappingNodes = []; + this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); + return overlappingNodes; + }, + + + /** + * retrieve all nodes in the navigation controls 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 + */ + _getAllNavigationNodesOverlappingWith : function (object) { + var overlappingNodes = []; + this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes); + return overlappingNodes; + }, + + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + _pointerToPositionObject : function(pointer) { + var x = this._canvasToX(pointer.x); + var y = this._canvasToY(pointer.y); + + return {left: x, + top: y, + right: x, + bottom: y}; + }, + + /** + * Return a position object in canvasspace from a single point in screenspace + * + * @param pointer + * @returns {{left: number, top: number, right: number, bottom: number}} + * @private + */ + _pointerToScreenPositionObject : function(pointer) { + var x = pointer.x; + var y = pointer.y; + + return {left: x, + top: y, + right: x, + bottom: y}; + }, + + + /** + * Get the top navigation controls node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node + * @private + */ + _getNavigationNodeAt : function (pointer) { + var screenPositionObject = this._pointerToScreenPositionObject(pointer); + var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject); + if (overlappingNodes.length > 0) { + return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]]; + } + else { + return null; + } + }, + + + /** + * Get the top node at the a specific point (like a click) + * + * @param {{x: Number, y: Number}} pointer + * @return {Node | null} node + * @private + */ + _getNodeAt : function (pointer) { + // we first check if this is an navigation controls element + var positionObject = this._pointerToPositionObject(pointer); + overlappingNodes = this._getAllNodesOverlappingWith(positionObject); + + // if there are overlapping nodes, select the last one, this is the + // one which is drawn on top of the others + if (overlappingNodes.length > 0) { + return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; + } + else { + return null; + } + }, + + + /** + * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call + * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. + * + * @param pointer + * @returns {null} + * @private + */ + _getEdgeAt : function(pointer) { + return null; + }, + + + /** + * Add object to the selection array. The this.selection id array may not be needed. + * + * @param obj + * @private + */ + _addToSelection : function(obj) { + this.selection.push(obj.id); + this.selectionObj[obj.id] = obj; + }, + + + /** + * Remove a single option from selection. + * + * @param obj + * @private + */ + _removeFromSelection : function(obj) { + for (var i = 0; i < this.selection.length; i++) { + if (obj.id == this.selection[i]) { + this.selection.splice(i,1); + break; + } + } + delete this.selectionObj[obj.id]; + }, + + + /** + * Unselect all. The selectionObj is useful for this. + * + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + _unselectAll : function(doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + + this.selection = []; + for (var objId in this.selectionObj) { + if (this.selectionObj.hasOwnProperty(objId)) { + this.selectionObj[objId].unselect(); + } + } + this.selectionObj = {}; + + if (doNotTrigger == false) { + this._trigger('select', { + nodes: this.getSelection() + }); + } + }, + + + /** + * Check if anything is selected + * + * @returns {boolean} + * @private + */ + _selectionIsEmpty : function() { + if (this.selection.length == 0) { + return true; + } + else { + return false; + } + }, + + + /** + * 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} node + * @param {Boolean} append + * @param {Boolean} [doNotTrigger] | ignore trigger + * @private + */ + _selectNode : function(node, append, doNotTrigger) { + if (doNotTrigger === undefined) { + doNotTrigger = false; + } + + if (this._selectionIsEmpty() == false && append == false) { + this._unselectAll(true); + } + + + if (node.selected == false) { + node.select(); + this._addToSelection(node); + } + else { + node.unselect(); + this._removeFromSelection(node); + } + if (doNotTrigger == false) { + this._trigger('select', { + nodes: this.getSelection() + }); + } + }, + + + /** + * handles the selection part of the touch, only for navigation controls elements; + * Touch is triggered before tap, also before hold. Hold triggers after a while. + * This is the most responsive solution + * + * @param {Object} pointer + * @private + */ + _handleTouch : function(pointer) { + if (this.constants.navigation.enabled == true) { + var node = this._getNavigationNodeAt(pointer); + if (node != null) { + if (this[node.triggerFunction] !== undefined) { + this[node.triggerFunction](); + } + } + } + }, + + + /** + * handles the selection part of the tap; + * + * @param {Object} pointer + * @private + */ + _handleTap : function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectNode(node,false); + } + else { + this._unselectAll(); + } + this._redraw(); + }, + + + /** + * handles the selection part of the double tap and opens a cluster if needed + * + * @param {Object} pointer + * @private + */ + _handleDoubleTap : function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null && node !== undefined) { + // we reset the areaCenter here so the opening of the node will occur + this.areaCenter = {"x" : this._canvasToX(pointer.x), + "y" : this._canvasToY(pointer.y)}; + this.openCluster(node); + } + }, + + + /** + * Handle the onHold selection part + * + * @param pointer + * @private + */ + _handleOnHold : function(pointer) { + var node = this._getNodeAt(pointer); + if (node != null) { + this._selectNode(node,true); + } + this._redraw(); + }, + + + /** + * handle the onRelease event. These functions are here for the navigation controls module. + * + * @private + */ + _handleOnRelease : function() { + this.xIncrement = 0; + this.yIncrement = 0; + this.zoomIncrement = 0; + this._unHighlightAll(); + }, + + + + /** + * + * retrieve the currently selected nodes + * @return {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + getSelection : function() { + return this.selection.concat([]); + }, + + /** + * + * retrieve the currently selected nodes as objects + * @return {Objects} selection An array with the ids of the + * selected nodes. + */ + getSelectionObjects : function() { + return this.selectionObj; + }, + + /** + * // TODO: rework this function, it is from the old system + * + * select zero or more nodes + * @param {Number[] | String[]} selection An array with the ids of the + * selected nodes. + */ + setSelection : function(selection) { + var i, iMax, id; + + if (!selection || (selection.length == undefined)) + throw 'Selection must be an array with ids'; + + // first unselect any selected node + this._unselectAll(true); + + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + + var node = this.nodes[id]; + if (!node) { + throw new RangeError('Node with id "' + id + '" not found'); + } + this._selectNode(node,true,true); + } + + this.redraw(); + }, + + + /** + * TODO: rework this function, it is from the old system + * + * Validate the selection: remove ids of nodes which no longer exist + * @private + */ + _updateSelection : function () { + var i = 0; + while (i < this.selection.length) { + var nodeId = this.selection[i]; + if (!this.nodes.hasOwnProperty(nodeId)) { + this.selection.splice(i, 1); + delete this.selectionObj[nodeId]; + } + else { + i++; + } + } + } + + + /** + * Unselect selected nodes. If no selection array is provided, all nodes + * are unselected + * @param {Object[]} selection Array with selection objects, each selection + * object has a parameter row. Optional + * @param {Boolean} triggerSelect If true (default), the select event + * is triggered when nodes are unselected + * @return {Boolean} changed True if the selection is changed + * @private + */ + /* _unselectNodes : function(selection, triggerSelect) { + var changed = false; + var i, iMax, id; + + if (selection) { + // remove provided selections + for (i = 0, iMax = selection.length; i < iMax; i++) { + id = selection[i]; + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } + var j = 0; + while (j < this.selection.length) { + if (this.selection[j] == id) { + this.selection.splice(j, 1); + changed = true; + } + else { + j++; + } + } + } + } + else if (this.selection && this.selection.length) { + // remove all selections + for (i = 0, iMax = this.selection.length; i < iMax; i++) { + id = this.selection[i]; + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } + changed = true; + } + this.selection = []; + } + + if (changed && (triggerSelect == true || triggerSelect == undefined)) { + // fire the select event + this._trigger('select', { + nodes: this.getSelection() + }); + } + + return changed; + }, +*/ +/** + * select all nodes on given location x, y + * @param {Array} selection an array with node ids + * @param {boolean} append If true, the new selection will be appended to the + * current selection (except for duplicate entries) + * @return {Boolean} changed True if the selection is changed + * @private + */ +/* _selectNodes : function(selection, append) { + var changed = false; + var i, iMax; + + // TODO: the selectNodes method is a little messy, rework this + + // check if the current selection equals the desired selection + var selectionAlreadyThere = true; + if (selection.length != this.selection.length) { + selectionAlreadyThere = false; + } + else { + for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) { + if (selection[i] != this.selection[i]) { + selectionAlreadyThere = false; + break; + } + } + } + if (selectionAlreadyThere) { + return changed; + } + + if (append == undefined || append == false) { + // first deselect any selected node + var triggerSelect = false; + changed = this._unselectNodes(undefined, triggerSelect); + } + + for (i = 0, iMax = selection.length; i < iMax; i++) { + // add each of the new selections, but only when they are not duplicate + var id = selection[i]; + var isDuplicate = (this.selection.indexOf(id) != -1); + if (!isDuplicate) { + this.nodes[id].select(); + this.selection.push(id); + changed = true; + } + } + + if (changed) { + // fire the select event + this._trigger('select', { + nodes: this.getSelection() + }); + } + + return changed; + }, + */ +}; + + + diff --git a/src/graph/img/downarrow.png b/src/graph/img/downarrow.png new file mode 100644 index 00000000..e77d5e6d Binary files /dev/null and b/src/graph/img/downarrow.png differ diff --git a/src/graph/img/leftarrow.png b/src/graph/img/leftarrow.png new file mode 100644 index 00000000..3823536e Binary files /dev/null and b/src/graph/img/leftarrow.png differ diff --git a/src/graph/img/minus.png b/src/graph/img/minus.png new file mode 100644 index 00000000..30698076 Binary files /dev/null and b/src/graph/img/minus.png differ diff --git a/src/graph/img/plus.png b/src/graph/img/plus.png new file mode 100644 index 00000000..f7ab2a33 Binary files /dev/null and b/src/graph/img/plus.png differ diff --git a/src/graph/img/rightarrow.png b/src/graph/img/rightarrow.png new file mode 100644 index 00000000..c3a209d8 Binary files /dev/null and b/src/graph/img/rightarrow.png differ diff --git a/src/graph/img/uparrow.png b/src/graph/img/uparrow.png new file mode 100644 index 00000000..8aedced7 Binary files /dev/null and b/src/graph/img/uparrow.png differ diff --git a/src/graph/img/zoomExtends.png b/src/graph/img/zoomExtends.png new file mode 100644 index 00000000..74595c63 Binary files /dev/null and b/src/graph/img/zoomExtends.png differ diff --git a/src/module/imports.js b/src/module/imports.js index a466d2e9..ea8d8c92 100644 --- a/src/module/imports.js +++ b/src/module/imports.js @@ -4,6 +4,7 @@ // Try to load dependencies from the global window object. // If not available there, load via require. + var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); var Hammer; @@ -16,3 +17,16 @@ else { throw Error('hammer.js is only available in a browser, not in node.js.'); } } + +var mousetrap; +if (typeof window !== 'undefined') { + // load mousetrap.js only when running in a browser (where window is available) + mousetrap = window['mousetrap'] || require('mousetrap'); +} +else { + mousetrap = function () { + throw Error('mouseTrap is only available in a browser, not in node.js.'); + } +} + + diff --git a/src/timeline/Range.js b/src/timeline/Range.js index b0f9bb7f..70c9083d 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -94,15 +94,30 @@ Range.prototype.subscribe = function (component, event, direction) { }; /** - * Event handler - * @param {String} event name of the event, for example 'click', 'mousemove' - * @param {function} callback callback handler, invoked with the raw HTML Event - * as parameter. + * Add event listener + * @param {String} event Name of the event. + * Available events: 'rangechange', 'rangechanged' + * @param {function} callback Callback function, invoked as callback({start: Date, end: Date}) */ -Range.prototype.on = function (event, callback) { +Range.prototype.on = function on (event, callback) { + var available = ['rangechange', 'rangechanged']; + + if (available.indexOf(event) == -1) { + throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); + } + events.addListener(this, event, callback); }; +/** + * Remove an event listener + * @param {String} event name of the event + * @param {function} callback callback handler + */ +Range.prototype.off = function off (event, callback) { + events.removeListener(this, event, callback); +}; + /** * Trigger an event * @param {String} event name of the event, available events: 'rangechange', @@ -139,8 +154,8 @@ Range.prototype.setRange = function(start, end) { * @private */ Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Number') : this.start, - newEnd = (end != null) ? util.convert(end, 'Number') : this.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; diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index ec122d74..38c19fae 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -83,18 +83,26 @@ function Timeline (container, items, options) { ); // TODO: reckon with options moveable and zoomable + // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable this.range.subscribe(this.rootPanel, 'move', 'horizontal'); this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); - this.range.on('rangechange', function () { + this.range.on('rangechange', function (properties) { var force = true; me.controller.requestReflow(force); + me._trigger('rangechange', properties); }); - this.range.on('rangechanged', function () { + this.range.on('rangechanged', function (properties) { var force = true; me.controller.requestReflow(force); + me._trigger('rangechanged', properties); }); - // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable + // single select (or unselect) when tapping an item + // TODO: implement ctrl+click + this.rootPanel.on('tap', this._onSelectItem.bind(this)); + + // multi select when holding mouse/touch, or on ctrl+click + this.rootPanel.on('hold', this._onMultiSelectItem.bind(this)); // time axis var timeaxisOptions = Object.create(rootOptions); @@ -139,10 +147,9 @@ function Timeline (container, items, options) { Timeline.prototype.setOptions = function (options) { util.extend(this.options, options); - // force update of range - // options.start and options.end can be undefined - //this.range.setRange(options.start, options.end); - this.range.setRange(); + // force update of range (apply new min/max etc.) + // both start and end are optional + this.range.setRange(options.start, options.end); this.controller.reflow(); this.controller.repaint(); @@ -198,29 +205,29 @@ Timeline.prototype.setItems = function(items) { var dataRange = this.getItemRange(); // add 5% space on both sides - var min = dataRange.min; - var max = dataRange.max; - if (min != null && max != null) { - var interval = (max.valueOf() - min.valueOf()); + 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 } - min = new Date(min.valueOf() - interval * 0.05); - max = new Date(max.valueOf() + interval * 0.05); + start = new Date(start.valueOf() - interval * 0.05); + end = new Date(end.valueOf() + interval * 0.05); } // override specified start and/or end date if (this.options.start != undefined) { - min = util.convert(this.options.start, 'Date'); + start = util.convert(this.options.start, 'Date'); } if (this.options.end != undefined) { - max = util.convert(this.options.end, 'Date'); + end = util.convert(this.options.end, 'Date'); } // apply range if there is a min or max available - if (min != null || max != null) { - this.range.setRange(min, max); + if (start != null || end != null) { + this.range.setRange(start, end); } } }; @@ -342,10 +349,128 @@ Timeline.prototype.getItemRange = function getItemRange() { }; /** - * Change the item selection, and/or get currently selected items - * @param {Array} [ids] An array with zero or more ids of the items to be selected. + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ +Timeline.prototype.setSelection = function setSelection (ids) { + if (this.content) this.content.setSelection(ids); +}; + +/** + * Get the selected items by their id * @return {Array} ids The ids of the selected items */ -Timeline.prototype.select = function select(ids) { - return this.content ? this.content.select(ids) : []; +Timeline.prototype.getSelection = function getSelection() { + return this.content ? this.content.getSelection() : []; +}; + +/** + * Add event listener + * @param {String} event Event name. Available events: + * 'rangechange', 'rangechanged', 'select' + * @param {function} callback Callback function, invoked as callback(properties) + * where properties is an optional object containing + * event specific properties. + */ +Timeline.prototype.on = function on (event, callback) { + var available = ['rangechange', 'rangechanged', 'select']; + + if (available.indexOf(event) == -1) { + throw new Error('Unknown event "' + event + '". Choose from ' + available.join()); + } + + events.addListener(this, event, callback); +}; + +/** + * Remove an event listener + * @param {String} event Event name + * @param {function} callback Callback function + */ +Timeline.prototype.off = function off (event, callback) { + events.removeListener(this, event, callback); +}; + +/** + * Trigger an event + * @param {String} event Event name, available events: 'rangechange', + * 'rangechanged', 'select' + * @param {Object} [properties] Event specific properties + * @private + */ +Timeline.prototype._trigger = function _trigger(event, properties) { + events.trigger(this, event, properties || {}); +}; + +/** + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private + */ +Timeline.prototype._onSelectItem = function (event) { + var item = this._itemFromTarget(event); + + var selection = item ? [item.id] : []; + this.setSelection(selection); + + this._trigger('select', { + items: this.getSelection() + }); + + event.stopPropagation(); +}; + +/** + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private + */ +Timeline.prototype._onMultiSelectItem = function (event) { + var selection, + item = this._itemFromTarget(event); + + if (!item) { + // do nothing... + return; + } + + selection = this.getSelection(); // current selection + var index = selection.indexOf(item.id); + if (index == -1) { + // item is not yet selected -> select it + selection.push(item.id); + } + else { + // item is already selected -> deselect it + selection.splice(index, 1); + } + this.setSelection(selection); + + this._trigger('select', { + items: this.getSelection() + }); + + event.stopPropagation(); }; + +/** + * 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 + * @private + */ +Timeline.prototype._itemFromTarget = function _itemFromTarget (event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } + + return null; +}; \ No newline at end of file diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index e95091f2..bf95c0c8 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -76,12 +76,22 @@ Group.prototype.setItems = function setItems(items) { }; /** - * Change the item selection, and/or get currently selected items - * @param {Array} [ids] An array with zero or more ids of the items to be selected. + * Set selected items by their id. Replaces the current selection. + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ +Group.prototype.setSelection = function setSelection(ids) { + if (this.itemset) this.itemset.setSelection(ids); +}; + +/** + * Get the selected items by their id * @return {Array} ids The ids of the selected items */ -Group.prototype.select = function select(ids) { - return this.itemset ? this.itemset.select(ids) : []; +Group.prototype.getSelection = function getSelection() { + return this.itemset ? this.itemset.getSelection() : []; }; /** diff --git a/src/timeline/component/GroupSet.js b/src/timeline/component/GroupSet.js index 79d13fdd..eefd397e 100644 --- a/src/timeline/component/GroupSet.js +++ b/src/timeline/component/GroupSet.js @@ -150,11 +150,32 @@ GroupSet.prototype.getGroups = function getGroups() { }; /** - * Change the item selection, and/or get currently selected items - * @param {Array} [ids] An array with zero or more ids of the items to be selected. + * Set selected items by their id. Replaces the current selection. + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. + */ +GroupSet.prototype.setSelection = function setSelection(ids) { + var selection = [], + groups = this.groups; + + // iterate over each of the groups + for (var id in groups) { + if (groups.hasOwnProperty(id)) { + var group = groups[id]; + group.setSelection(ids); + } + } + + return selection; +}; + +/** + * Get the selected items by their id * @return {Array} ids The ids of the selected items */ -GroupSet.prototype.select = function select(ids) { +GroupSet.prototype.getSelection = function getSelection() { var selection = [], groups = this.groups; @@ -162,7 +183,7 @@ GroupSet.prototype.select = function select(ids) { for (var id in groups) { if (groups.hasOwnProperty(id)) { var group = groups[id]; - selection = selection.concat(group.select(ids)); + selection = selection.concat(group.getSelection()); } } @@ -358,7 +379,7 @@ GroupSet.prototype.repaint = function repaint() { GroupSet.prototype._createLabel = function(id) { var group = this.groups[id]; var label = document.createElement('div'); - label.className = 'label'; + label.className = 'vlabel'; var inner = document.createElement('div'); inner.className = 'inner'; label.appendChild(inner); diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 4b5fa24a..20d7259b 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -112,11 +112,13 @@ ItemSet.prototype.setRange = function setRange(range) { }; /** - * Change the item selection, and/or get currently selected items - * @param {Array} [ids] An array with zero or more ids of the items to be selected. - * @return {Array} ids The ids of the selected items + * Set selected items by their id. Replaces the current selection + * Unknown id's are silently ignored. + * @param {Array} [ids] An array with zero or more id's of the items to be + * selected. If ids is an empty array, all items will be + * unselected. */ -ItemSet.prototype.select = function select(ids) { +ItemSet.prototype.setSelection = function setSelection(ids) { var i, ii, id, item, selection; if (ids) { @@ -152,11 +154,14 @@ ItemSet.prototype.select = function select(ids) { this.requestRepaint(); } } - else { - selection = this.selection.concat([]); - } +}; - return selection; +/** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ +ItemSet.prototype.getSelection = function getSelection() { + return this.selection.concat([]); }; /** @@ -262,80 +267,82 @@ ItemSet.prototype.repaint = function repaint() { }; // show/hide added/changed/removed items - Object.keys(queue).forEach(function (id) { - //var entry = queue[id]; - var action = queue[id]; - var item = items[id]; - //var item = entry.item; - //noinspection FallthroughInSwitchStatementJS - switch (action) { - case 'add': - case 'update': - var itemData = itemsData && itemsData.get(id, dataOptions); - - if (itemData) { - var type = itemData.type || - (itemData.start && itemData.end && 'range') || - options.type || - 'box'; - var constructor = ItemSet.types[type]; - - // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, hide and delete the item - changed += item.hide(); - item = null; + for (var id in queue) { + if (queue.hasOwnProperty(id)) { + var entry = queue[id], + item = items[id], + action = entry.action; + + //noinspection FallthroughInSwitchStatementJS + switch (action) { + case 'add': + case 'update': + var itemData = itemsData && itemsData.get(id, dataOptions); + + if (itemData) { + var type = itemData.type || + (itemData.start && itemData.end && 'range') || + options.type || + 'box'; + var constructor = ItemSet.types[type]; + + // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error? + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, hide and delete the item + changed += item.hide(); + item = null; + } + else { + item.data = itemData; // TODO: create a method item.setData ? + changed++; + } } - else { - item.data = itemData; // TODO: create a method item.setData ? - changed++; - } - } - if (!item) { - // create item - if (constructor) { - item = new constructor(me, itemData, options, defaultOptions); - item.id = id; - changed++; + if (!item) { + // create item + if (constructor) { + item = new constructor(me, itemData, options, defaultOptions); + item.id = entry.id; // we take entry.id, as id itself is stringified + changed++; + } + else { + throw new TypeError('Unknown item type "' + type + '"'); + } } - else { - throw new TypeError('Unknown item type "' + type + '"'); - } - } - // force a repaint (not only a reposition) - item.repaint(); + // force a repaint (not only a reposition) + item.repaint(); - items[id] = item; - } + items[id] = item; + } - // update queue - delete queue[id]; - break; + // update queue + delete queue[id]; + break; - case 'remove': - if (item) { - // remove the item from the set selected items - if (item.selected) { - me._deselect(id); - } + case 'remove': + if (item) { + // remove the item from the set selected items + if (item.selected) { + me._deselect(id); + } - // remove DOM of the item - changed += item.hide(); - } + // remove DOM of the item + changed += item.hide(); + } - // update lists - delete items[id]; - delete queue[id]; - break; + // update lists + delete items[id]; + delete queue[id]; + break; - default: - console.log('Error: unknown action "' + action + '"'); + default: + console.log('Error: unknown action "' + action + '"'); + } } - }); + } // reposition all items. Show items only when in the visible area util.forEach(this.items, function (item) { @@ -546,7 +553,10 @@ ItemSet.prototype._onRemove = function _onRemove(ids) { ItemSet.prototype._toQueue = function _toQueue(action, ids) { var queue = this.queue; ids.forEach(function (id) { - queue[id] = action; + queue[id] = { + id: id, + action: action + }; }); if (this.controller) { diff --git a/src/timeline/component/Panel.js b/src/timeline/component/Panel.js index e3badfae..a6fc5f06 100644 --- a/src/timeline/component/Panel.js +++ b/src/timeline/component/Panel.js @@ -54,7 +54,7 @@ Panel.prototype.repaint = function () { frame = this.frame; if (!frame) { frame = document.createElement('div'); - frame.className = 'panel'; + frame.className = 'vpanel'; var className = options.className; if (className) { diff --git a/src/timeline/component/css/groupset.css b/src/timeline/component/css/groupset.css index b6467a7f..1b497757 100644 --- a/src/timeline/component/css/groupset.css +++ b/src/timeline/component/css/groupset.css @@ -33,7 +33,7 @@ border-bottom: 1px solid #bfbfbf; } -.vis.timeline .labels .label-set .label { +.vis.timeline .labels .label-set .vlabel { position: absolute; left: 0; top: 0; @@ -41,19 +41,19 @@ color: #4d4d4d; } -.vis.timeline.top .labels .label-set .label, +.vis.timeline.top .labels .label-set .vlabel, .vis.timeline.top .groupset .itemset-axis { border-top: 1px solid #bfbfbf; border-bottom: none; } -.vis.timeline.bottom .labels .label-set .label, +.vis.timeline.bottom .labels .label-set .vlabel, .vis.timeline.bottom .groupset .itemset-axis { border-top: none; border-bottom: 1px solid #bfbfbf; } -.vis.timeline .labels .label-set .label .inner { +.vis.timeline .labels .label-set .vlabel .inner { display: inline-block; padding: 5px; } diff --git a/src/timeline/component/css/panel.css b/src/timeline/component/css/panel.css index 819f33f2..ba6c2ae2 100644 --- a/src/timeline/component/css/panel.css +++ b/src/timeline/component/css/panel.css @@ -8,7 +8,7 @@ box-sizing: border-box; } -.vis.timeline .panel { +.vis.timeline .vpanel { position: absolute; overflow: hidden; } diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index 15f9c6ed..cde86f5a 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -260,6 +260,9 @@ ItemBox.prototype._create = function _create() { // dot on axis dom.dot = document.createElement('DIV'); dom.dot.className = 'dot'; + + // attach this item as attribute + dom.box['timeline-item'] = this; } }; diff --git a/src/timeline/component/item/ItemPoint.js b/src/timeline/component/item/ItemPoint.js index f42d6322..1a78a92b 100644 --- a/src/timeline/component/item/ItemPoint.js +++ b/src/timeline/component/item/ItemPoint.js @@ -210,6 +210,9 @@ ItemPoint.prototype._create = function _create() { dom.dot = document.createElement('div'); dom.dot.className = 'dot'; dom.point.appendChild(dom.dot); + + // attach this item as attribute + dom.point['timeline-item'] = this; } }; diff --git a/src/timeline/component/item/ItemRange.js b/src/timeline/component/item/ItemRange.js index 92774683..a2feec99 100644 --- a/src/timeline/component/item/ItemRange.js +++ b/src/timeline/component/item/ItemRange.js @@ -226,6 +226,9 @@ ItemRange.prototype._create = function _create() { dom.content = document.createElement('div'); dom.content.className = 'content'; dom.box.appendChild(dom.content); + + // attach this item as attribute + dom.box['timeline-item'] = this; } }; diff --git a/test/dataset.js b/test/dataset.js index 4934a7f3..77619b9c 100644 --- a/test/dataset.js +++ b/test/dataset.js @@ -143,5 +143,23 @@ assert.deepEqual(data.getIds({ }), [3,1]); +data.clear(); + + +// test if the setting of the showInternalIds works locally for a single get request +data.add({content: 'Item 1'}); +data.add({content: 'Item 2'}); + +assert.strictEqual(data.get()[0].id, undefined); +assert.deepEqual((data.get({"showInternalIds": true})[0].id == undefined),false); +assert.deepEqual(data.isInternalId(data.get({"showInternalIds": true})[0].id), true); +assert.deepEqual((data.get()[0].id == undefined), true); + +// check if the global setting is applied correctly +var data = new DataSet({showInternalIds: true}); +data.add({content: 'Item 1'}); +assert.deepEqual((data.get()[0].id == undefined), false); +assert.deepEqual(data.isInternalId(data.get()[0].id), true); +assert.deepEqual((data.get({"showInternalIds": false})[0].id == undefined),true); // TODO: extensively test DataSet